Skip to content

Ruby Open Classes — Monkey Patching Refinements and Safe Modification Explained

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Ruby Open Classes. We cover key concepts, practical examples, and best practices to help you master this topic.

Ruby open classes let you reopen and modify any class at any time, including core classes like String and Array, with refinements providing lexically-scoped alternatives for safer code.

What You'll Learn

  • How to reopen and modify existing classes
  • When monkey patching is appropriate
  • Using refinements for scoped modifications
  • Best practices for safe class modifications

Why It Matters

Open classes are Ruby's most powerful and dangerous feature. Durga Antivirus Pro uses refinements to add scan methods to String without global pollution. Doda Browser uses monkey patching for cross-cutting logging in development. Understanding when to use each approach separates expert Rubyists from beginners.

Real-World Use

Rails adds dozens of methods to core classes (2.days.ago, "hello".titleize). Testing frameworks use monkey patching to add assertion methods. Gems use refinements to add functionality without conflicts.

flowchart LR
    A["Open Classes"] --> B["Monkey Patching"]
    B --> C["Refinements"]
    C --> D["safe_modify"]
    D --> E["Best Practices"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Reopening Classes

You can reopen any class at any time to add or modify methods:

class String
  def shout
    "#{upcase}!"
  end
end

puts "hello".shout        # HELLO!
puts "ruby rocks".shout   # RUBY ROCKS!

This works because Ruby's class definitions aren't closed — they're executable code that adds to the existing class.

Adding Methods to Existing Classes

class Array
  def second
    self[1]
  end

  def middle
    self[size / 2]
  end
end

arr = [10, 20, 30, 40, 50]
puts arr.second  # 20
puts arr.middle  # 30

Overriding Existing Methods

class Integer
  def +(other)
    self - other  # Redefine addition as subtraction!
  end
end

puts 5 + 3  # 2

This is why monkey patching is dangerous — you can break core language behavior.

Why Monkey Patching is Dangerous

class String
  def capitalize
    "#{self[0].upcase}#{self[1..-1]}"  # Bug: nil when empty
  end
end

puts "hello".capitalize  # Hello
puts "".capitalize       # NoMethodError: undefined method `upcase' for nil

A bug in your patch breaks every capitalize call in your entire application, including gems and Rails internals.

Safe Monkey Patching with Modules

module StringExtensions
  def to_slug
    downcase.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
  end
end

String.include(StringExtensions)

puts "Hello World!".to_slug        # hello-world
puts "Ruby 3.3 Features!".to_slug  # ruby-33-features

Using include with a module is safer than reopening the class directly.

Refinements (Ruby 2.0+)

Refinements provide scoped, opt-in modifications to classes:

module StringRefinements
  refine String do
    def shout
      "#{upcase}!"
    end

    def reverse_and_shout
      "#{reverse.upcase}!"
    end
  end
end

class Document
  using StringRefinements

  def format(text)
    text.shout
  end
end

puts Document.new.format("hello")  # HELLO!

# Outside the scope, String is unchanged
# puts "hello".shout  # NoMethodError

Refinements are active only within the lexical scope where using is called. They don't pollute global behavior.

Refinements in a Module

module ArrayStats
  refine Array do
    def median
      sorted = sort
      mid = size / 2
      size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0
    end

    def mode
      group_by(&:itself).max_by { |_, v| v.size }.first
    end
  end
end

class Analyzer
  using ArrayStats

  def analyze(data)
    puts "Median: #{data.median}"
    puts "Mode: #{data.mode}"
  end
end

Analyzer.new.analyze([1, 2, 2, 3, 4])
# Median: 2
# Mode: 2

Refinements Can Activate Methods

module IntegerDays
  refine Integer do
    def days
      self * 86400
    end

    def ago
      Time.now - days
    end

    def from_now
      Time.now + days
    end
  end
end

using IntegerDays

puts 5.days          # 432000
puts 2.days.ago      # 2026-06-26 12:00:00 (approximately)

This is how Rails' 2.days.ago works, but with refinements instead of global monkey patching.

The super Keyword in Patches

When overriding a method, you can still call the original with super:

class Hash
  def [](key)
    if key.is_a?(String)
      puts "Warning: accessing string key '#{key}'"
    end
    super
  end
end

h = { "name" => "Alice" }
puts h["name"]
# Warning: accessing string key 'name'
# Alice

The prepend Pattern

prepend inserts a module before the class in the method lookup chain, letting you override methods while still calling the original via super:

module LoggingWrapper
  def save
    puts "[LOG] Saving #{self.class}"
    super
    puts "[LOG] Saved successfully"
  end
end

class User
  prepend LoggingWrapper

  def save
    puts "User saved to database"
  end
end

User.new.save
# [LOG] Saving User
# User saved to database
# [LOG] Saved successfully

Open Classes for DSLs

Open classes make Ruby great for DSLs:

class Object
  def given(description, &block)
    puts "Given: #{description}"
    block.call if block
  end

  def when_action(description, &block)
    puts "When: #{description}"
    block.call if block
  end

  def then_expect(description, &block)
    result = block.call
    status = result ? "PASS" : "FAIL"
    puts "Then: #{description} [#{status}]"
  end
end

given "a user is logged in" do
  user = { name: "Alice", role: "admin" }

  when_action "the user views the admin panel" do
    result = user[:role] == "admin"
    then_expect "they see admin features" do
      result
    end
  end
end

Common Mistakes

1. Patching Core Methods Globally

class String
  def length
    super * 2  # Breaks everything!
  end
end

2. Not Using Refinements for Library Code

# Bad — pollutes globally
class Array
  def pluck(method)
    map(&method.to_sym)
  end
end

# Good — scoped to your code
module MyExtensions
  refine Array do
    def pluck(method)
      map(&method.to_sym)
    end
  end
end

3. Overwriting Methods Instead of Extending

# Bad — loses original behavior
class String
  def upcase
    "OVERRIDDEN"
  end
end

# Good — extends with super
class String
  def upcase
    "#{super}!"
  end
end

4. Expecting Refinements in called Methods

module MyRefine
  refine String do
    def dash_case
      tr("_", "-")
    end
  end
end

class Parser
  using MyRefine

  def parse(text)
    text.dash_case  # Works here
  end

  def self.parse(text)
    text.dash_case  # NoMethodError! Refinements don't apply to class methods
  end
end

5. Forgetting Refinements Don't Work in eval

module Test
  refine String do
    def foo; "bar"; end
  end
end

using Test
"hello".foo  # Works

"hello".instance_eval { foo }  # NoMethodError — refinements don't apply in eval

Practice Questions

1. What is an open class in Ruby?

The ability to reopen and modify any class at runtime, including adding, changing, or removing methods from existing classes like String, Array, or your own classes.

2. What are refinements?

A Ruby 2.0+ feature that allows scoped, opt-in modifications to classes. Refinements are active only within the lexical scope where using is called, preventing global pollution.

3. When should you use refinements vs monkey patching?

Use refinements for library code, production applications, and when you need to add methods to core classes. Use global monkey patching only for debugging, prototyping, or when the change must be application-wide.

4. How does prepend differ from include?

prepend inserts the module before the class in the method lookup chain. This lets the module override class methods while still calling the original via super. include inserts after the class.

Challenge: Create a refinement that adds a to_currency method to Numeric that formats numbers as currency strings (e.g., 1234.5 becomes "$1,234.50").

Solution
module CurrencyFormat
  refine Numeric do
    def to_currency(unit: "$", precision: 2)
      formatted = format("%.#{precision}f", self)
      parts = formatted.split(".")
      parts[0] = parts[0].reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
      "#{unit}#{parts.join(".")}"
    end
  end
end

using CurrencyFormat

puts 1234.5.to_currency            # $1,234.50
puts 1000000.to_currency           # $1,000,000.00
puts 0.99.to_currency              # $0.99
puts 42.to_currency(unit: "€", precision: 0)  # €42

FAQ

{{< faq question="Is monkey patching always bad?" >}} No. Rails famously adds methods to core classes (2.days.ago). The key is using refinements for library code and being careful with global patches. Test thoroughly and document your patches. {{< /faq >}}

{{< faq question="What's the difference between refinements and monkey patching?" >}} Monkey patching modifies classes globally — every part of your application sees the change. Refinements scope changes to specific lexical contexts using using. Refinements are the safer choice for most cases. {{< /faq >}}

{{< faq question="Can I use refinements in gems?" >}} Yes, and you should. Refinements let gems add methods to core classes without affecting the host application. The developer must opt-in with using in their own code. {{< /faq >}}

{{< faq question="Why does Rails use monkey patching instead of refinements?" >}} Rails was created before refinements existed (Ruby 2.0, 2013). Modern Rails uses refinements in some places but still relies on global patches for backward compatibility and because Rails owns the application layer. {{< /faq >}}

{{< faq question="Can I remove a method I added to an open class?" >}} You can use remove_method or undef_method. remove_method removes the method from the current class but inherited methods remain. undef_method prevents the method from being called at all. {{< /faq >}}

Try It Yourself

# open_classes_demo.rb

module SlugExtension
  refine String do
    def slugify
      downcase.gsub(/[^a-z0-9]+/, "-").gsub(/(^-|-$)/, "")
    end
  end
end

module ArrayExtension
  refine Array do
    def average
      sum.to_f / size
    end

    def median
      sorted = sort
      mid = size / 2
      size.odd? ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2.0
    end
  end
end

using SlugExtension
using ArrayExtension

puts "Hello World!".slugify     # hello-world
puts "Ruby 3.3!!".slugify       # ruby-33
puts [1, 2, 3, 4, 5].average   # 3.0
puts [1, 2, 3, 4, 5].median    # 3
puts [1, 2, 3, 4].median       # 2.5
puts "hello".respond_to?(:slugify)  # true

What's Next

Now that you understand open classes and refinements, apply your knowledge to Enumerable and the powerful collection methods it provides.

Topic Description Link
Ruby Enumerable map, select, reduce, group_by {{< ref "21-enumerable" >}}
Ruby Duck Typing respond_to?, method_missing {{< ref "15-duck-typing" >}}
Python Decorators Compare with Python's approach Python

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro