Skip to content

method_missing needs respond_to_missing?

Level: 301 · for a reader who has met a "ghost" method such as find_by_name in a library and wants to write one that behaves

One line: method_missing(name, *args) runs only after normal lookup has failed, and it must call super for names it does not handle; on its own it leaves respond_to? and method blind, so every ghost needs a matching respond_to_missing? — while Python's __getattr__ is likewise called only on failure, but hasattr and getattr see through it with no second hook.

When Ruby cannot find a method for a message it does not raise at once: it sends method_missing with the name and the arguments, and BasicObject's version raises the NoMethodError you know. Overriding it lets an object answer names it never defined — an ORM answers find_by_name, a proxy forwards everything — and the rule is that whatever the override does not understand goes to super, so typos still fail. That alone is half a feature. respond_to? does not ask method_missing, and neither does method, so a ghost method is invisible to every caller who checks before calling. respond_to_missing? is the second half: it is what respond_to? and method consult for unknown names, and it must say true for exactly the names method_missing accepts. The program measures a plain Db without it and a ListedDb with it, then the three classic mistakes: swallowing everything, forgetting that real methods never reach the hook, and calling the hook by hand. In prose only: every ghost call is a failed lookup followed by a Ruby-level hook, which is slower than a real method, so libraries define the method on first use, as Memo does (the cost is not machine-checked here).

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

 1. db.find_by_name("Ada") -- a ghost method       {name: "Ada", lang: "Ruby"}
 2. Record.new(name: "Ada").name -- attribute ghost "Ada"
 3. db.nope -- super keeps the error               NoMethodError: undefined method 'nope' for an instance of Db
 4. Probe.new.find_by_x("Ada") receives            [Symbol, :find_by_x, ["Ada"]]
 5. db.respond_to?(:find_by_name) -- no respond_to_missing? false
    db.method(:find_by_name)                       NameError: undefined method 'find_by_name' for class 'Db'
 6. listed.respond_to?(:find_by_name)              true
    listed.method(:find_by_lang).call("Python")    {name: "Guido", lang: "Python"}
 7. listed.methods.include?(:find_by_name)         false
 8. hook_calls: 2 ghosts (rows 1, 3), then rows, rows 2 (real methods never reach the hook)
 9. Swallow.new.typo -- method_missing without super nil
    Swallow.new.respond_to?(:typo)                 false
10. memo.say_hello twice -> value, hits            "HELLO" "HELLO", hits = 1
    Memo.instance_methods(false).include?(:say_hello) true
11. db.method_missing(:find_by_name, "Ada") explicitly NoMethodError: private method 'method_missing' called for an instance of Db
12. from row 3: e.name / e.receiver.equal?(db)     :nope / true

Reading the output

Rows 1 and 2 are the two shapes of a ghost: a name parsed for a field (find_by_name → :name) and a name looked up in a Hash (record.name). Row 3 is the super rule: db.nope still raises NoMethodError with the ordinary message, and row 12 (printed last, from the exception row 3 rescued) shows it carries the Symbol in name and the object in receiver. Row 4 prints what the hook receives — the name is a Symbol, and the arguments arrive with it, because in Ruby the call and the lookup are one event. Row 5 is the problem: Db has no respond_to_missing?, so respond_to?(:find_by_name) is false and method(:find_by_name) raises NameError, even though the call in row 1 worked. Row 6 is the fix: ListedDb adds a respond_to_missing? that says true for find_by_ names, and now both respond_to? and method — a real Method object you can call — work. Row 7: methods still does not list the ghost; it lists defined methods only. Row 8: by this point db has received two ghost messages (find_by_name in row 1, nope in row 3) and, just before the row, two calls to the real rows; the hook ran exactly twice — lookup succeeds for rows, so the hook is never consulted. Row 9 is the bug to avoid: a method_missing that returns nil for everything turns every typo into nil, and respond_to? still says false, so nothing warns you. Row 10 is the idiom libraries use: the first say_hello goes through the hook, which defines a real method, so the second call is ordinary and hits stays at 1. Row 11: method_missing is private (here explicitly, in BasicObject by design), so calling it directly with a receiver is itself a NoMethodError.

Compared with Python

