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 (11)
The setter case is the one that actually bit me in a Rails model. I had a callback doing
status = "paid"and spent an embarrassing amount of time wondering why nothing persisted, because of course it just created a local variable. After that, I started treating every bare assignment inside an instance method with suspicion. The scope gate framing is nice too, since it explains why the block case feels inconsistent when it really is not.Yeah, that
status = "paid"case is a nasty one because it looks completely reasonable at a glance. Ruby is basically saying โsure, thatโs a local variableโ while youโre thinking โno, update the model.โA similar thing shows up when you have a setter and a method with the same name:
The interesting part is that the setter itself isnโt the weird bit. The missing receiver is. Ruby needs
selfthere to know you mean the assignment method rather than creating a local. Thatโs also why I like the scope-gate framing. Once you start looking for the receiver and the current scope instead of memorizing Rubyโs โexceptions,โ these cases become much easier to reason about.That minimal repro is going straight into my notes. You put it better than I did: the setter isn't the weird bit, the missing receiver is. Once you read self as a message send instead of boilerplate, the behavior stops feeling arbitrary.
Exactly. And I think that distinction becomes even more useful once you hit Rails code that mixes implicit receivers with DSL-style APIs.
For example, in an ActiveRecord model,
updateandupdate!are explicit message sends, while something likestatus = "paid"is parsed as local assignment before Ruby ever gets to the question of which object should receive a message. That difference can become surprisingly important when a callback or service object is doing several things that look like ordinary Ruby expressions.Once you start reading
selfas the receiver of a message, these Rails โmagicโ moments become much less magical. You can usually trace the behavior back to ordinary Ruby parsing and method dispatch.Good point. The update(...) case is a nice anchor: it is clearly a message to the instance, so it steadies the mental model before you hit the ambiguous assignment. I still double-take on bare assignment inside callbacks now and then, even after years of this.
Yeah, and thatโs probably why I think the โreceiverโ model holds up better than memorizing Ruby syntax rules.
What gets interesting in Rails is that DSLs can deliberately make the receiver less obvious. Take something like:
Inside
publish_event,statusis an implicit message to the currentself, whileOrderEvent.create!has an explicit receiver. Thensaved_changesis another implicit message, but its meaning depends entirely on what objectselfis at that point.Thatโs where Rails can make the same Ruby rule feel more complicated than it actually is. The framework adds a lot of DSL surface area, but the underlying question is still pretty mechanical: who is receiving this message right now?
Once that becomes the first question you ask, callbacks, scopes, validations, and model DSLs start feeling a lot less magical.
That publish_event example nails it: implicit self in the method body, explicit OrderEvent.create!, then saved_changes where the receiver is whoever self happens to be. 'Who is receiving this message right now' is the one question that cuts through every Rails DSL. Stealing that framing.
The publish_event example is the sharpest version of this. saved_changes only means something against the current self, and self there is an instance sitting in an after_commit. You end up asking "who am I talking to" instead of "what's the rule for after_commit". That is the model paying rent.
That is a better rule of thumb than the one I gave. The only thing I would add is that the question works best once you start reading DSL calls like after_commit as ordinary class method calls, which is exactly the part Rails tutorials tend to skip.
Exactly. The callback-object case is where this distinction gets particularly interesting, because the receiver isn't always the model instance anymore.
Inside
OrderEventCallback,selfis the callback object, not theOrderinstance. That's why the model has to be passed in explicitly asorder. The same method name,after_commit, now runs in a different receiver context than a callback defined directly on the model.There's another subtlety here, though:
after_commitruns after the database transaction has committed. If creating the event fails, the order update has already persisted and won't be rolled back. So understanding the receiver tells you which object you're talking to, while understanding the callback's lifecycle tells you what guarantees you still have.That's probably the bigger lesson with Rails DSLs: the syntax tells you how behavior is registered, but you still need to trace the actual receiver and execution boundary to understand what the code guarantees.
The lifecycle point is the one I'd underline. Knowing the receiver answers which object you are talking to, but the execution boundary answers what you can still rely on, and an event write that fails after the order already persisted is exactly the kind of bug that only shows up in production. Great addition to the thread.