Hooks, eval and DSLs

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them. Ruby lets a program change itself while it runs: a class can be told when it gains a subclass or a method, a block can be run with a different self, and a string can be compiled into code. Those three abilities, callbacks (hooks), evaluation and the small languages built on top of them (DSLs), are what libraries such as test frameworks, command-line parsers and build tools use to make their APIs read like prose. They are also where most metaprogramming bugs and security problems come from, so this page spends as much time on what can go wrong as on what is possible.

The page starts with the hooks (the method that Ruby calls on a class or module when something happens to it), then the evaluation family (instance_eval, instance_exec, class_eval, module_eval, class_exec, eval and Binding), then why evaluating strings is dangerous, and finally how to put the pieces together into an internal DSL. Defining and calling methods by name at run time (send, define_method, method_missing) is Dynamic Dispatch and Method Definition, asking an object what it is made of is Reflection, Introspection and Tracing, and the ancestor chain that include, prepend and extend build is Modules and Mixins and Method Lookup and super. Singleton classes, where class methods live, are in Singleton Classes and Class-Level Behavior. Rails is out of scope for this reference.

Every example on this page was syntax-checked with ruby -wc, run with ruby -w on Ruby 4.0.7 and linted with RuboCop 1.92 using the configuration of the running shelf gem (target Ruby 4.0, double-quoted strings). The output shown in # => comments, and in the output blocks, is the real output. The examples reuse the shelf library catalogue used throughout this section (Shelf, Book, Isbn, Loan, Catalog, Shelf.configure) and the files that need it start with require "shelf" (see the shelf gem source). The few RuboCop exceptions are deliberate illustrations and are marked where they occur.

Hooks

What a hook is

A hook (also called a callback) is an ordinary method that Ruby itself calls when something happens: a class is subclassed, a module is mixed in, a method or constant is defined. The default implementations do nothing, which is why a class that defines none of them behaves normally. To use a hook you define a method with the right name on the object that receives the event, and the whole mechanism is then just method lookup (see Method Lookup and super).

Hook Receiver and where it is defined Called when Argument

inherited

The superclass, as a singleton method (def self.inherited). Documented on Class.

A subclass has been created, before its body runs.

The new subclass.

included

The module, as a singleton method. Documented on Module.

After include has inserted the module.

The class or module that included it.

extended

The module, as a singleton method.

After extend has added the module to an object’s singleton class.

The object that was extended.

prepended

The module, as a singleton method.

After prepend has inserted the module in front of a class.

The class that prepended it.

append_features, prepend_features, extend_object

The module (private methods of Module).

Just before the matching included, prepended or extended. These do the real insertion.

The target class or object.

method_added, method_removed, method_undefined

The class or module whose instance methods change. Documented on Module.

A method is defined, removed (remove_method) or undefined (undef_method, undef).

The method name as a Symbol.

singleton_method_added, singleton_method_removed, singleton_method_undefined

Any object, as a singleton method. Documented on BasicObject.

A singleton method of that object (for a class, a class method) changes.

The method name as a Symbol.

const_added (Ruby 3.2)

The class or module, as a singleton method. Documented on Module.

A constant is assigned, or a class or module is defined, inside it.

The constant name as a Symbol.

const_missing

The class or module, as a singleton method. Documented on Module.

A constant lookup fails, before NameError is raised.

The constant name as a Symbol.

Every hook in the table is looked up like any other method, so the usual rule for overriding applies: if a parent class or an included module may also define the hook, the override must call super. Forgetting it is the most common hook bug and the subject of the next section.

sequenceDiagram participant P as Parent class participant S as Sub class participant M as Mixin module P->>S: inherited(Sub) is called on Parent Note over S: the body of Sub starts running S->>S: def foo calls Sub.method_added(foo) S->>S: FOO = 1 calls Sub.const_added(FOO) S->>M: include Mixin calls Mixin.append_features(Sub) M-->>S: super inserts Mixin into the ancestors M->>S: Mixin.included(Sub) is called last

The diagram shows the order in which the events of class Sub < Parent reach their hooks: the creation hook before the body, each definition inside the body as it happens, and for a mixin the insertion before the notification. The examples below prove each step.

inherited

inherited is called on the superclass every time a class is created from it, whether with the class keyword, Class.new(Parent) or Struct.new and Data.define (which also create subclasses of Struct and Data). It is how plugin registries and per-subclass configuration are built. The rules that matter:

  • Always call super. The default Class#inherited is empty, so the hook appears to work without it. The problem is cooperation: the moment a module that is extend-ed into the class (or a parent class) also defines inherited, an override without super cuts the chain and silently disables the other implementation. RuboCop reports it as Lint/MissingSuper, which covers inherited, method_added, method_removed, method_undefined, the three singleton_method_* hooks and initialize of a subclass.

  • The hook is called on the direct superclass, and runs before the body of the subclass. The subclass exists and has its superclass set but no methods, constants or macros yet, so the hook cannot inspect them.

  • The class may have no name yet. With class Sqlite < Adapter the name is already set, but with Remote = Class.new(Adapter) the hook runs while the class is still anonymous, because the constant is assigned after Class.new returns.

  • It is inherited itself. A hook defined on Store also fires for grandchildren unless some subclass overrides it, as the next example shows.

The first example builds a registry of the adapters that Shelf::Store can have. Registry#inherited calls super and then records the subclass in the class instance variable of the class being subclassed. The second subclass, Careless, overrides inherited without super.

# frozen_string_literal: true

module Shelf
  # Gives a class hierarchy a registry of its direct subclasses.
  module Registry
    def inherited(subclass)
      super
      (@registry ||= []) << subclass
    end

    attr_reader :registry
  end

  # Root of the store adapters.
  class Store
    extend Registry
  end

  class Memory < Store; end

  # Overrides the hook but forgets to call super.
  class Careless < Store
    def self.inherited(subclass)
      puts "Careless saw #{subclass}"
    end
  end

  class Deep < Careless; end
end

Shelf::Store.registry # => [Shelf::Memory, Shelf::Careless]
Shelf::Careless.registry # => nil
Shelf::Store.registry.include?(Shelf::Deep) # => false
Shelf::Store.subclasses.sort_by(&:name) # => [Shelf::Careless, Shelf::Memory]
Shelf::Careless.singleton_class.ancestors.first(3) # => [#<Class:Shelf::Careless>, #<Class:Shelf::Store>, Shelf::Registry]

Output:

Careless saw Shelf::Deep

Source: Class#inherited

Deep was created from Careless, so Careless.inherited ran (that is the line printed) but, because it did not call super, Registry#inherited never saw Deep: it is missing from Shelf::Store.registry. Shelf::Careless.registry is nil for a second reason. Each class has its own @registry, and Careless never had one assigned. RuboCop flags the override with Lint/MissingSuper; the example keeps that single offense on purpose, to show the bug.

Ruby 3.1 added Class#subclasses, which returns the direct subclasses that currently exist, excluding singleton classes. Use it when you only need to ask, and a hook when you need to run code at the moment of subclassing. Its order is not specified (the example sorts the result), anonymous subclasses that have been garbage collected disappear from it, and it lists only direct subclasses. Before 3.1 the usual substitute was ObjectSpace.each_object(Class), which walks the whole heap. A method that returns every descendant, Module#descendants, appears in the development NEWS for the upcoming Ruby 4.1; it is not in 4.0.7 and is not used here.

The timing rules above are easy to confirm with a hook that prints what it can see.

# frozen_string_literal: true

module Shelf
  # Prints what the inherited hook can see about each new subclass.
  class Adapter
    def self.inherited(subclass)
      super
      puts "inherited: name=#{subclass.name.inspect} superclass=#{subclass.superclass}"
    end
  end

  class Sqlite < Adapter
    puts "body of Sqlite runs after the hook"
  end

  Remote = Class.new(Adapter) { puts "block of Class.new runs after the hook" }
  Class.new(Adapter)
end

Shelf::Remote.name # => "Shelf::Remote"

Output:

inherited: name="Shelf::Sqlite" superclass=Shelf::Adapter
body of Sqlite runs after the hook
inherited: name=nil superclass=Shelf::Adapter
block of Class.new runs after the hook
inherited: name=nil superclass=Shelf::Adapter

Source: Class#inherited

Both anonymous classes reported name=nil, and the block given to Class.new ran after the hook. After the assignment, Shelf::Remote.name is "Shelf::Remote". Code that needs the name has to wait, for example by registering lazily the first time the registry is read.

Class-level configuration copied to subclasses

Class instance variables belong to one class object and are not shared with subclasses, so a macro such as field that stores into @fields would leave every subclass with an empty table, or with nil. inherited is the natural place to give each subclass its own copy of the parent’s configuration, copying instead of sharing so that a subclass can add entries without changing the parent. This is the pattern behind most class-level DSLs (declared attributes, validations, routes).

# frozen_string_literal: true

module Shelf
  # Class-level macros plus per-class settings that subclasses inherit as a copy.
  module Fields
    def self.extended(base)
      base.instance_variable_set(:@fields, {})
    end

    def inherited(subclass)
      super
      subclass.instance_variable_set(:@fields, @fields.dup)
    end

    def field(name, default: nil)
      @fields[name] = default
      attr_accessor name
    end

    def fields = @fields.dup
  end

  # A mutable record described with the macros above.
  class Record
    extend Fields

    field :title, default: "untitled"

    def initialize(**values)
      self.class.fields.each { |name, default| public_send(:"#{name}=", values.fetch(name, default)) }
    end
  end

  # A subclass adds a field without touching the parent's table.
  class Magazine < Record
    field :issue, default: 1
  end
end

Shelf::Record.fields # => {title: "untitled"}
Shelf::Magazine.fields # => {title: "untitled", issue: 1}
magazine = Shelf::Magazine.new(title: "Wired")
[magazine.title, magazine.issue] # => ["Wired", 1]
Shelf::Record.new.respond_to?(:issue) # => false

Source: Class#inherited

Magazine starts with a copy of the Record fields (title), adds issue, and Record is untouched. The copy is shallow, which is enough for a Hash of Symbols to defaults. If the values are mutable (for example Arrays), duplicate them as well, or freeze them. The same idea, with extended setting up the first table, is how the class-methods module of the next section starts.

included, extended and prepended

The three module hooks are Module callbacks called on the module that was mixed in. included(base) receives the class that ran include, extended(object) the object that ran extend (often another class or module), and prepended(base) the class that ran prepend. A mixin that needs to add class methods as well as instance methods uses the classic idiom of extending the including class from included: instance methods are the module’s own methods, and the class methods go into a nested module (conventionally ClassMethods) that included passes to base.extend.

Behind each hook is a private method that does the work: include calls append_features(base), prepend calls prepend_features(base) and extend calls extend_object(object), and only then the matching notification hook. They are rarely overridden, and an override that does not call super means the module is not inserted at all, as the last lines of the example show. Passing several modules to one call (Branch.include(First, Second)) processes them from right to left, so the hooks run in the reverse of the written order while the ancestors keep the written order.

# frozen_string_literal: true

module Shelf
  # A mixin that reacts to every way of being mixed in.
  module Searchable
    def self.included(base)
      puts "included in #{base}"
      base.extend(ClassMethods)
    end

    def self.extended(object) = puts("extended #{object.class}")
    def self.prepended(base) = puts("prepended to #{base}")

    def self.append_features(base)
      puts "append_features #{base}"
      super
    end

    def self.prepend_features(base)
      puts "prepend_features #{base}"
      super
    end

    def self.extend_object(object)
      puts "extend_object #{object.class}"
      super
    end

    # Class-level API installed by the included hook.
    module ClassMethods
      def searchable_fields = @searchable_fields ||= []
    end

    def search = :ok
  end

  class Wing; include Searchable; end
  class Annex; prepend Searchable; end

  # Two modules included in one call are processed right to left.
  module First
    def self.included(base) = puts("First included in #{base}")
  end

  module Second
    def self.included(base) = puts("Second included in #{base}")
  end

  Branch = Class.new
  Branch.include(First, Second)

  # Skipping super in append_features means the module is never inserted.
  module Skipped
    def self.append_features(_base) = puts("skipping")
  end

  class Depot; include Skipped; end