Python's __getattr__(self, name) is the same hook with one structural difference: it is an attribute hook, not a call hook. db.find_by_name is a lookup that returns something, and the parentheses call it afterwards, so __getattr__ receives a str name and no arguments (row 4) and returns a callable — a closure over the field — for the call that follows. Raising AttributeError is the super (row 3), and the exception carries name and obj just as Ruby's carries name and receiver (row 12). The largest difference is row 5: hasattr is implemented as "try getattr, catch AttributeError", so a ghost is visible to hasattr and getattr with no second hook — there is no respond_to_missing? because there is nothing to fix. What Python lacks is a listing: dir(db) does not know about ghosts, and the counterpart of respond_to_missing? for listing is __dir__ (row 7), which ListedDb overrides. Row 8 measures the two Python hooks side by side: __getattribute__ runs on every attribute access, __getattr__ only when that fails — Counted saw three accesses but one ghost. Row 9 is the same bug, worse: a __getattr__ that never raises makes hasattr answer True for every name. Row 10 memoises with setattr on the instance, so the second access is a plain __dict__ hit. Row 11: the hook is a public dunder method, so db.__getattr__("find_by_name") is an ordinary call where Ruby's db.method_missing(...) is refused.

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

 1. db.find_by_name("Ada") -- a ghost method       {'name': 'Ada', 'lang': 'Ruby'}
 2. Record({"name": "Ada"}).name -- attribute ghost 'Ada'
 3. db.nope -- raise AttributeError keeps it       AttributeError
 4. Probe().find_by_x("Ada") receives              ['str', 'find_by_x', 'args arrive at the call, later']
 5. hasattr(db, "find_by_name") -- no extra hook needed True
    callable(getattr(db, "find_by_name"))          True
 6. hasattr(listed, "find_by_name")                True
    getattr(listed, "find_by_lang")("Python")      {'name': 'Guido', 'lang': 'Python'}
 7. "find_by_name" in dir(db) / in dir(listed)     False / True
 8. Counted: real, real, ghost -> always / only_missing 3 / 1
 9. Swallow().typo -- __getattr__ that never raises None
    hasattr(Swallow(), "typo")                     True
10. memo.say_hello() twice -> value, hits          'HELLO' 'HELLO', hits = 1
    "say_hello" in vars(memo)                      True
11. db.__getattr__("find_by_name")("Ada") explicitly {'name': 'Ada', 'lang': 'Ruby'}
12. from row 3: e.name / e.obj is db               'nope' / True

Try it

  1. Write a Settings object whose method_missing turns settings.host = "x" into a Hash write and settings.host into a Hash read, with a respond_to_missing? that says true for any writer and for every key already set; show respond_to?(:host=), respond_to?(:port), method(:host).call, and that settings.nope! still raises.
  2. Build a Proxy < BasicObject that forwards every message to a wrapped object through method_missing and __send__, logging each name; then make respond_to? work on it — which class's respond_to? do you have to reach for, since BasicObject does not have one?
Solution to 1

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

# method_missing_and_respond_to_missing_kata_rb.rb — an open Settings object.

class Settings
  def initialize = @data = {}

  def method_missing(name, *args)
    if name.end_with?("=") && args.size == 1
      @data[name.to_s.chomp("=").to_sym] = args.first    # settings.host = "x"
    elsif @data.key?(name) && args.empty?
      @data[name]                                        # settings.host
    else
      super                                              # anything else is still an error
    end
  end

  def respond_to_missing?(name, include_private = false)
    name.end_with?("=") || @data.key?(name) || super
  end
end

s = Settings.new
s.host = "example.org"
puts "1. s.host after s.host = \"example.org\" -> #{s.host.inspect}"
puts "2. s.respond_to?(:host=)                -> #{s.respond_to?(:host=)}"
puts "3. s.respond_to?(:host) / (:port)       -> #{s.respond_to?(:host)} / #{s.respond_to?(:port)}"
puts "4. s.method(:host).call                 -> #{s.method(:host).call.inspect}"
begin
  s.nope!
rescue NoMethodError => e
  puts "5. s.nope!                              -> #{e.class}: #{e.message}"
end

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

1. s.host after s.host = "example.org" -> "example.org"
2. s.respond_to?(:host=)                -> true
3. s.respond_to?(:host) / (:port)       -> true / false
4. s.method(:host).call                 -> "example.org"
5. s.nope!                              -> NoMethodError: undefined method 'nope!' for an instance of Settings

See also