Skip to content

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

  1. Write a Vault whose open(code) is public, whose combination reader is protected (so that == can compare two vaults) and whose audit_log is private. From the top level call each one, then reach the private one with send and show public_send refusing it.
  2. Make audit_log private with private def audit_log instead of a private section, then confirm with Vault.private_instance_methods(false) that both spellings produce the same list — and try private :audit_log before the def to see why the order matters: private with a name looks the method up, so a name that exists nowhere yet raises NameError. (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