Question
In Ruby, instance variables can be exposed using helper methods such as:
attr_accessor :var
attr_reader :var
attr_writer :var
Why would you choose attr_reader or attr_writer instead of simply using attr_accessor everywhere?
Is there any practical reason, such as performance, or are these alternatives mainly provided for design and code-organization purposes?
Short Answer
By the end of this page, you will understand what attr_reader, attr_writer, and attr_accessor do in Ruby, why they exist as separate tools, and how they help control access to an object's internal state. You will also see when to expose read-only, write-only, or read-write behavior in real Ruby code.
Concept
In Ruby, instance variables like @name belong to a specific object. By default, code outside the object cannot directly read or write those variables using regular method calls. Ruby provides attribute helpers to define methods for controlled access:
attr_readercreates a getter methodattr_writercreates a setter methodattr_accessorcreates both getter and setter methods
For example:
class User
attr_reader :name
attr_writer :password
attr_accessor :email
end
This is roughly equivalent to writing:
class User
def name
@name
end
def password=(value)
@password = value
end
def email
@email
end
def email=(value)
@email = value
end
end
The reason these helpers exist separately is not mainly performance. The main reason is encapsulation.
Encapsulation means an object controls how other code interacts with its internal data. Not every value should be freely readable and writable.
Why this matters
If you always use attr_accessor, every exposed attribute becomes both readable and changeable from outside the object. That may:
- allow invalid state changes
- reveal data that should stay private
- make debugging harder because any code can modify the object
- weaken the design of your class
Choosing between reader, writer, and accessor is a way of expressing intent:
attr_reader: others may see this value, but not change it directlyattr_writer: others may set this value, but not read it directlyattr_accessor: others may both read and change it
This makes your class API more deliberate and safer.
Mental Model
Think of an object like a building with doors.
attr_readergives people a window to look throughattr_writergives people a mail slot to put something inattr_accessorgives people both a window and a door
If every room had an open door, anyone could walk in and rearrange everything. That is convenient, but unsafe. Good class design means deciding which openings should exist.
Another way to think about it:
- A reader says, "You may inspect this"
- A writer says, "You may provide this"
- An accessor says, "You may inspect and change this"
The choice is about control, not just convenience.
Syntax and Examples
Basic syntax
class Person
attr_reader :name
attr_writer :password
attr_accessor :email
end
This creates these methods:
namepassword=emailemail=
Example: read-only attribute
class Product
attr_reader :id
def initialize(id)
@id = id
end
end
product = Product.new(101)
puts product.id
# 101
product.id = 200
# NoMethodError
Why use this?
An ID often should not change after an object is created. attr_reader lets other code view it without modifying it.
Example: write-only attribute
class Account
attr_writer :password
end
account = Account.new
account.password = "secret123"
account.password
# NoMethodError
Why use this?
A password may be something you want to set, but not expose for reading.
Example: read and write attribute
class Profile
attr_accessor :nickname
end
profile = Profile.new
profile.nickname = "ruby_dev"
puts profile.nickname
# ruby_dev
Why use this?
For values that are safe and expected to change freely, attr_accessor is fine.
Multiple attributes
class User
attr_reader :id, :created_at
attr_accessor :name, :email
attr_writer :password
end
This is common in real classes: different attributes have different access levels.
Step by Step Execution
Consider this class:
class Book
attr_reader :title
attr_accessor :price
def initialize(title, price)
@title = title
@price = price
end
end
book = Book.new("Ruby Basics", 20)
puts book.title
puts book.price
book.price = 25
puts book.price
Step-by-step
-
Ruby reads
attr_reader :title.- It creates a method called
title. - That method returns
@title.
- It creates a method called
-
Ruby reads
attr_accessor :price.- It creates a getter method
price. - It also creates a setter method
price=.
- It creates a getter method
-
Book.new("Ruby Basics", 20)runsinitialize.@titlebecomes"Ruby Basics"@pricebecomes20
-
book.titlecalls the generated reader.- It returns
@title - Output:
Ruby Basics
- It returns
-
book.pricecalls the generated getter.- It returns
@price - Output:
20
- It returns
-
book.price = 25calls the generated setter.- It updates
@priceto25
- It updates
-
book.priceis called again.- It now returns
25
- It now returns
Important detail
book.title = "New Title" would fail because only a reader was created for title, not a writer.
Real World Use Cases
1. Read-only IDs and timestamps
In many applications, values such as these should be visible but not directly changed:
- database IDs
- creation timestamps
- generated tokens
class Order
attr_reader :id, :created_at
end
2. Sensitive write-only input
Some values are set from outside but should not be readable later in plain form:
class User
attr_writer :password
end
In real apps, the writer often hashes the password instead of storing raw text.
3. Editable profile fields
Some fields are naturally both readable and writable:
class Profile
attr_accessor :display_name, :bio
end
4. Configuration objects
A class may expose certain settings for modification while keeping internal derived values read-only.
class AppConfig
attr_accessor :host, :port
attr_reader :connection_url
end
5. Domain rules
A business object may allow reading a value but only allow changes through special methods.
class BankAccount
attr_reader :balance
def deposit(amount)
@balance += amount
end
end
Here, balance should not be freely writable with balance = -1000.
Real Codebase Usage
In real Ruby codebases, developers usually avoid exposing everything with attr_accessor unless it is truly appropriate.
Common patterns
Guarding important state
class Invoice
attr_reader :status
def mark_paid
@status = :paid
end
end
This prevents outside code from doing:
invoice.status = :paid
Instead, the class controls how status changes happen.
Validation in custom setters
Sometimes developers start with attr_accessor, then replace it with a custom writer.
class Person
attr_reader :age
def age=(value)
raise ArgumentError, "age must be non-negative" if value < 0
@age = value
end
end
This keeps write access but adds rules.
Read-only public API, private internal updates
class Session
attr_reader :token
def refresh_token
@token = generate_token
end
private
def generate_token
"abc123"
end
end
Other code can read token, but only the class itself changes it.
Narrow public interface
Well-designed classes expose only what other parts of the system actually need. This makes the code:
- easier to understand
- safer to refactor
- less likely to be misused
Intention-revealing APIs
Using attr_reader instead of attr_accessor communicates that a value is not meant to be changed casually. That message helps teammates understand the class design quickly.
Common Mistakes
Using attr_accessor for everything
This is the most common mistake.
class User
attr_accessor :admin, :id, :created_at
end
This allows outside code to do things like:
user.admin = true
user.id = 999
That may be dangerous or invalid.
Better
class User
attr_reader :id, :created_at, :admin
end
Then provide controlled methods if needed.
Thinking these expose variables directly
These helpers create methods, not direct variable access syntax.
class Item
attr_accessor :name
end
This defines:
namename=
It does not make @name public.
Calling setters without self inside a class
Inside instance methods, this is a common bug:
class Person
attr_accessor :name
def rename(new_name)
name = new_name
end
end
This creates a local variable name instead of calling the setter.
Correct version
class Person
attr_accessor :name
def rename(new_name)
self.name = new_name
end
end
Using attr_writer without a real reason
Write-only attributes are less common. If code needs to set a value, ask whether it also needs to read it later. Use attr_writer only when hiding reads is intentional.
Forgetting that custom methods can replace helpers
If an attribute needs validation or transformation, you do not have to use the helper.
class Temperature
def celsius=(value)
raise ArgumentError, "too low" if value < -273.15
@celsius = value
end
def celsius
@celsius
end
end
Attribute helpers are convenience tools, not mandatory patterns.
Comparisons
| Helper | Creates getter? | Creates setter? | Typical use |
|---|---|---|---|
attr_reader | Yes | No | Read-only public data |
attr_writer | No | Yes | Write-only input |
attr_accessor | Yes | Yes | Fully readable and writable data |
attr_reader vs attr_accessor
- Use
attr_readerwhen a value should be visible but not freely changed. - Use
attr_accessorwhen external code should both inspect and update it.
attr_writer vs custom setter method
- Use
attr_writerfor a simple plain assignment. - Use a custom setter when you need validation, formatting, logging, or side effects.
class User
def email=(value)
raise ArgumentError, "invalid email" unless value.include?("@")
@email = value.downcase
end
end
Attribute helpers vs explicit methods
| Approach | Best for |
|---|---|
| Attribute helper | Simple getter/setter generation |
| Explicit method definitions | Validation, business rules, custom behavior |
The main choice is not speed. It is how much control your class should have over its public interface.
Cheat Sheet
attr_reader :name # creates name
attr_writer :name # creates name=
attr_accessor :name # creates name and name=
What each one does
attr_reader→ read onlyattr_writer→ write onlyattr_accessor→ read and write
Use these rules
- Use
attr_readerfor values others may inspect but should not modify directly. - Use
attr_writerfor values that should be set but not exposed. - Use
attr_accessoronly when both reading and writing are truly appropriate.
Common example
class User
attr_reader :id
attr_accessor :name
attr_writer :password
end
Important setter rule inside instance methods
self.name = "Alice" # calls setter
name = "Alice" # local variable assignment
When to avoid plain helpers
Write custom methods instead of helpers when you need:
- validation
- formatting
- conversion
- logging
- business rules
Key idea
These helpers define methods, not public instance variables.
FAQ
Why not just use attr_accessor for every attribute?
Because it exposes both reading and writing. That can make your objects too easy to misuse and can break encapsulation.
Is there a performance difference between attr_reader, attr_writer, and attr_accessor?
In normal Ruby code, performance is not the reason to choose between them. The choice is mainly about API design and access control.
When should I use attr_reader in Ruby?
Use it when outside code should be able to see a value but should not directly change it, such as IDs, timestamps, or computed state.
When should I use attr_writer in Ruby?
Use it when a value should be set from outside but should not be readable directly, such as sensitive input like a password.
Can I replace attr_accessor with custom methods later?
Yes. This is very common when you later need validation or business logic.
Does attr_accessor make instance variables public?
No. It creates public getter and setter methods. The instance variables themselves are still internal to the object.
Why does name = "Alice" sometimes not call the setter?
Inside an instance method, Ruby treats that as local variable assignment unless you write self.name = "Alice".
Can I use more than one attribute at once?
Yes.
attr_reader :id, :created_at
attr_accessor :name, :email
Mini Project
Description
Build a small BankAccount class that demonstrates why attr_reader is often safer than attr_accessor. In a real banking system, code should be able to read the balance, but it should not be able to set the balance directly to any arbitrary number. Instead, updates should happen through controlled methods like deposit and withdraw.
Goal
Create a Ruby class where the account balance can be read publicly, but changed only through methods that enforce simple rules.
Requirements
- Create a
BankAccountclass with a readablebalance - Initialize the account with a starting balance
- Add a
depositmethod that increases the balance - Add a
withdrawmethod that decreases the balance only if enough money exists - Prevent direct external assignment to
balance
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.