Refinements and Monkey Patching

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 classes are open: any program can reopen String or Integer and add, replace or remove methods, and the change is visible to every line of Ruby (and every library) running in the process. That is called monkey patching, and it is both the technique behind much of Ruby’s expressiveness and the source of some of its hardest-to-debug failures. Refinements (Module#refine and using) are the language’s answer: a way to extend a class that is visible only in the files and class bodies that ask for it. This page covers the whole subject: what a monkey patch is and how to find and tame one, Module#prepend as the patch that can call the original, refinements with their lexical scope rules, the method lookup order and super, the list of core methods that honor (and do not honor) refinements, import_methods, introspection, testing, the experimental Ruby::Box that isolates patches at process level, and a guide for choosing between all of these.

It builds on Classes and Objects, Modules and Mixins (include, prepend and extend) and Method Lookup and super, which explains the ancestor chain that refinements are inserted in front of. The mechanics that make patching possible are in Singleton Classes and Class-Level Behavior, Dynamic Dispatch and Method Definition and Hooks, eval and DSLs; how to look at what a patch changed is in Reflection, Introspection and Tracing. Visibility is covered in Visibility and Encapsulation, and the conversion methods that C code calls (to_str, to_ary) in Conversions and Coercion. How files are loaded, and therefore where a refinement is active, is in Code Loading: require and autoload. Testing patched code is in Testing with Minitest, and the usual mistakes are collected in Common Mistakes and Gotchas. For the same need in other languages, see C# extension members, Kotlin extension functions, Swift extensions, Scala extension methods, Groovy extension modules and Python decorators and metaclasses.

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, NewCops: enable, Style/Documentation off). The output shown is the real output. The examples that need the gem are run with ruby -Ilib, so require "shelf" finds it (see the shelf gem source); the shelf gem itself ships one refinement, Shelf::Refinements::TitleCase, which the examples reuse. One example (the collision in Collisions between patches) deliberately triggers a Ruby warning, and two carry a rubocop:disable comment for a deliberate illustration; each is called out where it appears.

Open classes and monkey patching

Ruby never closes a class. A class or module statement for a name that already exists reopens it, and every def inside adds a method or replaces an existing one. The classes shipped with Ruby are no exception: String, Integer, Array, Hash, Kernel and even BasicObject can be reopened. A change made this way to a class that you do not own is a monkey patch.

# frozen_string_literal: true

# A monkey patch: reopen a core class and add a method. Every caller sees it.
class String
  def title_case = split.map(&:capitalize).join(" ")
end

puts "the left hand of darkness".title_case
puts "a wizard of earthsea".respond_to?(:title_case)
puts String.method_defined?(:title_case)
puts String.instance_method(:title_case).owner
puts %w[ico stone].map(&:title_case).inspect
The Left Hand Of Darkness
true
true
String
["Ico", "Stone"]

