skip to content

A ReadOnlyTemperature subclass must stop responding to an inherited celsius= writer; in Ruby, why does remove_method fail there while undef_method works?

level: seniorimportance: nice to knowfreq 22%

answer

  1. own method table only
  2. lookup continues upward
  3. NameError: not defined in
  4. undef marks a dead end
  5. respond_to? turns false

basics

~20 s

remove_method deletes only a method the class itself defines, so on an inherited writer it raises NameError, and even when it succeeds lookup falls through to the parent. undef_method blocks the name in that class, so calls raise NoMethodError.

solid answer

~30 s

`remove_method` deletes an entry from **that class's own method table**; lookup then continues to the superclass and included modules. `celsius=` lives in `Temperature`, not in `ReadOnlyTemperature`, so `remove_method :celsius=` raises `NameError` ("method 'celsius=' not defined in ReadOnlyTemperature"). `undef_method :celsius=`, or the `undef` keyword, stores an **undefined marker** in the subclass that stops lookup there: calls raise `NoMethodError`, `respond_to?(:celsius=)` is `false`, and the parent keeps its writer. Both return the class and accept Strings. The design question is the bigger one: undefining an inherited method breaks substitutability, so a read-only wrapper or composition is usually the better model.

code

ruby · 14 lines
ruby
module Metric
  def unit = "metric"
end

class Temperature
  include Metric
  def unit = "celsius"
end

Temperature.new.unit                 # => "celsius"
Temperature.remove_method(:unit)
Temperature.new.unit                 # => "metric"  (lookup reaches Metric)
Temperature.undef_method(:unit)
Temperature.new.respond_to?(:unit)   # => false

go deeper

for a junior

Recall that remove_method deletes a class's own definition while undef_method makes the class stop responding to the name.

for a middle

Walk through lookup: remove lets the search continue to parents and modules, undef leaves a marker that stops it, and each raises NameError on misuse.

for a senior

Treat undefining inherited public methods as a design smell that breaks substitutability, and reach for composition or immutable value objects before editing method tables.

for a principal

Set a policy for when metaprogramming that edits method tables is acceptable in shared code, given its cost to readability and to tools that rely on stable interfaces.

## How method lookup meets the two methods When Ruby calls `obj.celsius=`, it walks the object's **ancestor chain** (its class, then modules and superclasses) and runs the first matching method entry. `remove_method` and `undef_method` both edit one class's method table, but they leave very different entries behind: | | `remove_method :name` | `undef_method :name` / `undef name` | |---|---|---| | Requires | the class itself defines `name` | `name` is found somewhere in the lookup chain | | Leaves behind | nothing; the entry is deleted | an "undefined" marker entry | | Lookup afterwards | continues to superclass and modules | stops at this class | | Call afterwards | parent's version runs, if any | `NoMethodError` | | Error when misused | `NameError`: not defined in the class | `NameError`: undefined method for the class | | Returns | the class (`self`) | the class (`self`) | Both have been **public** methods of `Module` since Ruby 2.5 and accept Strings as well as Symbols. ## The read-only subclass traced ```ruby class Temperature attr_accessor :celsius end class ReadOnlyTemperature < Temperature end ReadOnlyTemperature.remove_method(:celsius=) # NameError: method 'celsius=' not defined in ReadOnlyTemperature ReadOnlyTemperature.undef_method(:celsius=) t = ReadOnlyTemperature.new t.respond_to?(:celsius=) # => false t.celsius = 5 # NoMethodError: undefined method 'celsius=' Temperature.new.celsius = 5 # parent unaffected ``` 1. `remove_method` fails because the subclass table has no `celsius=` entry to delete. Had the subclass overridden the writer, removal would succeed and the **parent's writer would reappear**, the opposite of the goal. 2. `undef_method` succeeds because the name is findable through inheritance. The marker it adds makes lookup stop, so the subclass and its own subclasses no longer respond to it. 3. After an undef, a call still goes through the missing-method path, so a `method_missing` defined on the class would receive it. ## When each one is the right tool - **`remove_method`** undoes a definition in the same class: restoring inherited or module behaviour after an override, or clearing a method before redefining it without a redefinition warning. - **`undef_method` / `undef`** makes a class deliberately not respond to a name, for example blank-slate proxies that must not answer ordinary `Object` methods. - Ruby warns when either is used on `object_id`, `__id__`, `__send__` or `initialize` ("may cause serious problems"), because the runtime and libraries rely on them. ## Inspecting what happened After either operation, reflection shows the difference: - `method_defined?(:celsius=)` is `false` after an undef, because lookup stops at the marker; after a remove it is `true` again if an ancestor defines the name. - `instance_methods(false)` lists only the class's own defined methods, so neither a removed nor an undefined name appears. - `Module#undefined_instance_methods`, added in Ruby 3.2, lists the names a class has undefined itself, not those undefined in its ancestors. - The `undef` keyword accepts several names at once, `undef celsius=, reset`, and works anywhere `undef_method` would, with the same effect. ## The design judgement Undefining an inherited public method makes the subclass fail wherever a `Temperature` is expected, which violates the substitution a subclass promises. Alternatives that usually model "read-only" better: - **Composition:** a `TemperatureView` that holds a `Temperature` and exposes only readers. - **An immutable value object** created with the final value and frozen, so no writer exists in the first place. - **Raising a domain error** from an overriding writer when the class must stay substitutable but reject writes at runtime. In an interview, the strong answer explains the lookup mechanics first and then questions whether the subclass should exist at all.

  • In Ruby, if a Temperature class overrides `unit` and includes a module that also defines `unit`, what does `remove_method(:unit)` expose?
    The module's `unit`. Removing the class's own entry lets lookup continue along the ancestor chain, and an included module sits right after the class. To make `unit` unavailable on the class altogether, use `undef_method(:unit)` instead.
  • What does `undef_method :nope` raise in Ruby when no class in the ancestry defines `nope`?
    `NameError`, with a message of the form "undefined method 'nope' for class 'Temperature'". `undef_method` needs the name to be findable through lookup before it can mark it undefined, just as `remove_method` needs a local definition.

saying these in an interview costs you the question

  • remove_method on a subclass deletes the method from the parent class too.
  • undef_method removes the method from the superclass for every subclass.
  • remove_method and undef_method are two names for the same operation.
  • After undef_method, respond_to? still returns true because the parent defines it.
  • remove_method silently ignores a name the class does not define.