Skip to main content
⚡ Calmops

attr_accessor, attr_reader, and attr_writer in Ruby

Introduction

In Ruby, instance variables (@name, @age) are private by default — they can’t be accessed from outside the class. To expose them, you need getter and setter methods. Ruby provides attr_reader, attr_writer, and attr_accessor as shortcuts to generate these methods automatically.

The Problem They Solve

Without accessors, you’d write getter and setter methods manually:

class Person
  def initialize(name, age)
    @name = name
    @age  = age
  end

  # Getter
  def name
    @name
  end

  # Setter
  def name=(value)
    @name = value
  end

  # Getter
  def age
    @age
  end

  # Setter
  def age=(value)
    @age = value
  end
end

That’s a lot of boilerplate. The accessor macros replace all of it.

attr_reader: Read-Only Access

attr_reader generates only a getter method:

class BankAccount
  attr_reader :balance, :account_number

  def initialize(account_number, initial_balance)
    @account_number = account_number
    @balance        = initial_balance
  end

  def deposit(amount)
    @balance += amount
  end
end

account = BankAccount.new("ACC-001", 1000)
puts account.balance         # => 1000
puts account.account_number  # => "ACC-001"

account.balance = 2000  # => NoMethodError: undefined method 'balance='

Use attr_reader when you want to expose a value but prevent external modification.

attr_writer: Write-Only Access

attr_writer generates only a setter method:

class Config
  attr_writer :debug_mode
  attr_writer :log_level

  def initialize
    @debug_mode = false
    @log_level  = :info
  end

  def status
    "debug=#{@debug_mode}, level=#{@log_level}"
  end
end

config = Config.new
config.debug_mode = true
config.log_level  = :debug
puts config.status  # => "debug=true, level=debug"

config.debug_mode  # => NoMethodError: undefined method 'debug_mode'

attr_writer alone is rarely used — usually you want to read the value too.

attr_accessor: Read-Write Access

attr_accessor generates both getter and setter:

class Person
  attr_accessor :name, :email, :age

  def initialize(name, email, age)
    @name  = name
    @email = email
    @age   = age
  end

  def introduce
    "Hi, I'm #{name}, #{age} years old. Email: #{email}"
  end
end

person = Person.new("Alice", "[email protected]", 30)
puts person.name          # => "Alice"
puts person.introduce     # => "Hi, I'm Alice, 30 years old. Email: [email protected]"

person.name = "Alicia"
person.age  = 31
puts person.introduce     # => "Hi, I'm Alicia, 31 years old. Email: [email protected]"

What They Generate

These macros generate real Ruby methods. attr_accessor :name is equivalent to:

def name
  @name
end

def name=(value)
  @name = value
end

You can verify this:

class Dog
  attr_accessor :name
end

puts Dog.instance_methods(false).sort.inspect
# => [:name, :name=]

Mixing Accessor Types

Use different accessors for different attributes based on your encapsulation needs:

class User
  attr_accessor :name, :email      # read-write: users can update these
  attr_reader   :id, :created_at   # read-only: set once, never changed externally
  attr_writer   :password          # write-only: can set but never read back

  def initialize(id, name, email, password)
    @id         = id
    @name       = name
    @email      = email
    @created_at = Time.now
    self.password = password  # uses the setter (could add hashing here)
  end

  def authenticate(plain_password)
    BCrypt::Password.new(@password) == plain_password
  end
end

user = User.new(1, "Alice", "[email protected]", "secret123")
puts user.id         # => 1
puts user.name       # => "Alice"
user.name = "Alicia" # OK
user.id   = 99       # => NoMethodError
user.password        # => NoMethodError (write-only)

Custom Accessors with Validation

When you need validation or transformation, define the method manually instead of using the macro:

class Product
  attr_reader :name, :price

  def name=(value)
    raise ArgumentError, "Name can't be blank" if value.to_s.strip.empty?
    @name = value.strip
  end

  def price=(value)
    raise ArgumentError, "Price must be positive" unless value.to_f > 0
    @price = value.to_f.round(2)
  end
end

p = Product.new
p.name  = "  Widget  "
p.price = "19.999"

puts p.name   # => "Widget"   (stripped)
puts p.price  # => 20.0       (rounded)

p.name  = ""  # => ArgumentError: Name can't be blank
p.price = -5  # => ArgumentError: Price must be positive

Accessor with Callbacks

You can override the generated setter to add side effects:

class Observable
  attr_reader :value

  def initialize(value)
    @value     = value
    @callbacks = []
  end

  def value=(new_value)
    old_value = @value
    @value    = new_value
    @callbacks.each { |cb| cb.call(old_value, new_value) }
  end

  def on_change(&block)
    @callbacks << block
  end
end

counter = Observable.new(0)
counter.on_change { |old, new| puts "Changed: #{old}#{new}" }

counter.value = 5   # => Changed: 0 → 5
counter.value = 10  # => Changed: 5 → 10

Protected and Private Accessors

You can restrict accessor visibility:

class Employee
  attr_accessor :name

  def higher_paid_than?(other)
    salary > other.salary  # can call protected method on another Employee
  end

  protected
  attr_reader :salary

  private
  attr_writer :salary

  public
  def initialize(name, salary)
    @name   = name
    @salary = salary
  end
end

alice = Employee.new("Alice", 80_000)
bob   = Employee.new("Bob",   70_000)

puts alice.higher_paid_than?(bob)  # => true
puts alice.salary                  # => NoMethodError: protected method

Struct as an Alternative

For simple data containers, Struct auto-generates accessors and more:

# Equivalent to a class with attr_accessor for each field
Point = Struct.new(:x, :y) do
  def distance_to(other)
    Math.sqrt((x - other.x)**2 + (y - other.y)**2)
  end
end

p1 = Point.new(0, 0)
p2 = Point.new(3, 4)

puts p1.x                    # => 0
puts p1.distance_to(p2)      # => 5.0
puts p2.to_h.inspect         # => {:x=>3, :y=>4}

Quick Reference

Macro Generates Access
attr_reader :x def x; @x; end Read only
attr_writer :x def x=(v); @x=v; end Write only
attr_accessor :x Both getter and setter Read + Write

Resources

Comments