Instance Variables and attr_accessor
Every object in Ruby needs a place to keep its own private data — a Dog needs to remember its name, a BankAccount needs to remember its balance. That place is the instance variable, a piece of state that belongs to one specific object and lives for as long as that object does. Because instance variables are private by default, Ruby gives you attr_accessor and its relatives to generate the getter and setter methods that expose them safely. Understanding both halves of this pair is the foundation of writing real Ruby classes.
Overview / How it works
An instance variable is any variable whose name starts with a single @, such as @name or @balance. It is scoped to one object: two instances of the same class each have their own independent copy, even though they share the same class definition and the same methods. Instance variables are typically assigned inside initialize, the method Ruby calls automatically when you write ClassName.new(...), but they can be assigned in any instance method.
Here is the part that surprises people coming from languages like Java or Python: instance variables in Ruby do not need to be declared anywhere. The first time you assign @name = "Rex", Ruby creates that variable on that specific object. If you read an instance variable that has never been assigned, Ruby does not raise an error — it simply returns nil, because in Ruby, an uninitialized instance variable is treated as if it holds nil. This is a direct consequence of Ruby’s truthiness rules: since only nil and false are falsy (everything else, including 0 and "", is truthy), a missing instance variable reading as nil lets you write if @cache to mean “has this been set yet.”
Crucially, instance variables are not accessible from outside the object by default. If you write class Dog; def initialize(name); @name = name; end; end and then try Dog.new("Rex").@name, you get a syntax error — there is no dot syntax for reaching into another object’s instance variables. The only way to read or write them from outside is through a method that the object itself defines. That is exactly what attr_accessor, attr_reader, and attr_writer exist to automate: they are class-level methods that generate ordinary instance methods for you at the moment the class body is evaluated, saving you from typing def name; @name; end over and over.
Syntax
class ClassName
attr_accessor :symbol_name # generates both a reader and a writer
attr_reader :symbol_name # generates only a reader (get)
attr_writer :symbol_name # generates only a writer (set)
end
attr_accessor— takes one or more symbols and defines both a getter (obj.name) and a setter (obj.name = value) for the matching instance variable@name.attr_reader— defines only the getter. Use it for state that outside code should be able to read but never directly reassign.attr_writer— defines only the setter. Rarely used alone, but useful for write-only configuration values.- You can pass multiple symbols to any of them:
attr_accessor :title, :pages, :isbngenerates six methods in one line. - The symbol name must match the instance variable name (without the
@):attr_reader :balanceexposes@balance.
Examples
Example 1: Writing accessor methods by hand
Before reaching for attr_accessor, it helps to see what it replaces. Here every getter is written manually:
class Dog
def initialize(name, breed)
@name = name
@breed = breed
end
def name
@name
end
def breed
@breed
end
end
dog = Dog.new("Rex", "Labrador")
puts dog.name
puts dog.breed
puts dog.instance_variables.inspect
Rex
Labrador
[:@name, :@breed]
The name and breed methods each just return the corresponding instance variable — that is a getter, and it is exactly what attr_reader generates for you. Note the last line: every object can report its own instance variables via instance_variables, which returns an array of symbols.
Example 2: Replacing boilerplate with attr_accessor
class Book
attr_accessor :title, :pages
attr_reader :isbn
def initialize(title, pages, isbn)
@title = title
@pages = pages
@isbn = isbn
end
end
book = Book.new("Practical Ruby", 320, "978-1-234")
puts book.title
book.title = "Practical Ruby, 2nd Edition"
puts book.title
book.pages += 10
puts book.pages
puts book.isbn
Practical Ruby
Practical Ruby, 2nd Edition
330
978-1-234
title and pages get both a reader and a writer because of attr_accessor, so book.title = "..." works and book.pages += 10 works too (that line desugars to book.pages = book.pages + 10). isbn only got attr_reader, so it can be read but there is no isbn= method — trying book.isbn = "other" would raise NoMethodError, which is intentional: an ISBN shouldn’t change after a book is created.
Example 3: A realistic class with validation
attr_accessor is not always the right choice. Sometimes a setter needs to enforce a rule, so you write it by hand instead of exposing raw mutation:
class BankAccount
attr_reader :owner, :balance
def initialize(owner, balance: 0)
@owner = owner
@balance = balance
end
def deposit(amount)
@balance += amount
end
def withdraw(amount)
raise ArgumentError, "insufficient funds" if amount > @balance
@balance -= amount
end
end
account = BankAccount.new("Priya", balance: 100)
account.deposit(50)
account.withdraw(30)
puts "#{account.owner}'s balance: #{account.balance}"
begin
account.withdraw(1000)
rescue ArgumentError => e
puts "Error: #{e.message}"
end
Priya's balance: 120
Error: insufficient funds
balance only has a reader, so outside code can see the balance but cannot set account.balance = 999999 directly. The only ways to change it are deposit and withdraw, which means every change goes through validation. This is the core reason to prefer attr_reader plus explicit methods over attr_accessor whenever a value has rules attached to how it changes.
How it works step by step
- When Ruby evaluates a class body and hits
attr_accessor :title, it immediately defines two new instance methods on that class:title(returns@title) andtitle=(assigns to@titleand returns the assigned value). - When you later call
ClassName.new(...), Ruby allocates a new, empty object and callsinitializeon it. Whatever instance variablesinitializeassigns become that object’s state. - When you call
book.title, Ruby performs a normal method lookup: it searches the object’s class (and its ancestor chain) for a method namedtitle, finds the oneattr_accessorgenerated, and runs it — it returns@titlefrom that specific object. - When you call
book.title = "new value", Ruby is really calling the methodtitle=with one argument. The trailing=is part of the method name;book.title = xis syntactic sugar forbook.title=(x). - Because getters and setters are ordinary methods, you can always override what
attr_accessorgenerated by defining your own method with the same name later in the class body — the later definition wins.
Common Mistakes
Mistake 1: Forgetting the @ sigil
Leaving off the @ silently creates a local variable instead of setting instance state — there is no error, just a bug:
class Person
attr_reader :name
def initialize(name)
name = name # bug: reassigns the local parameter, never touches @name
end
end
person = Person.new("Ana")
puts person.name.inspect
nil
name = name is valid Ruby — it just assigns the local parameter name to itself and does nothing useful. @name is never set, so it defaults to nil, and person.name returns nil. The fix is to assign the instance variable explicitly:
class Person
attr_reader :name
def initialize(name)
@name = name
end
end
person = Person.new("Ana")
puts person.name.inspect
"Ana"
Mistake 2: Assuming attr_accessor returns a safe copy
A reader generated by attr_accessor or attr_reader returns the actual object stored in the instance variable, not a duplicate. If that object is mutable (like an Array or Hash), any code holding the returned reference can change the object’s internal state directly:
class ShoppingCart
attr_accessor :items
def initialize
@items = []
end
end
cart = ShoppingCart.new
cart.items << "Book"
list = cart.items
list << "Pen"
puts cart.items.inspect
["Book", "Pen"]
cart.items hands back the exact same array object stored in @items, so pushing onto list also changes cart's internal array — there was never a copy. A more defensive design exposes a read-only accessor and a controlled mutation method instead of the raw writer:
class ShoppingCart
attr_reader :items
def initialize
@items = []
end
def add_item(item)
@items << item
end
end
cart = ShoppingCart.new
cart.add_item("Book")
cart.add_item("Pen")
puts cart.items.inspect
["Book", "Pen"]
Dropping attr_accessor for attr_reader at least prevents cart.items = something_else from replacing the whole array from outside, and routing additions through add_item keeps the mutation logic in one place. (Note that cart.items is still the live array underneath, so truly locking it down further would mean returning @items.dup from a custom reader.)
Best Practices
- Use
attr_accessorfor simple, freely-settable state with no validation rules — configuration-style fields where any value the right type accepts is fine. - Use
attr_readeralone when outside code needs to see a value but should never set it directly (anid, a computed total, anisbn). - When a value has rules about how it changes (must stay positive, must go through a state machine), skip
attr_accessorentirely and write a named method likedepositorrenameinstead of a bare setter. - Prefer keyword arguments in
initializeonce you have more than one or two parameters —BankAccount.new("Priya", balance: 100)is far more readable than a long list of positional values. - Remember instance variables read as
nilbefore they are assigned — don't rely on this as a substitute for a real default; set defaults explicitly ininitialize. - Never expose a mutable collection with
attr_accessorif you need to control what gets added to it — useattr_readerplus dedicated methods, as shown in the shopping cart fix above.
Practice Exercises
- Write a
Temperatureclass with an instance variable@celsiusset ininitialize. Give it anattr_readerforcelsiusand a methodfahrenheitthat computes and returns the Fahrenheit equivalent (celsius * 9.0 / 5 + 32) without storing it in its own instance variable. - Write a
Counterclass with@countstarting at 0, anattr_reader :count, and anincrementmethod that adds 1 and returns the new count. Create two separateCounterinstances, increment one three times and the other once, and print both counts to prove they are independent. - Take the
ShoppingCartexample from Mistake 2 and add atotal_itemsmethod that returns@items.length, without adding anattr_accessorforitems. What would go wrong if you usedattr_accessorinstead ofattr_readerhere?
Summary
- Instance variables (
@name) hold per-object state and are private to the object — there is no dot syntax to reach them from outside. - An unset instance variable reads as
nilrather than raising an error, consistent with Ruby's rule that onlynilandfalseare falsy. attr_accessor :xgenerates both anxgetter and anx=setter;attr_readerandattr_writergenerate just one side.- Getters and setters are ordinary methods under the hood, found through normal method lookup, and can be overridden by defining your own method of the same name.
- A reader returns the actual referenced object, not a copy — mutating what it returns mutates the original's state, which is a common source of accidental coupling.
- Reach for
attr_readerplus explicit methods, instead ofattr_accessor, whenever a value needs validation or controlled mutation.