end

Object.new.extend(Shelf::Searchable)
Shelf::Wing.ancestors.first(2) # => [Shelf::Wing, Shelf::Searchable]
Shelf::Annex.ancestors.first(2) # => [Shelf::Searchable, Shelf::Annex]
Shelf::Wing.searchable_fields # => []
Shelf::Branch.ancestors.first(3) # => [Shelf::Branch, Shelf::First, Shelf::Second]
Shelf::Depot.include?(Shelf::Skipped) # => false

Output:

append_features Shelf::Wing
included in Shelf::Wing
prepend_features Shelf::Annex
prepended to Shelf::Annex
Second included in Shelf::Branch
First included in Shelf::Branch
skipping
extend_object Object
extended Object

Things to remember about these three hooks:

  • included runs after the module is already in the ancestors, so base.ancestors shows it. prepended likewise.

  • Mixing a module into a module (module Reader; include Searchable; end) calls Searchable.included(Reader) with the module as base, not with the classes that later include Reader. Chain the call (base.include(Other) inside your own included) or use extend on the class if the second module must reach the final class.

  • extended is not called by include. To react to both, define both hooks, or put the shared setup in a method that each hook calls.

  • Instead of included with ClassMethods, a module that only needs class-level methods can be used with extend directly (class Store; extend Registry; end, as in the first example), which needs no hook at all.

  • prepend plus a hook is the modern replacement for the old alias-the-original-method chain. The prepended module’s methods call super to reach the original, which keeps Method#owner and Method#super_method honest (see Modules and Mixins).

  • RuboCop’s Style/MixinGrouping asks for include with several modules to be written as separate statements, and Naming/MethodParameterName enforces parameter names of at least three characters, which is why the hook parameters are called base and object here.

method_added, method_removed, method_undefined and the singleton variants

method_added(name) is called on a class or module after an instance method has been defined with def, define_method, alias_method or alias, or created by attr_reader, attr_writer or attr_accessor (an accessor pair reports bar and bar=). Marking an inherited method private or public in a subclass can also report it, because Ruby then records an entry for it in the subclass; changing the visibility of a method defined in the same class does not. method_removed follows remove_method and method_undefined follows undef_method or the undef keyword (the difference between removing and undefining is in Dynamic Dispatch and Method Definition).

The singleton_method_* hooks are the same events for singleton methods and are defined on BasicObject, so they work for any object. For a class they report class methods (def self.build, and the methods in a class << self block). module_function generates them too, because it copies the method to the module’s singleton class (extend of a module with ordinary methods does not). One subtlety: defining singleton_method_added is itself a singleton-method definition, so the hook is called with its own name as soon as it exists, and the first line of the output below shows it.

const_added(name) (Ruby 3.2) is the constant counterpart. It is called for assignments such as VERSION = 1 and for the definition of a nested class or module, and it is not called for constants that were defined before the hook. For lookups that fail, const_missing(name) is called first and, if it calls super, ends in the usual NameError; autoloaders use it for lazy loading.

# frozen_string_literal: true

module Shelf
  # Records every definition event on itself.
  class Audited
    @log = []

    class << self
      attr_reader :log

      def method_added(name)
        super
        @log << [:added, name]
      end

      def method_removed(name)
        super
        @log << [:removed, name]
      end

      def method_undefined(name)
        super
        @log << [:undefined, name]
      end

      def singleton_method_added(name)
        super
        @log << [:singleton_added, name]
      end

      def const_added(name)
        super
        @log << [:const_added, name]
      end

      def const_missing(name)
        @log << [:const_missing, name]
        super
      end
    end

    VERSION = 1

    def title = "t"
    def author = "a"
    def self.build = new

    remove_method :title
    undef_method :author
  end
end

begin
  Shelf::Audited::Nope
rescue NameError => e
  Shelf::Audited.log << [:error, e.message]
end

Shelf::Audited.log.each { puts it.inspect }

Output:

[:singleton_added, :singleton_method_added]
[:singleton_added, :const_added]
[:singleton_added, :const_missing]
[:const_added, :VERSION]
[:added, :title]
[:added, :author]
[:singleton_added, :build]
[:removed, :title]
[:undefined, :author]
[:const_missing, :Nope]
[:error, "uninitialized constant Shelf::Audited::Nope"]

Only definitions that happen after a hook exists are reported, which is why attr_reader :log and the hook definitions themselves are not in the log (apart from the self-report of singleton_method_added). Always call super first, for the same cooperation reason as inherited.

Decorating methods from method_added

A frequent use of method_added is to wrap every method defined after some point, for logging, memoization or access checks. The main trap is recursion: if the hook defines a method on the same class (with define_method, alias_method and a new def), that definition triggers method_added again, and so on until Ruby raises SystemStackError ("stack level too deep"). The classic way out is a flag that stops the re-entry. The better way is to put the wrapper in a different module, usually one that is prepend-ed, so that its definitions notify that module, not the class. The example does that.

# frozen_string_literal: true

module Shelf
  # Wraps every public method defined after `extend Traced` in a logging layer.
  module Traced
    def self.extended(base)
      base.prepend(base.const_set(:Wrappers, Module.new))
    end

    def method_added(name)
      super
      return unless public_method_defined?(name)

      # The wrapper lives in another module, so defining it does not
      # call method_added on this class again.
      self::Wrappers.define_method(name) do |*args, **kwargs, &block|
        puts "-> #{name}"
        super(*args, **kwargs, &block).tap { puts "<- #{name}" }
      end
    end
  end

  # A desk that lends books.
  class Desk
    extend Traced

    def lend(title) = "lent #{title}"
    def back(title) = "got #{title}"
  end
end

Shelf::Desk.new.lend("Dune") # => "lent Dune"
Shelf::Desk.ancestors.first(2) # => [Shelf::Desk::Wrappers, Shelf::Desk]

Output:

-> lend
<- lend

