skip to content

In Ruby, what does Module#private_constant block, and which ways of reaching a private constant still work?

level: middleimportance: should knowfreq 30%

answer

  1. blocks only explicit :: paths
  2. NameError: private constant referenced
  3. bare name inside the namespace works
  4. const_get still returns it
  5. private keyword does not apply

basics

~20 s

Module#private_constant makes a scoped reference such as Ingest::JSON::Tokenizer raise NameError. Bare references from code inside the namespace, subclasses or includers still work, and const_get still returns it; it signals internal API, not a security boundary.

solid answer

~30 s

`private_constant :Tokenizer` inside `module Ingest::JSON` marks an existing constant private. Any **explicit scoped reference**, `Ingest::JSON::Tokenizer` from outside or even `JSON::Tokenizer` written inside the namespace, raises `NameError` with `private constant Ingest::JSON::Tokenizer referenced`. A **bare** `Tokenizer` written inside the namespace still works, as does a bare reference from a subclass or a class that includes the module, because unscoped lookup ignores constant visibility. `Module#const_get` also returns the value, and `Module#constants` leaves it out of its list. The `private` keyword affects methods only, so writing it above a constant does nothing for the constant. The constant must already exist when `private_constant` is called.

code

ruby · 17 lines
ruby
module Ingest
  module JSON
    class Tokenizer; end
    private_constant :Tokenizer
  end
end

Ingest::JSON.const_get(:Tokenizer) # => Ingest::JSON::Tokenizer
Ingest::JSON.constants             # => []

class JsonImporter
  include Ingest::JSON
  def build = Tokenizer.new # bare name via the included module: allowed
end

JsonImporter.new.build              # => a Tokenizer instance
Ingest::JSON::Tokenizer             # NameError: private constant Ingest::JSON::Tokenizer referenced

go deeper

for a junior

Know that private_constant exists to hide a constant from outside Mod::NAME references and that the private keyword does not do this.

for a middle

Explain that the check applies only to scoped references, list what still works (bare names, const_get, includers) and name the NameError message.

for a senior

Use private_constant to shape a gem's public surface, explain why it is not a security boundary, and plan a deprecation path with deprecate_constant before hiding a constant callers already use.

for a principal

Decide what a shared library promises as public API and make everything else private_constant, trading a small annotation cost for the freedom to refactor internals without breaking users.

## What private_constant is for A namespace groups constants, but by default every constant is public: any code can write `Ingest::JSON::Tokenizer` and start depending on it. `Module#private_constant` lets a library say "this class is an implementation detail". It takes one or more symbols naming **existing** constants in the receiver and returns the module. ```ruby module Ingest module JSON class Tokenizer; end private_constant :Tokenizer class Parser def tokens(raw) = Tokenizer.new # bare reference: allowed end end end Ingest::JSON::Tokenizer # NameError: private constant Ingest::JSON::Tokenizer referenced ``` ## What is blocked and what is not The rule is about the **form of the reference**, not about who makes it: | Reference | Result | |---|---| | `Ingest::JSON::Tokenizer` from anywhere | `NameError` | | `Tokenizer` inside `module Ingest::JSON` or a class nested in it | allowed (lexical lookup) | | `Tokenizer` inside a class that includes `Ingest::JSON` | allowed (ancestor lookup) | | `JSON::Tokenizer` or `Ingest::JSON::Tokenizer` written inside the namespace | `NameError` | | `Ingest::JSON.const_get(:Tokenizer)` | returns the class | | `Ingest::JSON.constants` | list without `:Tokenizer` | | `Ingest::JSON.const_defined?(:Tokenizer)` | `true` | A bare constant reference, resolved through the lexical nesting and the ancestors, does not check visibility at all. An explicit `Mod::NAME` path always does, even when written inside `Mod` itself. ## Common misunderstandings 1. **The `private` keyword.** `private` with no arguments changes the visibility of methods defined after it. Constants defined below it stay public. Only `private_constant` changes a constant. 2. **Order.** `private_constant :Tokenizer` must come after `Tokenizer` is defined; naming a constant that does not exist raises `NameError`. 3. **Reassignment.** Setting the constant again keeps the private flag. 4. **Undoing it.** `public_constant :Tokenizer` makes it public again. 5. **Security.** `const_get` still returns the constant and any code can reopen the module, so privacy is a statement of intent to other developers, not an access control. ## Why a library uses it - It keeps the public surface of a gem small: users see `Ingest::JSON::Parser` and cannot reach the tokenizer through a documented-looking path by accident. - It turns an accidental dependency into an immediate `NameError` instead of a breakage months later when the internal class is renamed. - It pairs naturally with nested namespaces, since code inside the namespace keeps using the bare name. ## Testing code that uses a private constant Tests are where private constants most often cause friction. A spec written outside the namespace cannot write `Ingest::JSON::Tokenizer.new` directly. There are three honest options: - Test through the public class (`Ingest::JSON::Parser`) that uses the tokenizer, which is usually the point of making it private. - Reach it deliberately with `Ingest::JSON.const_get(:Tokenizer)`, which makes the dependency on an internal visible in the test code. - Reconsider the design: if many callers need the class, it is public API and `private_constant` is the wrong annotation. Changing the constant back to public just to make a test pass hides the design question instead of answering it. ## A related tool: deprecate_constant When a public constant has to go away, `Module#deprecate_constant` keeps it working but emits a deprecation warning when it is referenced (visible when deprecation warnings are enabled). A typical path is to deprecate the old public name for a release and then make it private or remove it.

  • Can a subclass or includer reach a private constant with a bare name?
    Yes. A bare reference searches the lexical nesting and then the ancestors without checking constant visibility, so a class that includes the module or inherits from the class can write the plain name. Only a scoped `Mod::NAME` reference is refused.
  • How would you retire a public constant without breaking callers in one release?
    Mark it with `Module#deprecate_constant` first so references keep working but warn when deprecation warnings are on, give callers a release to move, and then make it private with `private_constant` or remove it.

saying these in an interview costs you the question

  • Writing private above a constant definition makes the constant private
  • private_constant hides the constant from const_get as well
  • A private constant cannot be used even inside its own module
  • private_constant can be called before the constant is defined
  • private_constant stops other code from reopening the module