Instance Variables and attr_accessor

Published by Programming Line in Ruby. Last updated .

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, :isbn generates six methods in one line.
  • The symbol name must match the instance variable name (without the @): attr_reader :balance exposes @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) and title= (assigns to @title and returns the assigned value).
  • When you later call ClassName.new(...), Ruby allocates a new, empty object and calls initialize on it. Whatever instance variables initialize assigns 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 named title, finds the one attr_accessor generated, and runs it — it returns @title from that specific object.
  • When you call book.title = "new value", Ruby is really calling the method title= with one argument. The trailing = is part of the method name; book.title = x is syntactic sugar for book.title=(x).
  • Because getters and setters are ordinary methods, you can always override what attr_accessor generated 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_accessor for simple, freely-settable state with no validation rules — configuration-style fields where any value the right type accepts is fine.
  • Use attr_reader alone when outside code needs to see a value but should never set it directly (an id, a computed total, an isbn).
  • When a value has rules about how it changes (must stay positive, must go through a state machine), skip attr_accessor entirely and write a named method like deposit or rename instead of a bare setter.
  • Prefer keyword arguments in initialize once 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 nil before they are assigned — don't rely on this as a substitute for a real default; set defaults explicitly in initialize.
  • Never expose a mutable collection with attr_accessor if you need to control what gets added to it — use attr_reader plus dedicated methods, as shown in the shopping cart fix above.

Practice Exercises

  • Write a Temperature class with an instance variable @celsius set in initialize. Give it an attr_reader for celsius and a method fahrenheit that computes and returns the Fahrenheit equivalent (celsius * 9.0 / 5 + 32) without storing it in its own instance variable.
  • Write a Counter class with @count starting at 0, an attr_reader :count, and an increment method that adds 1 and returns the new count. Create two separate Counter instances, increment one three times and the other once, and print both counts to prove they are independent.
  • Take the ShoppingCart example from Mistake 2 and add a total_items method that returns @items.length, without adding an attr_accessor for items. What would go wrong if you used attr_accessor instead of attr_reader here?

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 nil rather than raising an error, consistent with Ruby's rule that only nil and false are falsy.
  • attr_accessor :x generates both an x getter and an x= setter; attr_reader and attr_writer generate 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_reader plus explicit methods, instead of attr_accessor, whenever a value needs validation or controlled mutation.