Several details of this example are worth keeping. The wrapper takes *args, **kwargs and &block and passes all three on with an explicit-arguments super(...), so that keyword arguments stay keyword arguments (see Calling Methods and Argument Forwarding). The hook only wraps public methods, because a wrapper defined in a module would otherwise make a private method public. Desk.ancestors shows the generated Wrappers module in front of the class, and a Method#owner check on any wrapped method returns that module. A hook that needs to wrap methods that already exist does it once, at the end of the class body, by walking public_instance_methods(false).

Evaluating code with a different self

instance_eval, instance_exec, class_eval, module_eval and class_exec

All five methods run code in a context chosen by the caller. They differ in two settings that Ruby tracks for every piece of code: self, the receiver of calls without an explicit receiver, and the default definee (also called the cref or the default target of def), the class that a bare def adds a method to.

Method self inside Default definee for def Notes

obj.instance_eval { }

obj

The singleton class of obj

Passes obj to the block as its argument. Also accepts a string. Defined on BasicObject.

obj.instance_exec(*args) { }

obj

The singleton class of obj

Like instance_eval but passes args to the block, and takes no string.

mod.class_eval { } and mod.module_eval { }

mod

mod itself

Aliases of each other. Also accept a string. Defined on Module.

mod.class_exec(*args) { } and mod.module_exec(*args) { }

mod

mod itself

Like class_eval but passes args, and takes no string.

The pairing to memorise is that instance_eval makes self the receiver and defines singleton methods, while class_eval makes self the receiver and defines instance methods of the receiver. On a class, instance_eval therefore defines class methods and class_eval defines instance methods. instance_exec and class_exec exist to get arguments into the block: the block runs with a new self, so it cannot see the caller’s self or instance variables, but it can receive values as block parameters, and class_exec(:badge) { |name| define_method(name) { name } } is the clean way to generate a method whose name is known only at run time.

# frozen_string_literal: true

module Shelf
  # A desk with one private detail.
  class Desk
    def initialize = @secret = 42
  end
end

desk = Shelf::Desk.new

# instance_eval and instance_exec: self is the receiver.
desk.instance_eval { @secret } # => 42
desk.instance_eval { |object| object.equal?(self) } # => true
desk.instance_exec(5) { |extra| @secret + extra } # => 47

# def inside instance_eval defines a singleton method of the receiver.
desk.instance_eval { def greet = "hi" }
desk.singleton_methods # => [:greet]
Shelf::Desk.instance_methods(false) # => []

# def inside class_eval defines an instance method of the class.
Shelf::Desk.class_eval { def wave = "o/" }
Shelf::Desk.instance_methods(false) # => [:wave]

# instance_eval on a class defines a class method.
Shelf::Desk.instance_eval { def open = new }
Shelf::Desk.singleton_methods # => [:open]

# class_exec passes arguments, which makes define_method with a runtime name easy.
Shelf::Desk.class_exec(:badge) { |name| define_method(name) { name } }
desk.badge # => :badge

Since Ruby 3.1 instance_eval and instance_exec allocate the singleton class of the receiver only when something actually needs it (a def or similar), so using them just to read instance variables or to run a block as the receiver is cheap.

Blocks versus strings

All of the methods except the _exec pair also accept a string. The two forms are not equivalent:

  • A block is a closure. Constant lookup and local variables are those of the place where the block was written, not of the receiver. A constant referenced in the block resolves lexically; in the example below, LIMIT in a class_eval block is the top-level constant, not the one inside Shelf::Desk.

  • A string is compiled when it runs, with the receiver as self and as the lexical class, so the constant LIMIT resolves to the class’s own. It still sees the local variables of the code that called class_eval, but local variables that it creates stay inside it.

  • A block gets the file and line of its source for free. For a string, pass __FILE__ and __LINE__ + 1 as the second and third arguments (with a heredoc, the body starts on the next line), or errors and Method#source_location will point at (eval at ...), which is useless in a stack trace.

  • Blocks are checked when the file is parsed (a typo is a syntax error at load time) and can be seen by RuboCop and by coverage tools. Strings are only checked when they run.

# frozen_string_literal: true

LIMIT = :outer

module Shelf
  # A desk with its own LIMIT constant.
  class Desk
    LIMIT = :desk
  end
end

desk = Shelf::Desk.new
local = 10

# A block keeps the constant lookup and locals of where it was written.
Shelf::Desk.class_eval { LIMIT } # => :outer
desk.instance_eval { LIMIT } # => :outer
Shelf::Desk.class_eval { local } # => 10

# A string is compiled with the receiver as self and as the lexical class, so constants
# come from the class. It still sees the local variables of the caller.
Shelf::Desk.class_eval("LIMIT", __FILE__, __LINE__) # => :desk
Shelf::Desk.class_eval("local", __FILE__, __LINE__) # => 10

# But a variable first assigned inside the string does not leak back out.
Shelf::Desk.class_eval("created = 1", __FILE__, __LINE__)
binding.local_variable_defined?(:created) # => false

# A lambda is strict about arguments, and instance_eval passes the receiver as one.
strict = -> { :zero }
begin
  desk.instance_eval(&strict)
rescue ArgumentError => e
  puts e.message
end
desk.instance_exec(&strict) # => :zero

# Immediate values have no singleton class, frozen objects cannot change.
begin
  5.instance_eval { def double = self * 2 }
rescue TypeError => e
  puts "#{e.class}: #{e.message}"
end

begin
  Shelf::Desk.new.freeze.instance_eval { @secret = 1 }
rescue FrozenError => e
  puts e.class
end

Output:

wrong number of arguments (given 1, expected 0)
TypeError: can't define singleton
FrozenError

Three rules are visible here. A lambda passed with & keeps its strict arity, and instance_eval calls it with one argument (the receiver), so a lambda with no parameters raises ArgumentError where a proc would not; instance_exec passes no arguments unless asked. Integers, Floats and Symbols have no singleton class, so defining a method on one raises TypeError (nil, true and false accept it, the method then belongs to NilClass, TrueClass or FalseClass). A frozen object raises FrozenError as soon as the block assigns an instance variable. The common error inside a block that was meant for a DSL, a NameError for a name that exists in the caller but not in the new self, is described under Internal DSLs below.

Generating methods from a string

