Open classes and monkey patching in Ruby
One of Ruby’s most distinctive features is that classes are never truly closed. You can add methods to any class at any time, even built-in ones like String or Array. This capability, known as open classes, is incredibly powerful but comes with significant responsibility. This guide explores how open classes work, why they exist, and how to use them safely.
Key takeaways
- Open classes let you add methods to existing Ruby classes without subclassing.
- Monkey patching can improve readability, but it can also create collisions and surprises.
- Refinements, subclasses, module prepending, and namespaced modules are safer alternatives when you need to extend behaviour.
- The more widely shared the class, the more carefully you should treat changes to it.
If you only remember one thing, remember that open classes are a tool for behaviour, not a shortcut for poor design. Use them when they make the API clearer or when a framework needs to extend Ruby itself. Avoid them when a small helper method or a module would do the job more cleanly.
What are open classes?
In languages like Java or C++, once a class is defined, you cannot add new methods to it. Ruby takes a different approach: any class can be reopened and modified at any point in your program. This is called “open classes.”
class String
def shout
upcase + "!"
end
end
"hello".shout # => "HELLO!"
We just added a shout method to Ruby’s built-in String class. This works because in Ruby, classes are objects—specifically, instances of Class. When you reference a class that hasn’t been defined, Ruby creates it. When you reference an existing class, Ruby simply reopens it.
That behaviour is part of what makes Ruby feel so open-ended. You are not limited to the APIs that existed when the class was first defined, which is why Rails and many gems can add expressive helpers to core types.
How open classes work
Every class in Ruby has a constant that points to a Class object. When you use the class keyword with an existing class name, Ruby doesn’t create a new class—it reopens the existing one and adds whatever you define inside the block.
# This doesn't create a new Array
# It reopens the existing Array class
class Array
def summarize
"Array with #{length} elements"
end
end
[1, 2, 3].summarize # => "Array with 3 elements"
This mechanism is the foundation for many of Ruby’s dynamic features and is heavily used by frameworks like Rails.
The important detail is that reopening a class changes the class globally. That is useful when the change is intentional and shared, but it is also why open classes deserve more caution than a normal helper method. Any code that loads after the reopening sees the modified class, which means even distant parts of the codebase can be affected by a single change. That is what distinguishes a deliberate extension from an accidental side effect: the author needs to know the scope of the change and communicate it clearly.
Monkey Patching
When you modify a built-in class like String, Integer, or Hash, you’re engaging in what’s called “monkey patching.” The term carries a somewhat negative connotation because of the risks involved, but it’s a powerful technique when used carefully.
A practical example
Rail’s ActiveSupport extends Ruby core classes with useful methods:
# ActiveSupport adds many methods to core classes
"hello".present? # => true
"".present? # => false
nil.present? # => false
[1, 2, 3].present? # => true
[].present? # => false
{}.present? # => false
{a: 1}.present? # => true
These extensions make code more readable and are considered “acceptable” monkey patching because they’re well-documented, widely-used, and namespaced to avoid conflicts.
That is the difference between a helpful extension and a risky one. Well-named, widely documented patches are easier to audit and easier to remove if the ecosystem changes.
The dangers of monkey patching
While open classes are powerful, they can cause serious problems if used carelessly.
1. Method collisions
If two different libraries add the same method to a class, one will override the other, potentially breaking functionality:
# Library A adds this method
class String
def slugify
downcase.gsub(" ", "-")
end
end
# Library B adds a different implementation
class String
def slugify
downcase.tr(" ", "_")
end
end
# Whichever loaded last wins
"Hello World".slugify # => "hello_world" (or "hello-world")
This kind of silent override is especially dangerous because neither library knows about the other. The second library loads, replaces the method, and the first library’s callers start seeing different results without any error message. The only defense is awareness: if you are adding methods to a shared class, document the names clearly and check that no other dependency claims the same surface.
2. Unexpected Behavior
Modifying core classes can lead to subtle bugs that are hard to track down. Even a single method override can ripple through every part of the application that touches the modified class, and the symptoms often show up far from the actual change.
class Array
def first
"custom first"
end
end
# Now every array's 'first' method is changed
[1, 2, 3].first # => "custom first"
[].first # => "custom first"
This breaks the expected behavior of Array#first throughout your entire application.
The danger is not only that the method changes. It is also that the change can happen far away from the code that depends on it, which makes debugging slower and more frustrating.
3. Confusing code
When you modify core classes, anyone reading your code might be confused about where methods come from:
# Is this a built-in method or did someone modify String?
"my string".some_custom_method
That confusion matters because Ruby does not warn you when a method comes from a reopened class. The reader has to know the extension exists already, or they have to go hunting for it.
Safer ways to extend existing classes
Here are strategies to harness open classes safely.
1. Use refinements (Ruby 2.0+)
Refinements let you modify classes in a scoped, temporary way:
module StringExtensions
refine String do
def shout
upcase + "!"
end
end
end
# Only available within the using block
class MyApp
using StringExtensions
def greet
"hello".shout # => "HELLO!"
end
end
# Outside the refinement scope, String is unchanged
"hello".shout # => NoMethodError
Refinements solve many monkey patching concerns by limiting the scope of modifications.
This is often the best option when you want a local improvement without changing the global meaning of a core class. The code stays explicit, and the extension only applies where you opted in.
2. Subclass instead
When possible, create your own class that extends the built-in one:
class CustomArray < Array
def summarize
"Array with #{length} elements"
end
end
# Original Array is unchanged
[1, 2, 3].summarize # => NoMethodError
CustomArray.new([1, 2, 3]).summarize # => "Array with 3 elements"
Subclassing works well when the new behaviour is genuinely a different type rather than a different convenience method. It keeps the original class untouched and makes the extension easier to reason about.
3. Use module prepending (Ruby 2.0+)
Prepending a module lets you override methods while calling the original:
module StringReverser
def reverse
"Reversed: #{super}"
end
end
class String
prepend StringReverser
end
"hello".reverse # => "Reversed: olleh"
This approach maintains access to the original method via super.
Module prepending is a good fit when you want to change behaviour while still preserving the original implementation. It is powerful, but it should still be used with restraint because it changes method lookup order.
4. Namespace your extensions
Create a module that wraps the extended functionality:
module MyApp
module StringExtensions
def summarize
"#{self} (#{length} chars)"
end
end
end
# Use explicitly
"hello".extend(MyApp::StringExtensions).summarize
# => "hello (5 chars)"
Namespacing keeps the extension discoverable and avoids polluting the global class with methods that only matter in one application. That makes the codebase easier to scan and the extension easier to remove later if the design changes.
When to reopen a class
Open classes are appropriate when:
- Building a framework that needs to extend core classes (like Rails does)
- Adding domain-specific methods for a specific application
- Creating internal extensions with clear documentation
- Using refinements for temporary, scoped modifications
If the method really belongs to the type and the extension is broadly useful, an open class can be the most natural way to express it. The key is to be honest about the scope and the maintenance cost.
When to leave classes closed
Avoid open classes when:
- You need to add utility methods—consider module functions instead
- You’re working on code others will maintain
- The modification would change expected core behavior
- You can solve the problem with inheritance or composition
The rule of thumb is simple: if the extension would surprise another team, a future maintainer, or a library that shares the same runtime, choose a narrower alternative.
Frequently asked questions
Are open classes unique to Ruby?
No, but Ruby makes them much more common and much easier to use. That openness is part of the language’s design, and it is one reason Ruby frameworks can feel so expressive.
Is monkey patching always bad?
No. It is a tool. The real question is whether the change is documented, namespaced, and safe for the code that depends on the class. Rails uses this pattern carefully, which is why many developers accept it in that context.
What is the safest way to extend a core class?
Refinements are usually the safest option when you want a scoped change. Subclassing or namespaced modules are also good when the behaviour is specific to your application.
Summary
Open classes are one of Ruby’s most powerful features, enabling the dynamic metaprogramming that makes Rails and other frameworks possible. However, with great power comes great responsibility:
- Open classes let you modify any class at any time
- Monkey patching core classes can cause conflicts and bugs
- Use refinements, subclasses, prepending, or namespacing for safer modifications
- Always consider the maintainability implications before modifying built-in classes
Used judiciously, open classes become an invaluable tool in your Ruby toolkit. Open classes are part of what makes Ruby feel flexible and expressive, letting frameworks and applications extend core behaviour in ways that would be awkward in more rigid languages. The same power can also make code harder to understand if the changes are spread too widely or documented too poorly. Use the broadest extension only when the benefit is real and shared. Otherwise, prefer refinements, subclasses, or namespaced modules so the behaviour stays local and predictable.
Next steps
Now that you have a solid framework for deciding when to reach for open classes, the natural next question is what Ruby metaprogramming looks like at the method level. Once you are comfortable with class-level changes, method-level dynamism becomes the next layer to explore.
See Also
- /guides/ruby-metaprogramming-basics/ — Dynamic method creation and the metaprogramming tools that complement open classes
- /guides/ruby-method-missing/ — Intercepting calls to undefined methods, a technique often used alongside class reopening
- /guides/ruby-refinements/ — A more controlled alternative to monkey patching that limits method changes to a specific scope