The patch is a plain method definition, so everything in Methods and Parameters applies, and there is no special mark on the method. From the moment the class String body runs, title_case is an ordinary public instance method of String, owned by String itself (Method#owner prints String). Every piece of code in the process, including gems loaded later and code that was loaded earlier, can now call it, and respond_to? says yes.

Monkey patching is a legitimate tool. It is how a library adds to_json to the core classes after require "json", how a polyfill backports a method from a newer Ruby, and how a test double replaces a method for the duration of a test. The Ruby documentation says plainly what the cost is: the scope of the change is global, so all users of the patched class see the same changes, which can cause unintended side effects or breakage of programs. The sections below name those costs one by one.

The costs of a global change

Hazard What happens

Global reach

The patch is visible to the application, to every gem, and to the standard library. Code written years ago, that never heard of the patch, now runs against the new behavior.

Collisions

Two libraries (or a library and a newer Ruby) define the same method. The definition that loads last wins, silently unless -w is on. Ruby itself adds core methods in every release, and a patch that defined the same name earlier can mask the new one or be broken by it.

Load-order dependence

The behavior of a method depends on which require ran first. The same program behaves differently when a require is moved, when autoload loads a file lazily, or when a different gem set is installed.

Reach into C code

Replacing a method that the interpreter’s own C code calls (to_s, inspect, each, hash, eql?, coerce, to_str) changes what built-in methods such as Array#join, puts and Hash#[] do. Nothing in the call site hints at it.

Invisible provenance

Nothing in grep or in the stack trace says "this method comes from a patch"; Method#owner names the core class. Method#source_location is the only trace.

Hard to remove

Removing a patch means finding every caller that came to rely on it.

Hard to test

A patch applied by one test file leaks into every other test in the process, so order-dependent failures appear.

Collisions between patches

When two libraries define the same method on the same class, the later definition replaces the earlier one. With the -w flag (or $VERBOSE = true) Ruby reports the replacement on standard error with the file and line of both definitions; without the flag nothing is printed. The warning is deliberately triggered here to show what it looks like.

# frozen_string_literal: true

# Two libraries patch the same method. The last definition loaded wins.
module Libraries
  def self.load_a = String.class_eval { def slug = downcase.tr(" ", "-") }

  def self.load_b = String.class_eval { def slug = downcase.gsub(/\W+/, "_") }
end

Libraries.load_a
Libraries.load_b

puts "The Left Hand of Darkness".slug

# Find out where a method was defined.
location = "x".method(:slug).source_location
puts "#{File.basename(location[0])}:#{location[1]}"
p String.instance_method(:upcase).source_location
p String.method_defined?(:slug, false)
ex02.rb:7: warning: method redefined; discarding old slug
ex02.rb:5: warning: previous definition of slug was here
the_left_hand_of_darkness
ex02.rb:7
nil
true

Three tools find out who owns what. Method#source_location returns the file and line of a Ruby-defined method (and nil for one implemented in C, as String#upcase is). Module#method_defined? with the second argument false answers whether the class itself, not an ancestor, defines the method. And Method#owner names the class or module whose method table holds the method. RuboCop’s Lint/DuplicateMethods finds the same mistake statically inside one file.

A patched method that must keep working with every caller should also keep the signature of the original; the safest way to guarantee that is to forward everything with *, ** and & (see Calling Methods and Argument Forwarding).

What a monkey patch reaches

The reach of a patch includes the interpreter’s own C code. The example adds to_str and to_ary to a class that had neither. Before the patch, Array#join, string concatenation and multiple assignment do not treat the object as a string or an array; after it, they do, because those built-in methods ask the object for the implicit conversions (see Conversions and Coercion).

# frozen_string_literal: true

# The same additions as a plain monkey patch: every caller, Ruby or C, sees them.
module Demo
  class Receipt
    def to_s = "plain"
  end
end

receipt = Demo::Receipt.new
puts [receipt].join(",")
first, second = receipt
p [first.to_s, second]

Demo::Receipt.class_eval do
  def to_str = "patched-str"
  def to_ary = %w[patched ary]
end

puts [receipt].join(",")
puts(+"text " << receipt)
first, second = receipt
p [first, second]
plain
["plain", nil]
patched-str
text patched-str
["patched", "ary"]

Keep this contrast in mind for the refinement sections: a refinement of the same two methods is not seen by Array#join or by multiple assignment (see Which core methods honor refinements).

alias_method chains and prepend

Before refinements, before even Module#prepend (Ruby 2.0), a patch that wanted to call the original method used an alias chain: alias the original to a new name, define the new method, and call the alias. The technique works once and breaks when the patch is loaded twice (a load in a console, a reloading development server, two gems applying the same patch), because the second alias captures the already patched method, which calls the alias, which is now the patched method: unbounded recursion.

Module#prepend inserts a module in front of the class in the ancestor chain, so the module’s method runs first and super reaches the original. Prepending a module that is already in the ancestors does nothing, so the patch is idempotent.

# frozen_string_literal: true

module Demo
  # The older technique, alias_method chaining, breaks when applied twice.
  class Receipt
    def to_s = "receipt"
  end

  def self.apply_alias_patch
    Receipt.class_eval do
      alias_method :original_to_s, :to_s
      def to_s = "#{original_to_s}!"
    end
  end

  # prepend is safe to apply twice: the module is already in the ancestors.
  module Stamp
    def to_s = "#{super}!"
  end

  class Ticket
    def to_s = "ticket"
  end
end

Demo.apply_alias_patch
puts Demo::Receipt.new
Demo.apply_alias_patch
begin
  Demo::Receipt.new.to_s
rescue SystemStackError => e
  puts "#{e.class}: #{e.message}"
end

2.times { Demo::Ticket.prepend(Demo::Stamp) }
puts Demo::Ticket.new
p Demo::Ticket.ancestors.first(3)
receipt!
SystemStackError: stack level too deep
ticket!
[Demo::Stamp, Demo::Ticket, Object]

Source: Module#prepend

The same technique patches a class from the shelf gem. Shelf::Book is a Data class (see Struct, Data and Value Objects) whose block defines to_s. A prepended module wraps it without editing the gem, and Method#super_method and ancestors show the arrangement.

# frozen_string_literal: true

require "shelf"

# prepend puts the patch in front of the class, so super reaches the original.
module Shouting
  def to_s = "#{super}!"
end

isbn = Shelf::Isbn.new(digits: "978-0-306-40615-7")
book = Shelf::Book.new(isbn:, title: "Ubik", author: Shelf::Author.new(name: "Philip K. Dick"))
puts book

Shelf::Book.prepend(Shouting)
puts book
p Shelf::Book.ancestors.first(3)
p Shelf::Book.instance_method(:to_s).owner
p Shelf::Book.instance_method(:to_s).super_method.owner
Ubik by Philip K. Dick
Ubik by Philip K. Dick!
[Shouting, Shelf::Book, Data]
Shouting
Shelf::Book

A prepended patch is still global, but it is explicit (a named module with its own file), visible in ancestors, composable, removable in principle (not at run time: Ruby has no unprepend) and able to call the original with super. It is the recommended form of a global patch.

A defensive patching checklist

  • Prefer not to patch: use a function, extend on one object, or a wrapper (see Alternatives to patching a core class).

  • If a global change is unavoidable, make it a named module and prepend it, so the original stays reachable with super.

  • Add only methods that do not exist, and guard them, so a newer Ruby or another library wins (the polyfill below).

  • Keep every patch in one file or directory per class (for example core_ext/string.rb), documented and tested.

  • Choose names unlikely to collide (a library prefix), forward arguments completely, and keep the return type of the original.

  • Run the test suite with -w so redefinition warnings are visible.

  • Never replace methods that the interpreter or the standard library call (to_s, inspect, hash, eql?, each, coerce) in a way that changes their contract.

A polyfill adds a method only when it is missing. This is the one global patch that is nearly always safe, because it does nothing on a Ruby that already has the method.

# frozen_string_literal: true

# A guarded monkey patch: define the method only when it is missing,
# so a newer Ruby or another library wins.
module Polyfill
  def self.install(klass, name, &)
    return if klass.method_defined?(name) || klass.private_method_defined?(name)

    klass.define_method(name, &)
  end
end

p Polyfill.install(String, :each_char_pair) { each_char.each_slice(2).map(&:join) }
p Polyfill.install(String, :each_char_pair) { :never }
p Polyfill.install(Array, :sum) { :never }
p "isbn".each_char_pair
:each_char_pair
nil
nil
["is", "bn"]

Alternatives to patching a core class

Most of the time the need behind a monkey patch ("I want title_case on strings") can be met without changing String.

# frozen_string_literal: true

require "delegate"

# Alternatives that do not touch String for everybody.
module Titles
  def self.call(text) = text.split.map(&:capitalize).join(" ")

  def title_case = Titles.call(self)

  # A wrapper that forwards everything else to the wrapped string.
  class Heading < SimpleDelegator
    def title_case = Titles.call(__getobj__)
  end
end

# 1. A function.
puts Titles.call("the left hand of darkness")

# 2. extend on one object: only that string gains the method.
heading = (+"a wizard of earthsea").extend(Titles)
puts heading.title_case
puts "other".respond_to?(:title_case)

# 3. The wrapper.
puts Titles::Heading.new("the dispossessed").title_case
puts Titles::Heading.new("the dispossessed").length
The Left Hand Of Darkness
A Wizard Of Earthsea
false
The Dispossessed
16

Source: Object#extend

Technique Scope of the change Costs

A module function (Titles.call(text))

None: nothing is changed.

The call site reads less fluently.

Object#extend on one object

That one object (it gains a singleton class).

The object gains a singleton class, and every other object of the class is unaffected.

A wrapper such as SimpleDelegator

Only code that uses the wrapper.

The wrapper is a different object (equal?, is_a? and type checks differ).

A subclass

Only instances created from it.

Cannot change objects created elsewhere, such as literals.

Module#prepend

Global, for every instance of the class.

A monkey patch that is at least explicit and can call super.

A refinement

The files and class bodies that call using.

Lexical rules and a few unrefined core methods (see below).

Ruby::Box

A whole box (experimental in 4.0).

Experimental; breaks code that relies on global definitions.

Refinements

A refinement extends a class (or module) locally. It is defined in a module with Module#refine and switched on with using. The definition lives in a block whose self is an anonymous module, a Refinement, in which ordinary def statements define the new or replacing methods. The shelf gem has one, written exactly like this:

# frozen_string_literal: true

module Shelf
  module Refinements
    # Refines String instead of monkey patching it.
    module TitleCase
      refine String do
        def title_case = split.map(&:capitalize).join(" ")
      end
    end
  end
end

Source: Module#refine

The documentation describes the idea in three sentences: refinements reduce the impact of monkey patching on other users of the patched class, provide a way to extend a class locally, and can modify both classes and modules. Using the refinement is one line, using Shelf::Refinements::TitleCase, and from that line on the new method exists for code written below it.

# frozen_string_literal: true

require "shelf"

begin
  "the left hand of darkness".title_case
rescue NoMethodError => e
  puts e.message
end

using Shelf::Refinements::TitleCase

puts "the left hand of darkness".title_case
puts "a wizard of earthsea".respond_to?(:title_case)
puts "ursula".send(:title_case)
p %w[ico stone].map(&:title_case)
p String.method_defined?(:title_case)
p "x".methods.include?(:title_case)
undefined method 'title_case' for an instance of String
The Left Hand Of Darkness
true
Ursula
["Ico", "Stone"]
false
false

Notice what the output shows. The call written before using fails, with the usual NoMethodError message (Ruby 3.3 and later print for an instance of String, not the receiver’s inspect). After using, the method works, including through send and Symbol#to_proc. And the class itself is untouched: String.method_defined? is false and "x".methods does not list title_case. Reflection that lists methods does not see refinements.

The anatomy of a refinement

Piece Rule

refine Class do ... end

Private method of Module. The argument must be a class or a module (a module since Ruby 2.4); anything else raises TypeError (wrong argument type String (expected Class or Module)). Without a block it raises ArgumentError (no block given). Returns the Refinement it created.

Where refine may be called

In the body of a module. Class removes it, so Class.new { refine(String) {} } raises NoMethodError (undefined method 'refine' for class #<Class:0x...>). Calling it with an explicit receiver is an error too (private method 'refine' called for module ...).

The block

Evaluated like module_eval with the Refinement as self. def defines methods in the refinement; private, alias_method, attr_reader and constants work as in any module.

Several refine blocks in one module

Independent refinements (one per target). All of the module’s refinements become active together with one using.

using Mod

Activates every refinement of Mod in the scope below it. Available as main.using at the top level of a file and Module#using in a class or module body.

Refinement

The class of the object refine creates. A subclass of Module. Its public instance method is target; import_methods is private and is called inside the refine block.

include / prepend in the block

Deprecated in Ruby 3.1 and removed in 3.2; they raise TypeError (Refinement#include has been removed). Use import_methods. RuboCop’s Lint/RefinementImportMethods reports the old form.

extend in the block

Accepted, but it extends the Refinement object itself, not the target class.

A refinement can replace an existing method as well as add one. It can refine operators (the refined Integer#+ is used by 1 + 1 in the using scope), Kernel methods and the singleton methods of a class (refine String.singleton_class), as the next example shows with a module refinement and a class-method refinement bundled by including one refinement module into another.

# frozen_string_literal: true

# Refine a module, a singleton class, and bundle refinements by including
# one refinement module into another.
module Extras
  module Lists
    refine Enumerable do
      def second = drop(1).first
    end
  end

  module Parsing
    refine String.singleton_class do
      def from_pairs(pairs) = pairs.map { |k, v| "#{k}=#{v}" }.join("&")
    end
  end

  module All
    include Lists
    include Parsing
  end
end

using Extras::All

p [10, 20, 30].second
p (1..5).second
p String.from_pairs(page: 2, per: 10)
p Extras::All.refinements
20
2
"page=2&per=10"
[]

Module#include between refinement-holding modules is supported and documented: when a module X is included into a module Y, Y inherits the refinements of X, and refinements in descendants have higher precedence than those of ancestors. That lets a library publish one module (Extras::All) that activates a set of refinements with a single using. Note that Extras::All.refinements is empty: Module#refinements lists only the refinements the module defines itself.

Scope: where a refinement is active

Refinements are lexical. The documentation states the rules:

  • A refinement can be activated at the top level of a file and inside a class or module body, never inside a method body.

  • It stays active until the end of the current class or module body, or until the end of the file for a top-level using.

  • Code before the using call does not see it. This includes methods defined earlier in the same file.

  • A method defined where a refinement is active keeps it active when it runs, wherever it is called from.

  • A method defined outside the scope does not get the refinement, even when it is called from inside the scope.

  • require, load and calls into other files leave the scope, so a refinement is not visible in them.

  • Reopening a class later does not reactivate the refinement in the new body.

The first example shows the file-level rules. plain_title is defined before using; refined_title after. Symbol#to_proc applied at a call site below using sees the refinement, as the last line shows.

# frozen_string_literal: true

require "shelf"

# A refinement is lexical: it is active from the using call to the end of the
# file (at top level) or of the class or module body (inside one).
def plain_title(text) = text.title_case

using Shelf::Refinements::TitleCase

def refined_title(text) = text.title_case

def attempt
  yield
rescue NoMethodError => e
  e.message
end

puts "ico".title_case
puts refined_title("stone of tears")
puts(attempt { plain_title("ico") })
puts(attempt { [["ico"]].flatten.map(&:title_case) })
Ico
Stone Of Tears
undefined method 'title_case' for an instance of String
Ico

The second example walks through the remaining cases: a class (here a module) body, reopening it, the ban on using in a method, eval with a string, and a using in a statement that never runs. It deliberately reopens Heading in the same file, which RuboCop’s Style/OneClassPerFile reports; the cop is disabled on that one line.

# frozen_string_literal: true

require "shelf"

def attempt
  yield
rescue NoMethodError => e
  e.message
end

module Heading
  def self.before(text) = attempt { text.title_case }

  using Shelf::Refinements::TitleCase

  def self.after(text) = attempt { text.title_case }
end

puts Heading.before("ursula")
puts Heading.after("ursula")

# Reopening the module does not reactivate the refinement.
module Heading # rubocop:disable Style/OneClassPerFile
  def self.again(text) = attempt { text.title_case }
end

puts Heading.again("ursula")

# using is not allowed in a method body.
def activate = using(Shelf::Refinements::TitleCase)

begin
  activate
rescue RuntimeError => e
  puts "#{e.class}: #{e.message}"
end

# using inside eval lasts until the end of the eval string.
puts eval(<<~RUBY, nil, __FILE__, __LINE__ + 1)
  using Shelf::Refinements::TitleCase
  "in eval: " + "le guin".title_case
RUBY
puts "after eval: #{attempt { 'le guin'.title_case }}"

# using in a branch that never runs activates nothing.
using Shelf::Refinements::TitleCase if ENV.key?("NEVER_SET_SHELF")
puts "after if: #{attempt { 'le guin'.title_case }}"
undefined method 'title_case' for an instance of String
Ursula
undefined method 'title_case' for an instance of String
RuntimeError: main.using is permitted only at toplevel
in eval: Le Guin
after eval: undefined method 'title_case' for an instance of String
after if: undefined method 'title_case' for an instance of String

Notes on the outcomes:

  • Heading.before is defined above the using in the module body: it fails. Heading.after is defined below: it works.

  • Reopening Heading starts a new body in which the refinement is not active: again fails.

  • using inside a method is rejected at run time with RuntimeError (main.using is permitted only at toplevel when the method is defined at top level). using is a method call, so a using that never executes (an if that is not taken) activates nothing.

  • using in an eval string works for the code of the string and ends with it. The documentation says "Refinements are active until the end of the eval string". Passing an explicit Binding as the second argument is different on 4.0.7: the refinement then remains active for the code of that binding’s scope after the eval returns. The behavior is not part of the documented rules; do not rely on it.

  • String-evaluated code (eval, instance_eval with a string) runs with the refinements that are active at the call site (checked on 4.0.7).

Several files

The rules matter most across files. A using in a file applies to every method that file defines, however far away the caller is, and does not leak into the file that requires it. This is why a library can use a refinement internally without imposing it on its users.

Three files: the refinement, a view that activates it and defines a method, and the main script.

# frozen_string_literal: true

module Titles
  refine String do
    def headline = split.map(&:capitalize).join(" ")
  end
end
# frozen_string_literal: true

require_relative "titles"

using Titles

# The refinement is active in this file, so it is active whenever
# HeadingView#render runs, no matter who calls it.
class HeadingView
  def render(text) = "<h1>#{text.headline}</h1>"
end
# frozen_string_literal: true

require_relative "heading_view"

puts HeadingView.new.render("the lathe of heaven")

begin
  "the lathe of heaven".headline
rescue NoMethodError => e
  puts e.message
end
<h1>The Lathe Of Heaven</h1>
undefined method 'headline' for an instance of String

HeadingView#render was defined in a file where using Titles is active, so it can call headline when main.rb calls it, though main.rb never activated the refinement and cannot call headline itself.

Code position Refinement active?

In a file, before using M

No

In a file, after using M (including inside classes and methods defined there)

Yes, until the end of the file

In a class body, before using M

No

In a class body, after using M

Yes, until the end of that class body

Inside a method body

using is not allowed; the method sees what the enclosing scope activated

In the eval string that contains using M

Yes, until the end of the string

In a file loaded with require or load from the using file

No

In a class reopened after the using scope ended

No

Method lookup and super with refinements

Refinements are consulted before the ordinary lookup for the class they refine. For a call on an instance of class C in a scope where refinements of C are active, Ruby checks, in order:

  1. the refinements of C, in the reverse order in which they were activated (the latest using first);

  2. the modules prepended to C;

  3. C itself;

  4. the modules included in C.

If nothing is found, the search repeats with the superclass of C. The ordinary rules (singleton classes, ancestors, prepend) are the subject of Method Lookup and super.

flowchart TD A["call recv.label in a scope with using"] --> B["refinements of C, latest using first"] B -->|"found"| Z(["run it"]) B -->|"missing"| C["modules prepended to C"] C -->|"found"| Z C -->|"missing"| D["C itself"] D -->|"found"| Z D -->|"missing"| E["modules included in C"] E -->|"found"| Z E -->|"missing"| F{"superclass?"} F -->|"yes"| B2["repeat with the superclass: its refinements, prepended modules, class and included modules"] B2 --> Z F -->|"no"| M(["method_missing, then NoMethodError"])

The example builds the full chain: a class with a prepended module, a subclass relation, two refinements of the same class activated one after the other, and a refinement of the superclass.

# frozen_string_literal: true

# Lookup order for a call: refinements (latest first), prepended modules,
# the class, included modules, then the superclass.
module Lookup
  module Tagged
    def label = "prepended(#{super})"
  end

  class Edition
    def label = "class"
  end

  class Hardcover < Edition
    prepend Tagged

    def label = "hardcover(#{super})"
  end

  module First
    refine Hardcover do
      def label = "first(#{super})"
    end
  end

  module Second
    refine Hardcover do
      def label = "second(#{super})"
    end
  end

  # A refinement of the superclass.
  module OnEdition
    refine Edition do
      def label = "refined edition"
      def kind = "refined kind"
    end
  end
end

copy = Lookup::Hardcover.new
puts copy.label

using Lookup::First
puts copy.label

using Lookup::Second
puts copy.label

using Lookup::OnEdition
puts copy.label
puts copy.kind
prepended(hardcover(class))
first(prepended(hardcover(class)))
second(prepended(hardcover(class)))
second(prepended(hardcover(class)))
refined kind

Reading the output:

  1. Without refinements: the prepended Tagged runs first, then Hardcover, whose super reaches Edition.

  2. After using Lookup::First, the refinement runs first and its super calls the refined class’s method, which still starts at Tagged.

  3. After using Lookup::Second, Second is consulted before First. Its super also calls the refined class’s method (not First), so First is skipped. The documentation states this: super in a method of a refinement invokes the method in the refined class even if another refinement is active in the same context.

  4. After using Lookup::OnEdition (a refinement of the superclass), copy.label is unchanged: the subclass defines label, so the search ends before the superclass’s refinements are reached. The super inside Hardcover#label is in a file region where OnEdition is not active, so it reaches the plain Edition#label.

  5. copy.kind is a method that exists only in the refinement of the superclass, so it is found.

The documentation gives the same rule with numbers. A refinement of Numeric that defines / is never used for an Integer, because Integer#/ exists and the lookup stops at Integer; but a refinement that defines a new method on Numeric is found for an Integer receiver.

# frozen_string_literal: true

# Numeric is the superclass of Integer. Integer#/ exists, so a refined
# Numeric#/ is never reached for an Integer, but a new Numeric method is.
module NumericExtras
  refine Numeric do
    def /(_other) = "refined"
    def half = self / 2
  end
end

using NumericExtras

puts 1 / 2
puts 5.half
puts 2.5.half
0
2
1.25

Here 1 / 2 is 0 (the unrefined Integer#/), 5.half is 2 (it calls self / 2, and the / it reaches inside the refinement block is still Integer#/), and 2.5.half is 1.25: Float#/ exists too.

The rules for super in the documentation, in full: from a method of a refinement, super looks first at the modules included in the refinement; then, since the current class is a refinement, the lookup proceeds as above for the refined class (the "Method Lookup" list); then the superclass. A super in a method of a module that is merely included in a refinement does not follow the refinement rule.

Which core methods honor refinements

A refinement is active for Ruby code that is lexically inside the using scope. Calls made from Ruby code outside that scope, and calls made by the interpreter’s C code (Array#join, puts, the destructuring in a, b = obj, Hash lookups), do not see it. Over the years Ruby taught a short list of core methods to honor them:

Since What changed

2.0 / 2.1

Refinements honored by ordinary method calls with an explicit or implicit receiver, in the using scope (experimental in 2.0, stable in 2.1).

2.4

Honored by Symbol#to_proc (the &:sym form), Kernel#send and BasicObject#__send__. Module#refine accepts a module. Module.used_modules is added.

2.5

Honored in string interpolation (the to_s of "#{obj}").

2.6

Honored at block passing (&:sym as an argument to a method such as map), Kernel#public_send and Kernel#respond_to?.

2.7

Honored by Object#method and Module#instance_method.

3.1

Refinement#import_methods (and the Refinement class); include/prepend in a refinement deprecated.

3.2

Module#refinements, Module.used_refinements and Refinement#refined_class.

3.3

Refinement#target; Refinement#refined_class deprecated.

3.4

Refinement#refined_class removed.

# frozen_string_literal: true

# What does a refinement reach? Ruby code that is lexically inside the
# using scope, and a short list of core methods that were taught about them.
module Demo
  class Receipt
    def to_s = "plain"
    def inspect = "plain-inspect"
    def to_str = "plain-str"
    def to_ary = %i[plain ary]
  end

  module Fancy
    refine Receipt do
      def to_s = "fancy"
      def inspect = "fancy-inspect"
      def to_str = "fancy-str"
      def to_ary = %i[fancy ary]
      def total = 42
    end
  end
end

using Demo::Fancy
receipt = Demo::Receipt.new
name = :total

puts "-- honored"
puts "receipt: #{receipt}"
puts receipt.total
puts receipt.send(name)
puts receipt.public_send(name)
puts receipt.respond_to?(name)
puts [receipt].map(&:total).inspect
puts receipt.method(name).call
puts Demo::Receipt.instance_method(name).bind_call(receipt)

puts "-- not honored"
puts [receipt].join(",")
puts(+"text " << receipt)
p receipt
first, second = receipt
p [first, second]
p receipt.methods.include?(name)
p Demo::Receipt.method_defined?(name)
-- honored
receipt: fancy
42
42
42
true
[42]
42
42
-- not honored
plain-str
text plain-str
plain-inspect
[:plain, :ary]
false
false

The "honored" half: a normal call, string interpolation, send, public_send, respond_to?, map(&:total), method and instance_method all see the refinement. The "not honored" half: Array#join and string concatenation call to_str, p calls inspect, destructuring calls to_ary: each of these is C code outside the lexical scope, so the original methods run (the output prints the plain values, not the refined ones). The listing methods (methods, method_defined?) do not include refined methods either. The documentation says that introspection methods such as Kernel#method or Kernel#methods do not honor refinements and that this "may be changed in the future"; in practice method and instance_method have honored them since 2.7 and the listing methods still do not. Test the behavior you depend on.

The differences in daily use:

Feature Monkey patch Refinement

obj.method_name in Ruby code

Yes

Yes, in the using scope

Array#join(sep) calls to_str / to_s on elements

Yes

No

a, b = obj calls to_ary

Yes

No

puts obj / p obj call to_s / inspect

Yes

No

"#{obj}" calls to_s

Yes

Yes (2.5)

Hash lookups call hash and eql?

Yes

No

obj.send(:name) / public_send

Yes

Yes (2.4 / 2.6)

obj.respond_to?(:name)

Yes

Yes (2.6)

Class#instance_methods, obj.methods

Yes

No

Visible to other files and gems

Yes

No

Can be undone

No

By leaving the scope

import_methods

A refinement cannot include a module (an error since the deprecation in 3.1). The replacement, Refinement#import_methods (Ruby 3.1), copies the methods of one or more modules into the refinement. Because the copy is compiled as part of the refinement, the refinement is active inside the imported methods: they can call the other refined methods of the same refinement. Methods that are only included normally never see it.

The documentation adds the restriction that follows from copying: only methods written in Ruby can be imported. C-defined methods (most of Enumerable, for instance) raise ArgumentError naming the first one it meets.

# frozen_string_literal: true

# A module cannot be included into a refinement; import_methods copies its
# Ruby-defined methods so that they are compiled with the refinement active.
module Imports
  module Padding
    def pad(width) = ljust(width, ".")
    def padded_title(width) = title_case.then { |s| s.ljust(width, ".") }
  end

  module Display
    refine String do
      def title_case = split.map(&:capitalize).join(" ")

      import_methods Padding
    end
  end
end

using Imports::Display

puts "dune".pad(8)
puts "the dispossessed".padded_title(20)

# Mixed in normally, the module's methods were compiled without the refinement.
plain = (+"dune").extend(Imports::Padding)

begin
  plain.padded_title(8)
rescue NameError => e
  puts "#{e.class}: #{e.name}"
end

# Only methods written in Ruby can be imported.
begin
  Module.new do
    refine(String) { import_methods Enumerable }
  end
rescue ArgumentError => e
  puts "#{e.class}: #{e.message}"
end

begin
  Module.new do
    refine(String) { include Comparable } # rubocop:disable Lint/RefinementImportMethods
  end
rescue TypeError => e
  puts "#{e.class}: #{e.message}"
end
dune....
The Dispossessed....
NameError: title_case
ArgumentError: Can't import method which is not defined with Ruby code: Enumerable#each_cons
TypeError: Refinement#include has been removed

Padding#padded_title calls title_case with an implicit receiver. When mixed into plain through extend, it was compiled outside the refinement and fails with a NameError (a bare name that is neither a local variable nor a method reads as a variable-or-method reference, hence NameError, not NoMethodError). Imported into Display, it is compiled with the refinement active and works. The include Comparable in the last block is the deliberate illustration of the removed form; RuboCop’s Lint/RefinementImportMethods flags it and the line carries a rubocop:disable comment.

Introspection

The introspection API is small and consistent:

API Meaning

Module#refinements (3.2)

Array of the Refinement objects defined by the module (not those inherited through include).

Refinement#target (3.3)

The class or module refined. Replaces refined_class, which was deprecated in 3.3 and removed in 3.4.

Module.used_modules (2.4)

Array of modules whose refinements are active at the call site, latest first.

Module.used_refinements (3.2)

Array of the active Refinement objects.

Refinement#import_methods (3.1)

See the previous section.

Method#owner

For a refined method, the Refinement itself.

# frozen_string_literal: true

require "shelf"

title_case = Shelf::Refinements::TitleCase

p title_case.refinements
refinement = title_case.refinements.first
p refinement.class
p refinement.target
p refinement.instance_methods(false)
p refinement.ancestors.first(2)
p refinement.respond_to?(:refined_class)

p Module.used_modules
p Module.used_refinements

using Shelf::Refinements::TitleCase

p Module.used_modules
p Module.used_refinements

# respond_to? knows about refinements; the reflection that lists methods does not.
p "x".respond_to?(:title_case)
p String.method_defined?(:title_case)
p "x".methods.include?(:title_case)
p refinement.instance_method(:title_case).owner
[#<refinement:String@Shelf::Refinements::TitleCase>]
Refinement
String
[:title_case]
[#<refinement:String@Shelf::Refinements::TitleCase>]
false
[]
[]
[Shelf::Refinements::TitleCase]
[#<refinement:String@Shelf::Refinements::TitleCase>]
true
false
false
#<refinement:String@Shelf::Refinements::TitleCase>

Module.used_modules and Module.used_refinements report the lexical state at the call site, so both print [] above the using line and the module and refinement below it. respond_to? honors refinements (true), while method_defined? and methods do not (false). Refinement#inspect is #<refinement:Target@Module>.

Refinements in practice

  • Define refinements in a module that says what it does (Shelf::Refinements::TitleCase), one target per refine block, in a file of its own.

  • Name the refinement module after its purpose, not the refined class, and document the added methods in the module’s comment, since documentation generators do not list refined methods under the refined class.

  • Activate with using at the top of the file that needs it, so the dependency is visible. A using in a file that is only loaded for its side effects ends with that file and activates nothing elsewhere.

  • A library that uses a refinement internally keeps using in its own files; it does not leak to its users, which is the entire point.

  • A library that offers a refinement to its users documents the exact using line and the Ruby version.

  • Keep the refined methods small and prefer adding methods over replacing existing ones; a replacing refinement changes the behavior of code in the using scope that expects the original.

  • In a refinement block, a method replacing an existing one can reach the original with super.

Testing a refinement

A test file activates the refinement exactly like production code would, with using at the top of the file (the refinement is active until the end of the file, so it does not leak into other test files), and asserts the behavior on the refined class. It is also worth testing that the core class is untouched.

# frozen_string_literal: true

require "minitest/autorun"
require "shelf"

# The test file activates the refinement, exactly like production code would.
using Shelf::Refinements::TitleCase

class TitleCaseTest < Minitest::Test
  def test_capitalizes_each_word
    assert_equal "The Left Hand Of Darkness", "the left hand of darkness".title_case
  end

  def test_core_class_is_untouched
    refute String.method_defined?(:title_case)
  end

  def test_other_words_stay_untouched
    assert_equal "Ico", "ICO".title_case
  end
end
Run options: --seed 42

# Running:

...

Finished in 0.000207s, 14492.7492 runs/s, 14492.7492 assertions/s.

3 runs, 3 assertions, 0 failures, 0 errors, 0 skips

Source: Minitest

Run it with ruby -w -Ilib test.rb --seed 42. The seed makes the order reproducible; the timing line differs on each run. Minitest and RSpec are covered in Testing with Minitest and RSpec and Test Doubles. For a test that stubs a method on a core class, use a stub or allow(...).to receive for the duration of one test instead of a global patch.

Performance and interoperability

  • Calls to refined methods take a different path in the VM than ordinary calls, so a hot loop may behave differently from the unrefined version. Measure before drawing conclusions, with the tools in Performance, Benchmarking and Profiling.

  • C code never sees a refinement, and neither does library code outside the using scope: JSON.generate([1]) ignores a refined Integer#to_json, and a Hash uses the original hash and eql?.

  • Tools that rely on ObjectSpace, instance_methods or documentation generators do not list refined methods.

  • IRB accepts a top-level using typed at the prompt, and the refinement stays active for the rest of the session (see IRB and Debugging).

Ruby::Box: isolation of definitions (experimental)

Ruby 4.0 adds Ruby::Box, an experimental in-process separation of classes and modules. The documentation’s description is "separated spaces in a Ruby process, to isolate application codes, libraries and monkey patches". A box is a copy of the definitions of the built-in classes; a monkey patch applied in one box is invisible in every other box. Ruby prints a warning that the feature is experimental when it starts with it enabled (hide it with -W:no-experimental).

The feature is off by default. It must be enabled when the process starts by setting the environment variable RUBY_BOX=1; setting it from inside the program does nothing. Without it, Ruby::Box.new raises RuntimeError (Ruby Box is disabled. Set RUBY_BOX=1 environment variable to use Ruby::Box.). The example below must be run as RUBY_BOX=1 ruby -W:no-experimental box_demo.rb.

# frozen_string_literal: true

# Ruby::Box is experimental in Ruby 4.0. Start the process with RUBY_BOX=1.
puts Ruby::Box.enabled?

box = Ruby::Box.new
box.eval(<<~RUBY)
  class String
    def title_case = split.map(&:capitalize).join(" ")
  end

  puts "in the box: " + "the dispossessed".title_case
RUBY

p "the dispossessed".respond_to?(:title_case)
puts Ruby::Box.current == Ruby::Box.main
p box.eval("Ruby::Box.current") == box
p box::String == String
true
in the box: The Dispossessed
false
true
true
true

Source: Ruby Box

What the output shows:

  • Ruby::Box.enabled? reports whether RUBY_BOX=1 was given.

  • Ruby::Box.new creates a user box; Box#eval, Box#require, Box#require_relative and Box#load run code in it.

  • The String patch defined inside the box (title_case) works there and is invisible outside: the main box answers false.

  • Ruby::Box.current is the box that runs the calling code; the main script runs in the "main" box.

  • box::String == String: builtin classes are the same objects, with box-local definitions; a reference like box::String is the String of the current box.

How it differs from refinements: a refinement is a lexical tool, controlled by using in the source, and it can be used in one file of a program; a box is a process-level tool, controlled by where the code is loaded (a file runs in a single box), and everything defined by the loaded files, including constants, global variables and class variables, is separated. The two are complementary. Constants, globals, top-level methods and monkey patches made in a box stay there.

The documentation lists known issues: native extensions may fail to install with RUBY_BOX=1, some libraries such as active_support/core_ext may not work, and methods defined in a box may not be called by built-in methods written in Ruby (a patch of Hash#each in a user box does not change Hash#map, which runs in the root box). Because of those limitations and because the feature is experimental, the API may change; do not use it in production in 4.0. How boxes interact with require and the load path is covered in Code Loading: require and autoload.

Choosing a technique

flowchart TD N(["I want to add or change behavior of a class I do not own"]) --> Q1{"can a function, a wrapper or a subclass do?"} Q1 -->|"yes"| W(["use it: no patch"]) Q1 -->|"no"| Q2{"only some files need it?"} Q2 -->|"yes"| R(["refine and using"]) Q2 -->|"no"| Q3{"must it wrap an existing method?"} Q3 -->|"yes"| P(["prepend a named module"]) Q3 -->|"no"| Q4{"adding a method that may already exist?"} Q4 -->|"yes"| PF(["guarded polyfill"]) Q4 -->|"no"| Q5{"must the patch not leak to other code in the process?"} Q5 -->|"yes"| BOX(["Ruby::Box, experimental"]) Q5 -->|"no"| MP(["monkey patch, documented and tested"])
Situation Use Why

A helper for a few call sites

A module function

No global state, no surprises.

Fluent methods on a core class in one library

Refinement

Private to the library; users are unaffected.

Behavior change that every caller must see (a security fix, a backport)

Prepended module or guarded polyfill

Global but explicit and removable in the source.

Wrap a method of a class you do not own

prepend

super reaches the original; idempotent.

A test double for one test

A stub scoped to the test

Reverts automatically.

Separate two incompatible versions of the same patch in one process

Ruby::Box (experimental)

Process-level separation.

An internal DSL that adds methods to blocks

instance_eval / Object#extend

No core class touched (see Hooks, eval and DSLs).

Where older books and tutorials are outdated

Books and tutorials on Ruby metaprogramming predate several changes that matter here. The points below correct the common ones; each is verified against the 4.0 documentation and the release notes.

Older statement Current behavior

Refinements arrived in Ruby 2.0

They were experimental in 2.0 and became a stable feature in 2.1; the later honoring of send, &:sym, public_send, respond_to? and method (2.4 to 2.7) removed most "refinements are not seen by X" caveats.

Refinements are not honored by send, Symbol#to_proc or respond_to?

They are (see the table above). What is still unrefined is C-level calls such as Array#join, puts, destructuring and Hash keys.

A refinement may include a module

include and prepend in a refinement were deprecated in 3.1 and removed in 3.2 (they raise TypeError); use import_methods (3.1).

Use alias_method to wrap the original

Prefer Module#prepend (2.0) and super; alias chains recurse when applied twice.

Refinement#refined_class

Removed in 3.4; use Refinement#target (3.3).

Introspection of refinements is not possible

Module#refinements and Module.used_refinements (3.2), Module.used_modules (2.4), Refinement#target (3.3).

A monkey patch cannot be isolated from the rest of the process

Ruby 4.0 adds the experimental Ruby::Box for process-level isolation of definitions; refinements isolate lexically.

Error messages like undefined method `x' for nil:NilClass

Ruby 3.3 and 3.4 print undefined method 'x' for nil; the older format with a backtick and the class name is gone.

Hash#inspect shows {:name=>"Ico"}

Since 3.4: {name: "Ico"}.

Common exceptions and warnings

Message Cause

NoMethodError: undefined method 'title_case' for an instance of String

The call is outside the using scope, or before the using line, or in a file that did not activate it.

NameError: undefined local variable or method 'title_case' for ...

The same, for a bare name with an implicit receiver.

RuntimeError: main.using is permitted only at toplevel

using was called inside a method.

TypeError: Refinement#include has been removed / Refinement#prepend has been removed

include or prepend inside refine; use import_methods.

ArgumentError: no block given

refine was called without a block.

TypeError: wrong argument type String (expected Class or Module)

The argument of refine is not a class or module.

ArgumentError: Can't import method which is not defined with Ruby code: Enumerable#...

import_methods with a module whose methods are written in C.

NoMethodError: undefined method 'refine' for class #<Class:0x...>

refine is not available on classes, only on modules.

NoMethodError: private method 'refine' called for module ...

refine was called with an explicit receiver; call it inside the module body.

warning: method redefined; discarding old name and previous definition of name was here

-w or $VERBOSE = true: a method was redefined (a monkey patch collision).

SystemStackError: stack level too deep

An alias-chain patch was applied twice.

RuntimeError: Ruby Box is disabled. Set RUBY_BOX=1 environment variable to use Ruby::Box.

Ruby::Box.new without RUBY_BOX=1.

warning: Ruby::Box is experimental, and the behavior may change in the future!

Ruby started with RUBY_BOX=1; -W:no-experimental hides it.

RuboCop and tooling

  • Lint/RefinementImportMethods (RuboCop 1.27, pending by default, enabled by NewCops: enable) reports include and prepend inside a refine block and suggests import_methods.

  • Lint/DuplicateMethods reports a method defined twice in the same class within the analyzed files: the static counterpart of Ruby’s runtime redefinition warning.

  • Style/OneClassPerFile reports reopening classes in the same file (useful in a codebase that wants each patch in its own file).

  • ruby -w (or $VERBOSE = true) prints the method redefined warnings; Warning[:deprecated] = true shows deprecated features such as chilled string mutation.

  • Method#owner and Method#source_location find the origin of a method; Module#refinements and Module.used_refinements list refinements.

Pitfalls

  • Calling a refined method from a method defined above the using line: the method is outside the scope.

  • Expecting using in a gem’s lib file to activate the refinement for the application. It does not leak, by design.

  • Relying on Array#join, puts or a, b = obj to see a refined to_s, to_str or to_ary: the C implementations call the originals.

  • Refining a method that Hash or Set call (hash, eql?): the container uses the original and lookups misbehave.

  • Putting using inside a method or an if branch and expecting a conditional activation.

  • Refining a superclass method that the subclass overrides: the subclass’s method wins.

  • Writing a patch with alias_method and loading it twice.

  • Patching a core class in a test file and forgetting that the change leaks to the rest of the suite.

  • Reopening or creating a class by accident: class Isbn at the top level creates a new ::Isbn instead of reopening Shelf::Isbn, and Ruby reports no error.

  • Using Ruby::Box as a stable security boundary: it is experimental and not a sandbox.

Version notes

Version Change

2.0

Module#refine and using (experimental). Module#prepend.

2.1

Refinements stabilized.

2.4

Refinements honored by Symbol#to_proc, Kernel#send and BasicObject#__send__. Module#refine accepts a module. Module.used_modules.

2.5

Refinements honored in string interpolation.

2.6

Refinements honored at block passing, Kernel#public_send and Kernel#respond_to?.

2.7

Refinements honored by Object#method and Module#instance_method.

3.1

Refinement class and Refinement#import_methods; include and prepend in a refinement deprecated (removed in 3.2; they raise TypeError).

3.2

Module#refinements, Module.used_refinements, Refinement#refined_class.

3.3

Refinement#target; refined_class deprecated. NoMethodError messages say for an instance of ClassName.

3.4

Refinement#refined_class removed. Error messages use an apostrophe instead of a backtick.

4.0

Experimental Ruby::Box (RUBY_BOX=1).