Generating many similar methods is the legitimate use of string class_eval: when every method is the same except for a name, a string compiles into plain, fast methods that read like hand-written ones, and it supports syntax that define_method cannot, such as a method with its own default arguments, a begin block or yield. The cost is that the string is code you must trust and test. When a closure is enough, prefer define_method, which is also shown below so the two can be compared. The convention taken from large codebases is to pass the file and line, and to add a comment that shows what the generated code looks like; RuboCop enforces both with Style/EvalWithLocation and Style/DocumentDynamicEvalDefinition.

# frozen_string_literal: true

require "shelf"

module Shelf
  class Loan
    STATES.each do |state|
      # The string is evaluated with the file and line of the heredoc,
      # so a stack trace points at this source. The comment shows the result.
      class_eval <<~RUBY, __FILE__, __LINE__ + 1
        def #{state}? = @state == :#{state} # def lent? = @state == :lent
      RUBY

      # The same method with a closure needs no string at all.
      define_method(:"#{state}_again?") { @state == state }
    end
  end
end

book = Shelf::Book.new(Shelf::Isbn.new("978-0-596-51774-8"), "Dune", Shelf::Author.new("Frank Herbert"))
loan = Shelf::Loan.new(book, state: :lent)
[loan.lent?, loan.available?, loan.lent_again?] # => [true, false, true]
puts Shelf::Loan.instance_method(:lent?).source_location.inspect

# Without a file and line, errors in generated code point at the eval itself.
Shelf::Loan.class_eval("def broken = raise('boom')") # rubocop:disable Style/EvalWithLocation
begin
  loan.broken
rescue RuntimeError => e
  puts e.backtrace.first
end

Shelf::Loan.class_eval("def fixed = raise('boom')", __FILE__, __LINE__)
begin
  loan.fixed
rescue RuntimeError => e
  puts e.backtrace.first
end

Output:

["predicates.rb", 11]
(eval at predicates.rb:26):1:in 'Shelf::Loan#broken'
predicates.rb:33:in 'Shelf::Loan#fixed'

The generated predicates report the line of the heredoc as their source location. The backtrace of broken names only (eval at ...), while the one of fixed points at the file and line that were passed in. The one RuboCop exception, the rubocop:disable Style/EvalWithLocation comment, exists to show the failing form on purpose.

eval and Binding

Kernel#eval

Kernel#eval(string, binding = nil, file = nil, line = nil) compiles and runs a string. Without a binding it uses the self and the local variables of the caller. With a binding argument it uses those of that binding, and with file and line it labels the code for error messages, backtraces and __FILE__. Since Ruby 3.3 the default file name is (eval at path:line), which names the call site (earlier releases printed just (eval)). Syntax errors in the string raise SyntaxError, with the position inside the string.

eval returns the value of the last expression. A local variable first assigned by the string exists only inside that evaluation and is not visible to the code that called eval; to keep it, evaluate against a Binding object that you hold on to, as the next section shows. Under ruby -w the evaluated code is also checked: a statement whose value is discarded produces "possibly useless use" warnings, and a variable used only inside strings is reported as "assigned but unused", because the parser cannot see inside the string.

Binding

A Binding is an object that captures a point in a program: the local variables in scope, self and the current method and block context. Kernel#binding returns the one for the current position, Proc#binding returns the one captured by a proc, and TOPLEVEL_BINDING is the one of the main script. The useful Binding methods are local_variables, local_variable_get, local_variable_set (which can add a variable that exists only in that binding), local_variable_defined?, eval, receiver (the self of the binding) and source_location. Ruby 4.0 adds implicit_parameters, implicit_parameter_get and implicit_parameter_defined? for the numbered parameters (_1) and the it parameter, which local_variables no longer includes. binding.irb opens an IRB session at that point and is described in IRB and Debugging.

Bindings keep every local variable they can see alive for as long as the Binding is alive, so holding on to one (or to a proc that is later asked for binding) can keep large objects from being collected. Kernel#binding raises RuntimeError when called from a method implemented in C (Ruby 3.2).

# frozen_string_literal: true

# This file shows eval without its location on purpose, so the cop that asks for it is off.
# rubocop:disable Style/EvalWithLocation

# Returns the binding of a method call, with its parameters inside.
def loan_binding(title, days) = binding

copy = loan_binding("Dune", 14)
copy.local_variables # => [:title, :days]
copy.local_variable_get(:days) # => 14
copy.local_variable_defined?(:fee) # => false
copy.local_variable_set(:fee, 5)
copy.eval("days + fee") # => 19
copy.receiver # => main
copy.source_location.last # => 7

# Kernel#eval without a binding uses the caller's own locals.
def double(limit) = limit.then { eval("limit * 2") }
double(3) # => 6

begin
  eval("limit * 2", copy)
rescue NameError => e
  puts e.message
end

# A proc remembers the binding where it was created.
def counter
  count = 0
  -> { count += 1 }
end

tick = counter
tick.call # => 1
tick.binding.local_variable_get(:count) # => 1
tick.binding.local_variable_set(:count, 10)
tick.call # => 11

# File and line shape the names and errors of evaluated code.
puts eval("__FILE__")
puts eval("__FILE__", nil, "shelf/virtual.rb", 7)
eval("__LINE__", nil, "shelf/virtual.rb", 7) # => 7

begin
  eval("end", nil, "shelf/virtual.rb", 7)
rescue SyntaxError => e
  puts e.message
end

# rubocop:enable Style/EvalWithLocation

Output:

undefined local variable or method 'limit' for main
(eval at bindings.rb:41)
shelf/virtual.rb
shelf/virtual.rb:7: syntax error found
> 7 | end
    | ^~~ unexpected 'end', ignoring it

The limit lookup fails because limit is a parameter of double, not a local of loan_binding. The closure of counter stays reachable through tick.binding, which is why a Proc can be used to read and even change the variables it captured. The file-wide rubocop:disable Style/EvalWithLocation at the top and bottom of the file marks the second deliberate exception, because this file is about eval without a location.

ERB is the standard library’s best-known user of bindings. ERB#result(binding) evaluates the template with the locals of that binding, and ERB#result_with_hash builds a fresh binding from a Hash and is usually the safer choice. The last lines of the next example use both.

