private means "no receiver"¶
Level: 201 · a reader who has seen private and assumed it works like Java's or Python's
One line: A private method can be called only without an explicit receiver (or with self.), a protected method only from inside the same class on any instance of it — and send walks past both while public_send does not.
Ruby's visibility rules are about the call site, not about which class the caller belongs to. A private method may be called as a bare name, audit, or since Ruby 2.7 as self.audit; any other receiver, even another instance of the same class, is refused with NoMethodError. A protected method is the middle setting: other.balance works inside Account's own methods, which is what a == comparing two accounts needs, and fails from outside. private is not a lock either — send(:audit) calls anything, which is why public_send exists for dispatching a name you did not write yourself. There are three ways to mark a method private (a private section, private :name afterwards, and private def name, which works because def returns the method's name as a Symbol), private_class_method :new hides a constructor behind a factory, and initialize is private automatically. The program builds an Account that exercises every one of those.
Verified output of private_means_no_receiver_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. acct.audit -- private, explicit receiver NoMethodError: private method 'audit' called for an instance of Account
2. inside the class, plain audit "audited ann"
3. inside the class, self.audit (since 2.7) "audited ann"
4. protected balance: acct == other / acct.balance true / NoMethodError: protected method 'balance' called for an instance of Account
5. private_class_method :new -- Account.new NoMethodError: private method 'new' called for class Account; Account.open works: #<Account x>
6. send(:audit) / public_send(:audit) "audited ann" / NoMethodError: private method 'audit' called for an instance of Account
7. private_instance_methods(false).sort [:audit, :helper1, :helper2, :initialize, :total=]
8. protected / public instance methods (false) [:balance] / [:==, :inspect, :receiverless_call, :self_dot_call, :set_through_self]
9. a private writer called as self.total = 1 1
10. respond_to?(:audit) / respond_to?(:audit, true) false / true
Reading the output¶
Row 1 is the rule: acct.audit names a receiver, so the private method is refused, and the message says why. Rows 2 and 3 are the two allowed spellings from inside the class — the bare name, and self.audit, which Ruby 2.7 made legal so that a private method and a private writer (row 9, self.total = 1, which always had to be written that way) follow one rule. Row 4 is protected: acct == other reaches other.balance from inside Account, and the same balance called from the top level is refused with a message that says protected rather than private.
Row 5 hides the constructor: after private_class_method :new, Account.new is a private call on the class and the factory Account.open — which calls new receiverless — is the only way in. Row 6 is the pair to remember when metaprogramming: send ignores visibility and public_send respects it. Row 7 lists what the three spellings produced, plus initialize, which Ruby makes private without being asked, and total=, the private writer. Row 8 lists the protected and public methods, so you can see that private did not swallow anything defined after the public keyword. Row 10 shows that respond_to? answers false for a private method unless you pass true as the second argument.
Compared with Python¶
Python has no private keyword and enforces nothing: the twin's row 1 calls acct._audit() from outside and it simply works, because the leading underscore is a convention documented in PEP 8, not a rule the interpreter checks. Row 2 is the deepest difference: Ruby's "no receiver" spelling does not exist in Python — a bare _audit() inside a method is looked up as a global and raises NameError, and self._audit() (row 3) is the only way to call a method. Nothing is protected either (row 4: acct._balance reads fine from outside), a constructor cannot be made private (row 5: Account("x", 0) still works beside the @classmethod factory), and getattr reaches every name, so there is no public_send to contrast it with (row 6). What Python does have is name mangling: a name with two leading underscores is stored as _Account__vault (row 7), so acct.__vault() fails with AttributeError while acct._Account__vault() succeeds (row 8) — a guard against accidental clashes in subclasses, not a privacy mechanism. hasattr (row 10) sees every unmangled name, where Ruby's respond_to? hides private ones by default.
Verified output of private_means_no_receiver_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. acct._audit() -- convention only, explicit receiver 'audited ann'
2. inside the class, plain _audit() NameError
3. inside the class, self._audit() 'audited ann'
4. no protected: acct == other / acct._balance True / 0
5. no private constructor -- Account("x", 0) <Account x>; Account.open works: <Account x>
6. getattr(acct, "_audit")() / no public_send 'audited ann' / getattr reaches every name
7. names in vars(Account) that start with _ ['_Account__vault', '_audit', '_helper1']
8. acct.__vault() / acct._Account__vault() AttributeError / 'vault'
9. a "private" attribute set as self._total = 1 1
10. hasattr(acct, "_audit") / hasattr(acct, "__vault") True / False
The conventions are spelled out in PEP 8, Naming Conventions ↗ and the mangling rule in the tutorial under Private Variables ↗.
Try it¶
- Write a
Vaultwhoseopen(code)is public, whosecombinationreader is protected (so that==can compare two vaults) and whoseaudit_logis private. From the top level call each one, then reach the private one withsendand showpublic_sendrefusing it. - Make
audit_logprivate withprivate def audit_loginstead of aprivatesection, then confirm withVault.private_instance_methods(false)that both spellings produce the same list — and tryprivate :audit_logbefore thedefto see why the order matters:privatewith a name looks the method up, so a name that exists nowhere yet raisesNameError. (Not machine-checked here.)
Solution to 1
private_means_no_receiver_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: a Vault whose `open` is public, whose `combination` is protected
# (so two vaults can compare themselves) and whose `audit_log` is private.
class Vault
def initialize(combination)
@combination = combination
end
def inspect = "#<Vault>"
def open(code) = code == combination ? "open" : "locked"
def ==(other) = other.is_a?(Vault) && combination == other.combination
protected
def combination = @combination
private
def audit_log = "audit: opened by #{combination}"
end
def attempt
yield
rescue NoMethodError => e
"#{e.class}: #{e.message}"
end
v = Vault.new(1234)
puts "v.open(1234): #{v.open(1234)}"
puts "v.open(1): #{v.open(1)}"
puts "v == Vault.new(1234): #{v == Vault.new(1234)}"
puts "v == Vault.new(9): #{v == Vault.new(9)}"
puts "v.combination: #{attempt { v.combination }}"
puts "v.audit_log: #{attempt { v.audit_log }}"
puts "v.send(:audit_log): #{v.send(:audit_log)}"
puts "v.public_send(:audit_log): #{attempt { v.public_send(:audit_log) }}"
Verified output of private_means_no_receiver_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
v.open(1234): open
v.open(1): locked
v == Vault.new(1234): true
v == Vault.new(9): false
v.combination: NoMethodError: protected method 'combination' called for an instance of Vault
v.audit_log: NoMethodError: private method 'audit_log' called for an instance of Vault
v.send(:audit_log): audit: opened by 1234
v.public_send(:audit_log): NoMethodError: private method 'audit_log' called for an instance of Vault
See also¶
- Instance variables are private — the other privacy rule: an
@variablehas no receiver at all - Method lookup and
super— where the receiver's class is searched once visibility lets the call through selfis implicit — what a receiverless call is a call onsendandpublic_send— row 6 as a whole lesson: dispatching by a name built at runtimenew,allocateandinitialize— whyinitializeis private and whatprivate_class_method :newhides- Modules and visibility ↗ — a third design: Rust's
pubis decided per module, checked at compile time - Common error messages — Twenty-nine errors you will meet in your first month of Ruby, each with the code that raises it, the…
- Twenty-three Ruby traps for a Python programmer — The twenty-three places where a Python reflex gives the wrong answer in Ruby