Modules Explained
A module in Ruby is a container for methods, classes, and constants that — unlike a class — can never be instantiated with .new and can never be subclassed. That single restriction is what makes modules so useful: Ruby leans on them for two distinct jobs, organizing code into namespaces so names don’t collide, and mixing shared behavior into otherwise unrelated classes without resorting to multiple inheritance. Once you see how include, extend, and prepend actually change an object’s method lookup chain, modules stop feeling like a separate topic and start feeling like the same object model you already know, applied more flexibly.
Overview / How Modules Work
A module is defined with module…end, and it looks a lot like a class. The difference is behavioral: you can’t write SomeModule.new, and a module can’t appear as a class’s superclass. Because a module can’t sit in a normal inheritance chain the way a class does, Ruby uses it for two very different jobs instead: namespacing (grouping related classes, methods, and constants under one name) and mixins (packaging a set of methods that can be dropped into any class to give it shared behavior).
Ruby deliberately does not support multiple inheritance — a class can only have one superclass. Modules are the sanctioned workaround. When you write include SomeModule inside a class body, Ruby doesn’t copy the module’s methods into the class. Instead, it splices the module into the class’s ancestor chain, immediately above the class itself. Every object has an ancestor chain, and every method call walks that chain from most specific to least specific, looking for a matching method: first the object’s singleton class (if it has singleton methods), then any modules prepended to the class, then the class itself, then any included modules (searched in the reverse order they were included), then the superclass and its own included modules, and so on up through Object, Kernel, and finally BasicObject. You can inspect this chain yourself by calling SomeClass.ancestors.
This is also the mechanism behind super: calling super doesn’t mean “look in the parent class” so much as “continue the same lookup along the ancestor chain from just after where the current method was found.” That’s why a module method and a class’s own method of the same name can cooperate through super without either one needing to know the other exists.
There’s a close relative of include called extend. Where include mixes a module’s methods in as instance methods, extend mixes them in as methods on one specific object — most commonly used inside a class body to add class-level (“static-like”) methods. And prepend, added later to the language, inserts the module before the class in the ancestor chain instead of after, letting the module’s method run first and optionally call super to reach the class’s own implementation — a clean way to wrap or decorate existing behavior.
Syntax
The most basic module, used as a namespace with a module-level (“static”) method:
module Greeter
def self.hello(name)
"Hi, #{name}!"
end
end
puts Greeter.hello("Sam")
Output:
Hi, Sam!
| Keyword | What it does | Typical use |
|---|---|---|
include |
Adds the module’s methods as instance methods, inserted just above the class in the ancestor chain | Sharing behavior between objects, e.g. Comparable, Enumerable |
extend |
Adds the module’s methods directly to the receiver’s singleton class | Adding class-level methods, or extra behavior on one object |
prepend |
Inserts the module below the class in the ancestor chain, so its methods run before the class’s own | Wrapping or decorating an existing method, then calling super |
module_function |
Turns subsequent methods into private instance methods and public methods on the module itself | Utility modules, similar to Math |
Namespace::Const |
Accesses a constant or class nested inside a module | Avoiding naming collisions |
Examples
Example 1: Using a Module as a Namespace
The simplest use of a module has nothing to do with mixins — it’s just a named box that keeps related classes and constants together and out of the global namespace:
module Shapes
PI = 3.14159
class Circle
def initialize(radius)
@radius = radius
end
def area
(Shapes::PI * @radius ** 2).round(2)
end
end
end
circle = Shapes::Circle.new(5)
puts circle.area
puts Shapes::PI
Output:
78.54
3.14159
Because Circle is defined inside Shapes, its full name is Shapes::Circle, and it reaches the module’s constant with Shapes::PI (or just PI from inside the module body, since constant lookup checks the lexically enclosing scope first). If another part of the program also defined a class called Circle, the two would never collide, because they live in different namespaces.
Example 2: Sharing Behavior with a Mixin
The more common use of modules is packaging behavior that multiple, otherwise unrelated classes can share by calling include:
module Greetable
def greet
"Hello, my name is #{name}!"
end
end
class Person
include Greetable
attr_reader :name
def initialize(name)
@name = name
end
end
alice = Person.new("Alice")
puts alice.greet
puts Person.ancestors.inspect
Output:
Hello, my name is Alice!
[Person, Greetable, Object, Kernel, BasicObject]
Greetable doesn’t know anything about Person specifically — it just assumes whatever class includes it defines a name method. Any class that provides one, from a Product to a Pet, could include Greetable and get the same greet method for free. That’s the essence of a mixin: behavior defined once, usable across classes that share nothing else.
Example 3: Mixing In Comparable
Ruby’s standard library ships several ready-made mixins, and Comparable is one of the most useful. Define a single method, <=> (the “spaceship operator”, returning -1, 0, or 1), include Comparable, and you get <, >, <=, >=, ==, between?, clamp, and the ability to sort a collection of your objects, all for free:
class Money
include Comparable
attr_reader :cents
def initialize(cents)
@cents = cents
end
def <=>(other)
cents <=> other.cents
end
def to_s
"$#{cents / 100.0}"
end
end
prices = [Money.new(500), Money.new(150), Money.new(1000)]
puts prices.sort.map(&:to_s).join(", ")
puts Money.new(300) > Money.new(200)
Output:
$1.5, $5.0, $10.0
true
sort works because Array#sort compares elements with <=>, and Comparable is what turns that one method into every other comparison operator. Write <=> once, get more than half a dozen methods back.
How It Works Step by Step
Walking through alice.greet from Example 2 shows exactly what include did:
alice.greetis called. Ruby first checksalice‘s singleton class — no singleton method namedgreetis defined there, so the search continues.- Next Ruby checks the
Personclass itself.Persondoesn’t definegreetdirectly. - Next it checks modules
included intoPerson, and findsGreetable, which does definegreet. The search stops here and that method runs. - Inside
greet,selfis stillalice— mixin methods run with the including object asself, exactly like a method defined directly on the class — sonameresolves throughalice‘s ownattr_reader, returning"Alice". Person.ancestorsprints the exact chain that was searched:[Person, Greetable, Object, Kernel, BasicObject].Greetablesits directly abovePersonbecause that’s whereincludespliced it in.
Common Mistakes
Mistake 1: calling an instance method on the module itself. A method defined normally inside a module (def double(x)) is an instance method — it only becomes available once the module is mixed into a class with include. Calling it directly on the module, as if the module understood it, raises NoMethodError:
module MathHelpers
def double(x)
x * 2
end
end
puts MathHelpers.double(4)
To call a method directly on the module — the way you’d call Math.sqrt(9) — either define it with def self.method_name, or add module_function above the definitions so each one becomes both a private instance method and a public method on the module:
module MathHelpers
module_function
def double(x)
x * 2
end
end
puts MathHelpers.double(4)
Output:
8
Mistake 2: assuming a constant is fully immutable. A constant only locks the reference — reassigning it elsewhere triggers a warning, but nothing stops code from mutating the array or hash the constant points to. Passing a constant into a method that mutates it with << silently changes it everywhere it’s used:
module Config
DEFAULT_TAGS = ["ruby", "web"]
end
def add_tag(tags)
tags << "backend"
end
add_tag(Config::DEFAULT_TAGS)
puts Config::DEFAULT_TAGS.inspect
Output:
["ruby", "web", "backend"]
Freeze the array when you define it, and have callers build a new collection instead of mutating the shared one:
module Config
DEFAULT_TAGS = ["ruby", "web"].freeze
end
def add_tag(tags)
tags + ["backend"]
end
new_tags = add_tag(Config::DEFAULT_TAGS)
puts new_tags.inspect
puts Config::DEFAULT_TAGS.inspect
Output:
["ruby", "web", "backend"]
["ruby", "web"]
Best Practices
- Prefer mixing in a small, focused module over building a deep class hierarchy just to share a couple of methods.
- Name mixins that add a capability with an
-ableadjective (Comparable,Enumerable,Greetable) so their purpose is obvious at the include site. - For a module that’s purely a bag of utility methods, never meant to be mixed in, make every method a module method with
module_functionordef self.method_namerather than leaving them as unreachable instance methods. - Document what method(s) a mixin expects the including class to define —
Comparableexpects<=>; your own module should be just as explicit. - Freeze constants that hold mutable objects like arrays and hashes so a stray
<<elsewhere in the codebase can’t silently mutate shared state. - Reach for
prependonly when you specifically need to run code before or around an existing method and callsuper; for ordinary behavior sharing,includeis simpler and far more common.
Practice Exercises
1. Write a module called Loggable with one instance method, log(message), that prints [ClassName] message using the including object’s own class name (hint: self.class.name inside the module method). Include it in two unrelated classes, such as Order and User, and confirm each one logs with its own class name.
2. Build a namespace module named Bank containing a class Account and a module-level constant MIN_BALANCE. Access the constant from outside the module with Bank::MIN_BALANCE, and create an account with Bank::Account.new.
3. Create a module Sizeable that includes Comparable and defines <=> in terms of a size method that any including class must provide. Mix it into two different classes (say Box and Crate), give each a size, and confirm you can sort an array containing instances of both by size.
Summary
- A module is defined with
module…end; it can never be instantiated or subclassed. includeinserts a module’s instance methods into a class’s ancestor chain, just above the class.extendadds a module’s methods as methods on the receiver itself, commonly used for class-level methods.prependinserts a module below the class in the chain, letting it intercept calls and forward to the original withsuper.- Method lookup always walks a fixed order: singleton class, prepended modules, the class, included modules (most recently included first), then superclasses up to
BasicObject— check it anytime withSomeClass.ancestors. - Modules can also namespace classes and constants (
Namespace::Thing) to avoid name collisions, entirely independent of mixins. module_functionordef self.method_nameturns a module’s methods into callable module-level utilities, similar toMath.sqrt.- Constants only protect the reference from reassignment — the object itself stays mutable unless you
freezeit.