Why string eval is dangerous

eval, instance_eval with a string, class_eval with a string and Binding#eval all run whatever text they get with the full privileges of the Ruby process: they can read files, run commands, change global state and exit. If any part of the string comes from outside the program (a request, a file, a database field, a configuration value, an environment variable) the program has a code-injection hole, and the injected code runs with the same rights as the application. It does not help to filter the input: the Ruby grammar is far too big to describe what is "safe". Older Rubies had $SAFE levels and object tainting for this purpose; both were removed (the $SAFE variable lost its special meaning in Ruby 3.0 and Object#taint and its family were removed in Ruby 3.2), and in any case they were never a reliable sandbox. There is no in-process sandbox in Ruby 4.0: untrusted code needs a separate process, container or Wasm runtime (see Security).

The same applies to the other ways of turning data into code: Kernel#send or public_send with a method name from the user (send("exit") runs it, public_send refuses private methods), Module#const_get and Object#instance_variable_get with a user-controlled name, Kernel#system and backticks with string interpolation, and Marshal.load and unsafe YAML loading on untrusted input. The alternatives, in order of preference:

Instead of Use

eval("#{text}") to turn text into a number or a boolean

Integer(text, exception: false), Float(text, exception: false), a case over fixed strings.

eval or instance_eval of configuration text

A data format (JSON, or YAML with safe_load) parsed into a Hash, then an explicit mapping onto the objects.

send(user_input) or eval("object.#{user_input}")

public_send after checking the name against an allow-list, or a Hash from names to lambdas.

String class_eval to generate methods

define_method with a closure when the method body can be a block; a string only when the whole text is a constant of the program.

A template engine fed untrusted templates

A logic-less template (no embedded Ruby) or a sandboxed service. ERB templates are code and must be trusted.

Running untrusted Ruby

A separate operating system process with limited rights; not eval, not a Ractor, not $SAFE.

The example shows the injected assignment, the safe parsing, the allow-list and the ERB lines. In real code the first call is the one that would be removed.

# frozen_string_literal: true

require "erb"
require "shelf"

# Imagine this text arrived in a form field or a query string.
loan_days = "Shelf.config.loan_days = 9999"

# Dangerous: the text is run as Ruby with the full power of the process.
eval(loan_days) # rubocop:disable Security/Eval
Shelf.config.loan_days # => 9999

# Safe: parse the value, never execute it.
Shelf.configure { |config| config.loan_days = 14 }
Integer("21", exception: false) # => 21
Integer(loan_days, exception: false) # => nil

# Safe dispatch: an allow-list in front of public_send. Kernel methods such as exit are private.
ALLOWED = %i[title author].freeze
book = Shelf::Book.new(Shelf::Isbn.new("978-0-596-51774-8"), "Dune", Shelf::Author.new("Frank Herbert"))
field = "title"
book.public_send(field) if ALLOWED.include?(field.to_sym) # => "Dune"

command = "exit"
begin
  book.public_send(command)
rescue NoMethodError => e
  puts e.message
end

# ERB evaluates trusted templates against a Binding or a hash of locals.
template = ERB.new("<%= book.title %> by <%= book.author.name %>")
template.result_with_hash(book:) # => "Dune by Frank Herbert"
template.result(binding) # => "Dune by Frank Herbert"

Output:

private method 'exit' called for an instance of Shelf::Book

The injected string changed Shelf.config.loan_days to 9999 without any validation; the same text could have read a file or run a program. The single RuboCop exception is the rubocop:disable Security/Eval comment on that line, which is the cop that exists to forbid exactly this call. The exit check shows why public_send is the right default: exit is a private method of Kernel, so it is rejected, while send("exit") would have called it.

The cases where string evaluation is fine are the ones where the string is part of the program: a template that ships with the code, a generator (as in the predicates example) that interpolates only names the program itself controls, a debugger or console (IRB is an interactive eval by design), and tests that exercise code from fixtures.

Internal DSLs

What a DSL is in Ruby

A domain-specific language (DSL) is a small language for one task. An internal DSL, the kind this page is about, is just Ruby code written so that it reads like declarations: Shelf.configure do |config| config.loan_days = 21 end, a test framework’s describe blocks, a build file’s task definitions. Everything is ordinary method calls and blocks, so the DSL author’s tools are exactly the ones above: yield, instance_eval, instance_exec, hooks and class-level macros. The goal is readability for the person who writes the DSL code, and the cost is that the implementation uses the language’s dynamic features, which makes tracebacks, editor completion and static analysis weaker. Use a DSL when many people write the same kind of declaration; use plain objects, keyword arguments and Data when a single caller builds the object once.

Style Strengths Weaknesses

Yield an object: configure { |c| c.loan_days = 21 }

Explicit receiver; the block still sees the caller’s self, methods and instance variables; completion and static analysis work.

A little more typing in every line.

instance_eval: configure { loan_days 21 }

No receiver needed; reads like a declaration.

self changes: the caller’s methods and instance variables are gone, and assignments to instance variables silently land on the DSL object. Typos become NameError or NoMethodError on the wrong object.

Both, chosen by the block arity

Callers can use either style.

Two behaviours to document and test.

Class-level macros: field :title, validates :isbn

Declarations sit in the class body and generate methods; one extend adds the whole vocabulary.

The hooks and the copy-on-inherit rules are part of the contract (see inherited above).

Clean room: a BasicObject that forwards chosen methods

The block can call only what the DSL allows; no accidental puts, exit or Kernel call.

More code, and method_missing costs a little on every call.

The shelf configuration DSL

Shelf.configure (in the gem since the first page of this section) uses the first style: it yields its Config struct. The next file keeps that and adds a Settings builder that evaluates the same kind of block three ways, so the differences can be compared side by side: build with instance_eval, build_with with instance_exec to pass arguments in, and configure, which looks at the block’s arity and picks the style. The Librarian class shows the price of instance_eval: inside the block the librarian’s own helper method and instance variable are not reachable.

# frozen_string_literal: true

require "shelf"

# Style 1 is already in the gem: yield an explicit receiver.
Shelf.configure do |config|
  config.loan_days = 21
