Hooks fire when code is defined, not when it runs¶
Level: 301 · for a reader who has seen def self.included(base) in a gem and wondered who calls it
One line: inherited, included, extended, prepended, method_added, singleton_method_added and const_added are private methods Ruby calls while a class body is being executed — one hook per event — and Python's __init_subclass__ is a single hook for the first of these, with a metaclass's __prepare__ and __new__ standing in for all the others.
A Ruby class body is code that runs, and while it runs the interpreter reports what it is doing to a set of hooks. Class#inherited(sub) runs on the parent the moment class Sub < Parent (or Class.new(Parent)) creates the subclass — before the body, and for Class.new before the class has a name. Module#included(base), extended(obj) and prepended(base) run after the module has been wired into the ancestor chain (append_features runs just before, and does the wiring). Module#method_added(name) runs after every method definition, whether it came from def, attr_accessor, alias_method or define_method; singleton_method_added after every def self.x; and const_added after every constant assignment, nested classes included — this last one since Ruby 3.2 (the version is not machine-checked here). All of them are private, all default to doing nothing, and all are meant to be overridden by whoever wants a registry, a plugin system or a class-level option. The one hook you write most is included with base.extend(ClassMethods): it is how a module hands its includer class methods as well as instance methods.
Verified output of hooks_inherited_included_method_added_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. Exporter.registry after two `class X < Exporter` ["CsvExporter", "JsonExporter"]
2. Class.new(Exporter): registry.last, anon.name nil / nil
XmlExporter = anon; anon.name "XmlExporter"
3. class CsvExporter < Exporter; tag "csv"; end CsvExporter.tag = "csv"
4. include Persist: Model.table_name, .new.save "models", "saved models"
5. LOG order during include ["append_features Model", "included in Model, already in ancestors: true"]
6. Object.new.extend(Persist) ["extended a Object"]
7. class Audited; prepend Persist; end ["prepended to Audited"]; ancestors.first(2) = [Persist, Audited]
8. method_added saw (def, attr_accessor, alias, define_method, private) [:load, :id, :id=, :fetch, :touch, :hidden]
9. singleton_method_added saw [:create]
10. const_added saw [:VERSION, :Inner]
11. Exporter.subclasses.map(&:name).sort ["CsvExporter", "JsonExporter", "XmlExporter"]
12. class Leaf < Mid: Base2.all, and Mid's own hook ["Mid", "Leaf"], ["Mid saw Leaf"]
13. the hooks are private: inherited/included/method_added [true, true, true]
Reading the output¶
Row 1: inherited fired twice, once per class X < Exporter, and the registry holds the names in definition order. Row 2 shows the timing trap: Class.new(Exporter) fires the hook before any constant names the class, so the registry recorded nil — assigning XmlExporter = anon afterwards names the class but does not re-run the hook. Row 3 is how Ruby passes a class-level option: there is no keyword syntax on class, so tag "csv" is just a class method called inside the body. Rows 4 and 5 are the included pattern and its timing: Model.table_name exists because included called base.extend(ClassMethods), and append_features ran before included, by which time the module was already in the ancestors. Rows 6 and 7 are the same idea for the other two ways of mixing in — extend on a single object fires extended, prepend fires prepended and puts the module before the class in ancestors. Rows 8–10 are the per-definition hooks, kept out of the way by living in a Watch module the class extends: method_added saw the def, the two methods attr_accessor made, the alias, the define_method and the private one; singleton_method_added saw create; const_added saw the constant and the nested class. Row 11 is the read-only view, Class#subclasses — printed sorted, because the order it returns is not part of its contract; it arrived in Ruby 3.1 (the version is not machine-checked here). Row 12 is the rule every hook needs: call super, so a hook in a subclass does not silence the parent's. Row 13: the hooks are private methods; you never call them, Ruby does.
Compared with Python¶
Python has one hook where Ruby has seven, and it is a class method on the parent: __init_subclass__(cls, **kwargs) runs whenever a subclass is created, by class statement or by type() (rows 1 and 2), and the subclass always has its name by then — there are no anonymous classes to catch out a registry. It also receives keyword arguments written on the class line, class CsvExporter(Exporter, tag="csv") (row 3), which is Python's spelling of Ruby's tag "csv" call — and an option nobody consumes is a TypeError, because object.__init_subclass__ accepts none. A mixin is a base class, so included has no separate twin: the mixin's own __init_subclass__ fires when a class lists it as a base, and class methods come along by ordinary inheritance with no extend(ClassMethods) dance (row 4). Row 5's log is the order Python builds a class in: the body's assignments first (logged by the dictionary __prepare__ returned), then __set_name__ on each descriptor, then __init_subclass__ on the bases — the same "wiring first, hook after" order as Ruby's append_features then included. Extending one object is a class swap, o.__class__ = type(..., (Loud, Model), {}), which fires the same hook (row 6); prepend has no twin — a subclass whose MRO lists Audit first is the closest, and super().save() chains through it (row 7). The per-definition hooks are where the models diverge: Python reports nothing per def, but a metaclass sees the whole body at once — __prepare__ returns the dictionary the body writes into, so every assignment can be logged in order, and __new__ receives the finished namespace and can sort it into functions, classmethods and the rest (rows 8–10). __set_name__ is the one per-attribute hook, on descriptors rather than on the class: Field learns it was assigned to Model.id. __subclasses__() is Class#subclasses (row 11), super().__init_subclass__(**kwargs) is the super of row 12, and row 13 is the reason the hook needs no decorator: Python makes __init_subclass__ a classmethod implicitly.
Verified output of hooks_inherited_included_method_added_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. Exporter.registry after two `class X(Exporter)` ['CsvExporter', 'JsonExporter']
2. type("Anon", (Exporter,), {}): registry[-1] 'Anon' -- named at birth
anon.__name__ 'Anon'
3. class CsvExporter(Exporter, tag="csv") CsvExporter.tag = 'csv'
an option nobody consumes TypeError
4. class Model(Persist): table_name(), ().save() 'models', 'saved models'
5. LOG order while the class was built ['body defined load', 'body defined id', 'body defined fetch', 'body defined touch', 'body defined _hidden', 'body defined create', 'body defined VERSION', 'set_name Model.id', 'Persist mixed into Model, already in mro: True']
6. o.__class__ = type('LoudModel', (Loud, Model), {}) ['Loud mixed into LoudModel', 'Persist mixed into LoudModel, already in mro: True']; isinstance(o, Loud) = True
7. Audited = type(..., (Audit, Model), {}) ['Persist mixed into Audited, already in mro: True']; mro[:2] = ['Audited', 'Audit']
Audited().save() 'audit then saved auditeds'
8. metaclass saw functions (def, alias, lambda, _hidden) ['load', 'fetch', 'touch', '_hidden']
9. metaclass saw classmethods ['create']
10. metaclass saw the rest of the body ['id', 'VERSION']
11. sorted(c.__name__ for c in Exporter.__subclasses__()) ['Anon', 'CsvExporter', 'JsonExporter']
12. class Leaf(Mid): Base2.all, and Mid's own hook ['Mid', 'Leaf'], ['Mid saw Leaf']
13. __init_subclass__ is implicitly a classmethod classmethod
Try it¶
- Build a
Commandbase class whoseinheritedhook registers each subclass under a snake_case name derived from the class name (ListFiles→"list_files"), and aCommand.run(name, *args)that finds and runs one. Print the registry keys, two dispatches, what an unknown name raises, andCommand.subclasses. - Write a
Validationsmodule whoseincludedhook gives the class avalidates(:attr, presence: true)class method (kept inClassMethods) and an instance-levelvalid?that checks every registered attribute. Then re-do it as a Python mixin with__init_subclass__reading aVALIDATESclass attribute. Which version needed theextendstep, and why?
Solution to 1
hooks_inherited_included_method_added_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# hooks_inherited_included_method_added_kata_rb.rb — a command registry filled by `inherited`.
class Command
def self.registry = @registry ||= {}
def self.inherited(sub)
super
name = sub.name.gsub(/([a-z])([A-Z])/, '\1_\2').downcase # ListFiles -> list_files
Command.registry[name] = sub
end
def self.run(name, *args)
klass = Command.registry.fetch(name) { raise ArgumentError, "no command #{name.inspect}" }
klass.new.call(*args)
end
end
class ListFiles < Command
def call(dir = ".") = "listing #{dir}"
end
class ShowVersion < Command
def call = "version 1.0"
end
puts "1. Command.registry.keys -> #{Command.registry.keys.inspect}"
puts "2. Command.run(\"list_files\", \"/tmp\") -> #{Command.run("list_files", "/tmp").inspect}"
puts "3. Command.run(\"show_version\") -> #{Command.run("show_version").inspect}"
begin
Command.run("nope")
rescue ArgumentError => e
puts "4. Command.run(\"nope\") -> #{e.class}: #{e.message}"
end
puts "5. Command.subclasses.map(&:name).sort -> #{Command.subclasses.map(&:name).sort.inspect}"
Verified output of hooks_inherited_included_method_added_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
define_methodturns a block into a method —method_addedfires for these tooClass.newmakes a class; a constant names it — whyinheritedsawnilin row 2- A DSL is
instance_evalplus stored blocks — class-level options liketag "csv"are a small DSL - Mixins:
include,extend,prepend— what the three hooks of rows 4–7 are reporting on - Method lookup and
super— the ancestor chainappend_featuresedits TracePoint— hooks on running code, the other half of "defined, not run"- Singleton classes — where
extendandsingleton_method_addedoperate - Class variables are shared with subclasses —
@@countis one variable, owned by the class that first assigns it and seen by every subclass and every… - Ruby has no docstrings — A Ruby method carries no documentation at run time