Skip to content

send reaches private methods; public_send does not

Level: 301 · for a reader who knows what private means in Ruby and wants to call a method whose name is computed at run time

One line: obj.send(:name, *args) calls a method by name and ignores visibility, public_send honours it, and __send__ still works on an object whose class has its own send — while Python's getattr(obj, "name")(*args) has no visibility to ignore, only a naming convention and a compile-time mangling rule.

A Ruby method call is a message: calc.add(2, 3) sends :add with two arguments to calc. send lets you compose that message from data — a Symbol, a String, or a name built by interpolation — which is what every serializer, test framework and ORM does. It is defined on Kernel, and it has one property that surprises people: it ignores private and protected. That is deliberate (tests and libraries use it to reach internals), and it is why Ruby 1.9 added public_send, which dispatches the same way but refuses a private method exactly as an explicit receiver would (the history is not machine-checked here). The third spelling, BasicObject#__send__, exists because send is an ordinary method name that a class may take for itself — a Mailer or a socket wants a send — and the underscored name is the one nobody overrides. The program measures all three, plus respond_to?, the question to ask before sending a name that came from outside.

Verified output of send_and_public_send_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. calc.send(:add, 2, 3)                          5
 2. calc.send("add", 2, 3) -- a String works too   5
 3. Report.new.send(:"to_#{fmt}"), fmt = "csv"     "a,b"
 4. 1.send(:+, 2) -- operators are methods         3
 5. [1, 2, 3].send(:map) { it * 2 } -- block passes [2, 4, 6]
 6. vault.send(:code) -- a reader is a method call 42
 7. vault.send(:secret) -- private, still called   "the code is 42"
 8. vault.public_send(:secret)                     NoMethodError: private method 'secret' called for an instance of Vault
 9. respond_to?(:secret) / respond_to?(:secret, true) false / true
10. vault.send(:nope) -> e.class, e.name           NoMethodError, :nope
11. mailer.send(:deliver) -- send is overridden    "mailing :deliver"
    mailer.__send__(:deliver)                      "delivered"
    mailer.public_send(:deliver)                   "delivered"
12. owner of send / __send__ / public_send         Kernel / BasicObject / Kernel
13. calc.send(:add, 1) -- arity is still checked   ArgumentError: wrong number of arguments (given 1, expected 2)

Reading the output

Rows 1–5 are the ordinary uses: the name may be a Symbol or a String, it may be assembled at run time (:"to_#{fmt}"), an operator is just a method with a strange name, and a block passes through send like any other call. Row 6 shows there is no separate notion of "attribute" to fetch — attr_reader :code defined a method, so send(:code) is a call. Rows 7 and 8 are the lesson: send calls the private secret as if it were public, while public_send raises the same NoMethodError that vault.secret would, with the same "private method 'secret' called for an instance of Vault" message. Row 9 is the polite way to find out in advance: respond_to? answers false for a private method unless you pass true as its second argument. Row 10 shows what an unknown name costs — a NoMethodError whose name attribute carries the Symbol you sent — because send has no default-value form. Row 11 is why __send__ exists: Mailer defines its own send, so mailer.send(:deliver) runs the mailer's method with :deliver as its message argument; __send__ and public_send still dispatch. Row 12 locates the three methods: send and public_send live in Kernel, __send__ in BasicObject, so even a BasicObject proxy has __send__. Row 13: send relaxes visibility and nothing else — the arity check still fires.

Compared with Python

The twin uses getattr, which does not call anything: it returns the attribute, and you call the result, so getattr(vault, "_code") hands back the integer and callable says False (row 6) where Ruby's send(:code) ran a method. Python has no private: one leading underscore is a convention that getattr ignores as readily as send ignores private (row 7). Two leading underscores are mangled at compile time to _Vault__mangled, so getattr(vault, "__mangled") fails not because access is forbidden but because no attribute has that name, and the mangled name works (row 8) — the only wall Python builds is a naming wall, which is why there is no public_send. hasattr is respond_to?, and it simply tries the lookup (row 9). getattr takes a default where Ruby makes you rescue or ask first (row 10). Because getattr is a builtin function and not a method on the object, a class cannot shadow it: the Mailer twin's own getattr method is just another method, and nothing plays the role of __send__ (row 11). operator.methodcaller (row 2) packages a name and arguments for later, the way a Ruby lambda around send would, and operator.add is the function form of + (row 4). Row 13 is the same in both: dispatching by name does not relax the argument count.

Verified output of send_and_public_send_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. getattr(calc, "add")(2, 3)                     5
 2. operator.methodcaller("add", 2, 3)(calc)       5
 3. getattr(Report(), f"to_{fmt}")(), fmt = "csv"  'a,b'
 4. (1).__add__(2) / operator.add(1, 2)            3 / 3
 5. list(map(lambda x: x * 2, [1, 2, 3]))          [2, 4, 6]
 6. getattr(vault, "_code") / callable(...)        42 / False
 7. getattr(vault, "_secret")() -- convention only 'the code is 42'
 8. getattr(vault, "__mangled")                    AttributeError: no attribute '__mangled'
    getattr(vault, "_Vault__mangled")              7
 9. hasattr: "_secret" / "__mangled" / "_Vault__mangled" True / False / True
10. getattr(vault, "nope") -> type, e.name         AttributeError, 'nope'
    getattr(vault, "nope", None) -- a default      None
11. getattr(mailer, "deliver")() -- a builtin, not a method 'delivered'
    mailer.getattr("deliver") -- the class's own method 'shadow'
    no __send__ needed                             getattr cannot be shadowed by the class
12. getattr.__module__ / type(getattr).__name__    builtins / builtin_function_or_method
13. getattr(calc, "add")(1) -- arity is still checked TypeError

Try it

  1. Write dispatch(obj, name, *args) that returns :unknown unless obj.respond_to?(name) and otherwise calls obj.public_send(name, *args). Try it on Calc#add, on a private Vault#secret, on a misspelt name, and once with a name built by format.
  2. Give a socket-like class its own send(bytes) method and show three ways to still call another of its methods by a run-time name: __send__, public_send and method(name).call. Which of the three still refuses a private method, and what does respond_to? need in order to say true for it?
Solution to 1

send_and_public_send_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# send_and_public_send_kata_rb.rb — dispatch(obj, name, *args): ask before sending.

class Calc
  def add(a, b) = a + b
end

class Vault
  private
  def secret = "the code"
end

def dispatch(obj, name, *args)
  return :unknown unless obj.respond_to?(name)   # private methods answer false here
  obj.public_send(name, *args)
end

puts "1. dispatch(Calc.new, :add, 2, 3)      -> #{dispatch(Calc.new, :add, 2, 3).inspect}"
puts "2. dispatch(Vault.new, :secret)        -> #{dispatch(Vault.new, :secret).inspect}"
puts "3. dispatch(Calc.new, :addd, 2, 3)    -> #{dispatch(Calc.new, :addd, 2, 3).inspect}"
op = "add"
puts "4. dispatch(Calc.new, format(\"%s\", op), 5, 6) -> #{dispatch(Calc.new, format("%s", op), 5, 6).inspect}"

Verified output of send_and_public_send_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. dispatch(Calc.new, :add, 2, 3)      -> 5
2. dispatch(Vault.new, :secret)        -> :unknown
3. dispatch(Calc.new, :addd, 2, 3)    -> :unknown
4. dispatch(Calc.new, format("%s", op), 5, 6) -> 11

See also