end
Shelf.config.loan_days # => 21

module Shelf
  # Collects settings with blocks evaluated in three different ways.
  class Settings
    # Style 2: instance_eval, so the block calls the DSL methods without a receiver.
    def self.build(&)
      new.tap { |settings| settings.instance_eval(&) }.to_h
    end

    # Style 2 with arguments: instance_exec.
    def self.build_with(*, &)
      new.tap { |settings| settings.instance_exec(*, &) }.to_h
    end

    # Style 3: support both by looking at the block arity.
    def self.configure(&block)
      settings = new
      block.arity.zero? ? settings.instance_eval(&block) : block.call(settings)
      settings.to_h
    end

    def initialize = @values = {}

    def loan_days(days) = @values[:loan_days] = days
    def max_loans(count) = @values[:max_loans] = count
    def to_h = @values.dup
  end

  # Its settings block cannot call the librarian's own helper.
  class Librarian
    attr_reader :name

    def initialize = @name = "Ana"

    def settings
      Settings.build do
        loan_days 7
        @name = "Bo"
        max_loans helper
      end
    end

    def helper = 5
  end
end

limit = 3
settings = Shelf::Settings.build do
  loan_days 28
  max_loans limit
end
settings.to_h # => {loan_days: 28, max_loans: 3}

# The cost of instance_eval: self changes, so the caller's methods and instance variables disappear.
librarian = Shelf::Librarian.new
begin
  librarian.settings
rescue NameError => e
  puts e.message
end
librarian.name # => "Ana"

Shelf::Settings.build_with(10) { |days| loan_days days } # => {loan_days: 10}
Shelf::Settings.configure { loan_days 1 } # => {loan_days: 1}
Shelf::Settings.configure { |settings| settings.loan_days 2 } # => {loan_days: 2}

Output:

undefined local variable or method 'helper' for an instance of Shelf::Settings

Notice that max_loans limit worked: the block still closes over the caller’s local variable limit, because a block is a closure and instance_eval changes only self, not local variables. What disappears is anything that needs the caller’s self: instance variables, private helper methods and self.class. In the Librarian example the assignment @name = "Bo" ran, but on the Settings object, which is why librarian.name is still "Ana", and the call to helper failed with NameError mentioning Shelf::Settings. A DSL that supports the instance_eval style should say so in its documentation, and offer the yield style for blocks that need the caller’s context.

Class-level macros

The class-level DSLs of libraries (attribute :title, has_many, validates) are a module with singleton-style methods plus hooks. The recipe is: define the vocabulary in a module, extend it into the base class, let extended or the first macro initialise the storage, let inherited copy the storage into subclasses, and generate the methods with define_method, attr_accessor or class_eval. The Fields module in the inherited section above is a complete, small example of it. Two details are easy to miss: macros run at class-definition time, so they should be cheap and must not touch the database or the network; and a macro that stores state in class instance variables is shared by every thread, so if it can be called at run time, guard it with a mutex (see Threads and the GVL).

Clean rooms

A clean room is an object that is deliberately almost empty, used as the self of the block. Subclassing BasicObject gives an object with only eight public instance methods (the example prints them), so puts, raise, require and the rest of Kernel are not callable by accident; the DSL decides in method_missing what to forward and what to reject. When doing this, call Kernel methods with an explicit ::Kernel and constants with a leading ::, because inside a BasicObject subclass the constant lookup does not reach Object (that is what the leading :: does). Forward arguments with anonymous * and & so that keyword arguments and blocks pass through unchanged.

# frozen_string_literal: true

require "shelf"

module Shelf
  # A BasicObject has almost no methods, so a DSL block cannot call Kernel by accident.
  class CleanRoom < BasicObject
    def initialize(target) = @target = target

    def method_missing(name, *, &)
      if @target.respond_to?(name)
        @target.public_send(name, *, &)
      else
        ::Kernel.raise ::NoMethodError, "the DSL has no #{name}"
      end
    end

    def respond_to_missing?(name, include_private = false) = @target.respond_to?(name, include_private)
  end
end

room = Shelf::CleanRoom.new([3, 1, 2])
room.instance_eval { sort } # => [1, 2, 3]

begin
  room.instance_eval { puts "hello" }
rescue NoMethodError => e
  puts e.message
end

BasicObject.instance_methods.sort # => [:!, :!=, :==, :__id__, :__send__, :equal?, :instance_eval, :instance_exec]

Output:

the DSL has no puts

Designing a DSL that stays maintainable

  • Prefer the yield style for configuration and use instance_eval only for declarative blocks whose lines are all calls to the DSL. Do not mix: a block that needs the caller’s methods and the DSL’s methods will fail at the first collision.

  • Validate in the DSL methods and raise ArgumentError with a message that names the keyword and the offending value. A DSL is read by people who did not write it.

  • Return self or a frozen result from the end of the block so callers cannot keep editing it.

  • Keep the vocabulary small and documented, with RDoc or YARD (including @!method for generated methods; see Documentation with RDoc and YARD), and test the DSL through its public entry point (see Testing with Minitest). Generated code needs tests more than hand-written code does, because nothing else reads it.

  • Keep stack traces readable: pass __FILE__ and __LINE__ to string evaluation, avoid deep method_missing chains, and respond to respond_to_missing? when you do use method_missing.

  • Do not evaluate DSL files that users upload. A file in a Ruby DSL is a program (see Why string eval is dangerous).

  • When a plain Hash, a Data value or keyword arguments would express the same thing, use those.

Where older books and tutorials are outdated

Hooks and evaluation are the part of Ruby where many printed examples still show the pre-2.0 or pre-3.0 style. The table lists what changed, so that an old example can be recognised and modernised.

Older advice Ruby 4.0 practice Since

Override inherited without calling super because the default does nothing

Always call super; Lint/MissingSuper reports the omission. Without it, other hooks in the chain are silently cut off.

Always true, now linted

List subclasses with ObjectSpace.each_object(Class) or by registering in inherited

Class#subclasses for the direct subclasses; a registry only when ordering or metadata is needed. Module#descendants is in the development NEWS for the upcoming 4.1, not in 4.0.

Ruby 3.1

