self is one of those Ruby keywords that looks easy until you meet it in real code.
You see it in a setter:
self.name = normalized_name
Then in a class method:
def self.from_cents(...)
Then somewhere inside a DSL or instance_eval, and suddenly the question isn't really:
What does
selfmean?
The better question is:
Who is
selfright here?
That small change makes Ruby's object model much easier to follow.
The setter case is where it starts to matter
Consider a model-like object:
class Customer
attr_accessor :name, :email
def initialize(name:, email:)
@name = name
@email = email
end
def normalize
self.name = name.strip.downcase
self.email = email.strip.downcase
end
def contact
"#{name} <#{email}>"
end
end
customer = Customer.new(
name: "Maya",
email: "MAYA@EXAMPLE.COM "
)
customer.normalize
puts customer.contact
There is something easy to miss here:
self.name = name.strip.downcase
Why not:
name = name.strip.downcase
Because those two lines are doing different things.
The second one is a local variable assignment.
The first one calls the name= setter on the current object.
Ruby lets us call the getter without explicitly writing the receiver:
name
But assignment creates an ambiguity. When we write:
self.name = ...
we are making the receiver explicit.
In other words:
self.name = "Maya"
means:
Call
name=on the object I'm currently working with.
That object is self.
And notice something important.
We didn't create self.
We didn't assign it.
Ruby already knew what self was when normalize started running.
That is the first useful clue.
self is less about a special keyword and more about the object currently receiving the work.
Most of the time, Ruby hides it from you
Take a slightly more realistic object:
class Invoice
def initialize(customer, total)
@customer = customer
@total = total
@status = :open
end
def close
return false unless valid_for_closing?
mark_as_paid
notify_customer
true
end
private
def valid_for_closing?
@total.positive? && @status == :open
end
def mark_as_paid
@status = :paid
end
def notify_customer
puts "Invoice notification sent to #{@customer}"
end
end
Inside close, Ruby sees:
mark_as_paid
notify_customer
You could make the receiver explicit:
self.mark_as_paid
self.notify_customer
But normally there is no reason to.
The receiver is already there.
It is self.
So when Ruby evaluates:
mark_as_paid
the useful mental model is:
self.mark_as_paid
The receiver is implicit.
This is one of those Ruby features that makes code pleasant to read, but it can also hide an important detail.
The receiver isn't missing.
It is invisible.
And that becomes interesting when we start asking when Ruby changes that context.
There are scope gates for self
This is where I find it useful to stop thinking about every Ruby construct as if it creates a new self.
It doesn't.
A block is a good example.
Consider:
class Report
def initialize(rows)
@rows = rows
end
def failed_rows
failures = []
@rows.each do |row|
next unless row[:status] == :failed
failures << {
id: row[:id],
reason: row[:error]
}
end
failures
end
end
Inside the block:
@rows.each do |row|
row is the block's local value.
It is not self.
And the block does not automatically replace the surrounding self.
So inside that block, self is still the Report instance.
This gives us a useful mental distinction:
Not every nested piece of Ruby code is a new self scope.
A block can introduce local variables and control flow without automatically changing the receiver.
But structures such as:
class ...
module ...
def ...
create important boundaries in how Ruby evaluates code and what self refers to.
And then there are APIs that deliberately change it.
For this article, I'll call these boundaries scope gates. That's a mental model, not an official Ruby term.
The useful idea is simply:
A block is not automatically a
selfgate. Context-changing constructs and APIs are.
Once you see that, some Ruby behavior that initially feels inconsistent becomes much easier to predict.
And now we can look at the class side.
The same keyword can point at the class
Consider a factory-style class method:
class Invoice
def initialize(customer, total)
@customer = customer
@total = total
end
def self.from_cents(customer, cents)
new(customer, cents / 100.0)
end
def summary
"#{@customer}: $#{format('%.2f', @total)}"
end
end
invoice = Invoice.from_cents("Maya", 12500)
puts invoice.summary
This line:
def self.from_cents
can look like special Ruby grammar.
It isn't really a separate universe.
The important part is still the receiver.
While Ruby evaluates the body of the class definition, self refers to the Invoice class object.
So:
def self.from_cents
is essentially saying:
Define
from_centson the object currently referenced byself.
And in this context, that object is Invoice.
That is why this works:
Invoice.from_cents(...)
The class itself is an object, and that object can receive method calls.
Now compare the two situations:
invoice.summary
Here the receiver is an Invoice instance.
Invoice.from_cents(...)
Here the receiver is the Invoice class object.
Nothing about the keyword changed.
The context did.
That distinction is much more useful than memorizing "instance method versus class method" as two unrelated Ruby features.
Rails makes this even more visible
You see the same idea constantly in Rails models.
For example:
class Subscription < ApplicationRecord
scope :active, -> {
where(status: :active)
}
def self.for_customer(customer)
active.where(customer: customer)
end
def cancel!
update!(
status: :cancelled,
cancelled_at: Time.current
)
end
end
There are several receivers involved here.
When you call:
subscription.cancel!
the receiver is the subscription instance.
Inside cancel!, the call to:
update!(...)
is being sent to that instance.
But:
Subscription.for_customer(customer)
is sent to the Subscription class object.
And Rails adds another interesting layer with scope.
The block passed to scope is not simply "a new self." The surrounding Ruby context and the way Rails evaluates the scope both matter.
This is why a useful question when reading Rails code is not just:
What method is this?
but:
What object is receiving this method here?
That question scales much better when the code gets more dynamic.
Then instance_eval deliberately moves the context
Now we can make the context change explicit.
Consider a configuration object:
class Configuration
def initialize
@values = {}
end
def [](key)
@values[key]
end
def values
@values.dup
end
end
config = Configuration.new
config.instance_eval do
@values[:environment] = "production"
@values[:region] = "eu-west"
@values[:logging] = :verbose
end
puts config[:environment]
puts config[:region]
Why can this block access:
@values
directly?
Because instance_eval evaluates the block with the receiver as self.
Before entering the block, self referred to the surrounding context.
Inside:
config.instance_eval do
self becomes config.
That is a deliberate context change.
And this is where the earlier idea of scope gates becomes useful.
The block itself didn't inherently create a new self.
instance_eval did.
Ruby gives the programmer an explicit mechanism for saying:
Evaluate this code as if this object is the current receiver.
That is a very different thing from simply passing a block around.
instance_exec takes the same idea one step further
There is a subtle difference between instance_eval and instance_exec that becomes important when designing DSLs.
Suppose we want to configure an object, but we also want to pass information into the block:
class Endpoint
def initialize
@settings = {}
end
def settings
@settings
end
end
endpoint = Endpoint.new
method = :post
path = "/users"
endpoint.instance_exec(method, path) do |http_method, route|
@settings[:method] = http_method
@settings[:path] = route
@settings[:authenticated] = true
end
puts endpoint.settings
The important part is:
endpoint.instance_exec(method, path) do |http_method, route|
instance_exec changes self to endpoint, just like the context-changing idea we saw with instance_eval.
But it also lets us pass arguments into the block.
So inside the block we have both:
self
pointing at endpoint, and:
http_method
route
coming from the outside.
That combination is useful when building expressive APIs.
It lets a DSL control the execution context without losing the ability to feed the block data.
And suddenly a pattern like:
resource :users do
method :get
path "/users"
end
starts looking less magical.
The DSL designer may be combining:
- a receiver that becomes
self - a block that defines the configuration
- arguments or yielded values
- method dispatch through that context
The syntax is the surface.
The context is the machinery underneath.
This is why Ruby DSLs can feel strange
Once you start thinking in terms of context, Ruby DSLs become less mysterious.
You might see code that looks almost like configuration:
resource :users do
path "/users"
method :get
authentication :required
end
The interesting question isn't only:
What does
resourcedo?
It is also:
What is
selfwhile this block is being evaluated?
Maybe the DSL is yielding to a builder object.
Maybe it is using instance_eval.
Maybe it is using instance_exec because it needs both a changed receiver and arguments.
Maybe it is delegating method calls.
The surface syntax can look almost identical while the underlying execution model is very different.
That's why self becomes particularly useful when reading metaprogramming-heavy Ruby.
Instead of getting stuck on the syntax, look for the receiver.
Then look for the context.
Then look for the point where that context changes.
So, who is self?
I don't think of self as a definition I need to memorize anymore.
I think of it as a signpost.
When I see:
self.name = value
I ask:
Which object is receiving this assignment?
When I see:
def self.build
I ask:
What is
selfwhile Ruby is evaluating this class body?
When I see:
something.instance_eval do
I ask:
Did Ruby just change the receiver for this block?
When I see:
something.instance_exec(data) do |value|
I ask two questions:
What is
selfnow?
and:
What data was passed into this context?
And when I see a Ruby DSL that seems to have methods appearing out of nowhere, I ask:
Who is
selfhere, and where did that context come from?
That question usually gets me closer to the actual behavior than simply remembering what the keyword means.
Because self isn't really the interesting part.
Context is.
Once you start reading Ruby through that lens, self becomes less of a keyword to memorize and more of a way to locate yourself inside Ruby's object model.
And I'm curious how other Ruby developers approach this.
Do you consciously think about self while reading Ruby code, or has it become one of those things you only notice when the context suddenly changes?

Top comments (0)