Detect a new constant by polling constants, or by wrapping const_missing

Module#const_added

Ruby 3.2

Wrap a method with alias_method and a new def (the alias chain), or with method_added plus alias

prepend a module whose methods call super; a method_added hook that defines the wrapper in a different module

Long available

Use $SAFE levels, taint and untrust to make eval of user input "safe"

Neither exists any more; do not evaluate untrusted input, parse it as data

Ruby 3.0 ($SAFE ends) and 3.2 (taint methods removed)

instance_eval creates a singleton class every time, so avoid it in hot code

The singleton class is allocated only when a definition needs it

Ruby 3.1

Evaluated code is named (eval) in traces, Binding#eval with one argument uses (eval) and line 1

(eval at file:line) names the call site; pass file and line to get your own

Ruby 3.0 for Binding#eval, 3.3 for (eval at ...)

Binding#local_variables lists numbered block parameters

Use implicit_parameters and its siblings

Ruby 4.0

OpenStruct or method_missing objects as the object a configuration block is evaluated against

A small class, Struct or Data with the methods you want; see Struct, Data and Value Objects

Current practice

Common exceptions and warnings

Error or warning Typical cause What to do

NameError: undefined local variable or method 'name' for an instance of the DSL class

The block of an instance_eval DSL calls a method or reads a name that belongs to the caller’s self.

Yield an object instead, or capture the value in a local variable before the block.

ArgumentError: wrong number of arguments (given 1, expected 0)

A lambda used where instance_eval passes the receiver as the argument.

Use a proc or a block, give the lambda a parameter, or call instance_exec.

TypeError: can’t define singleton

instance_eval { def ... } on an Integer (of any size), a Float or a Symbol. nil, true and false are different: they have classes of their own, so the definition succeeds.

Use class_eval on the class or wrap the value in an object.

FrozenError

instance_eval or class_eval modifies a frozen object or class.

Do the evaluation before freezing, or work on a dup.

SyntaxError at run time

A string passed to eval, class_eval or instance_eval is not valid Ruby. The message names the file and line you passed.

Pass __FILE__ and __LINE__; test generated code; prefer blocks.

NameError or NoMethodError from an evaluated string that works elsewhere

The string was compiled with a different self or constant scope than the code that built it.

Check the self and constant rules in the evaluation table; use an explicit receiver.

SystemStackError (stack level too deep)

method_added defines a method in the same class, which calls method_added again.

Define wrappers in another module, or guard with a flag.

warning: method redefined; discarding old foo (with -w)

A hook or an eval defines a method that already exists on the same class.

Remove the old definition first or avoid redefining it.

warning: possibly useless use of ... in void context (with -w)

An evaluated string contains a statement whose value is dropped.

Return the value or remove the statement.

warning: assigned but unused variable (with -w)

A local variable is read only from inside an eval string; the parser cannot see the use.

Pass the value another way, for example Binding#local_variable_get or a method parameter.

NoMethodError: private method 'name' called

public_send (correctly) refused a private method.

Keep the allow-list; do not switch to send to silence it.

RuboCop cops for hooks and evaluation

Cop What it checks

Lint/MissingSuper

The callbacks inherited, method_added, method_removed, method_undefined and the singleton_method_* hooks call super, and so does initialize of a subclass whose parent has state. included, extended, prepended and const_added are not checked, so review those by hand.

Security/Eval

Forbids eval of a non-literal string. Disabling it for one line, with a comment saying why, is the usual exception.

Style/EvalWithLocation

String eval, class_eval, module_eval and instance_eval pass __FILE__ and __LINE__ (with the right offset for heredocs).

Style/DocumentDynamicEvalDefinition

An interpolated string given to class_eval has a comment showing what the generated code looks like.

Style/SendWithLiteralMethodName

public_send("title") with a literal name should be a plain call. Variables are fine.

Style/MixinGrouping, Naming/MethodParameterName

The hook examples above: one include per statement, parameter names of at least three characters.

The cop catalogue is at Lint, Security and Style. Configuration and the other tools are in Style, Linting and Static Analysis.

Pitfalls

  • A hook without super works until another module in the chain defines the same hook. Make super the first line.

  • inherited runs before the subclass body, so it cannot see the subclass’s methods or constants and, for Class.new, not even its name. Defer anything that needs them.

  • A registry filled by inherited or included is global state. Reloading code (or defining classes in tests) fills it with stale entries, and a registry that is written after boot from several threads needs a mutex.

  • method_added that defines a method on its own class recurses without end; wrap in another module or guard with a flag.

  • instance_eval and class_eval have opposite effects on a bare def. When a method lands in the wrong place, the first thing to check is which one was used.

  • Instance variables set inside an instance_eval DSL block land on the DSL object, not on the caller. The program raises no error, which makes it a hard bug to see.

  • instance_eval with a lambda fails because it passes one argument; with instance_exec it works.

  • String eval is slower than a block because it is compiled on every call, hides errors until run time, and is the main source of injection bugs. Never build the string from external data.

  • A Binding holds on to every local variable it can see, so objects stay alive as long as the binding does.

  • const_missing and const_added run for every lookup or definition in that class; keep them fast and call super.

  • Module#included is not called when a module is mixed into another module and then into a class; the hook sees the intermediate module.

  • A class-level DSL reads class-instance state. Subclasses do not share it unless inherited copies it.

Version notes

Version Change relevant to hooks, eval and DSLs

3.0

$SAFE is a normal global variable with no special behaviour. Binding#eval with one argument reports the file as (eval) and the line as 1.

3.1

Class#subclasses is added. instance_eval and instance_exec allocate the singleton class only when needed.

3.2

Module#const_added and Class#attached_object are added. The taint, untaint, trust and untrust families are removed. Kernel#binding raises RuntimeError when called from a method defined in C.

3.3

Evaluated code without a given file reports (eval at path:line).

4.0

Binding#implicit_parameters, Binding#implicit_parameter_get and Binding#implicit_parameter_defined? are added; Binding#local_variables no longer includes numbered parameters.

4.1 (preview)

Module#descendants is in the development NEWS. 4.1 is not released as of 2026-10-18, so the method is not used in the examples.

References