Attributes (attr_accessor)
Reviewed & published by Brayan K
Ruby is a dynamic, beginner-friendly programming language, and attributes are how an object exposes its data — attr_accessor generates the getter and setter methods so you don't write them by hand.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll understand getters and setters, use attr_reader/writer/accessor, and write custom accessors with validation.
What You'll Learn in This Lesson
- Why instance variables are private
- Writing getters and setters by hand
- Generating them with attr_accessor
- attr_reader for read-only attributes
- attr_writer for write-only attributes
- Custom setters with validation
1️⃣ The Manual Way: Getters and Setters
Instance variables are private to the object. To expose one, you write a getter (a method that returns it) and a setter (a method named with a trailing = that assigns it). Seeing this by hand makes the shortcut obvious.
# Instance variables are PRIVATE. To read or change them from outside,
# you normally write getter and setter methods by hand:
class PersonManual
def initialize(name)
@name = name
end
def name # getter: returns the value
@name
end
def name=(value) # setter: the = makes it assignable
@name = value
end
end
p1 = PersonManual.new("Ada")
puts p1.name # -> Ada (calls the getter)
p1.name = "Grace" # calls the setter method name=
puts p1.name # -> Grace2️⃣ attr_accessor, attr_reader, attr_writer
attr_accessor :name generates both the getter and the setter automatically. Use attr_reader for read-only data (a getter only) and attr_writer for write-only. You list the attributes as symbols.
# attr_accessor writes those getter AND setter methods for you.
class Person
attr_accessor :name, :age # read + write for both
attr_reader :id # read-only (getter only)
def initialize(name, age, id)
@name = name
@age = age
@id = id
end
end
p = Person.new("Ada", 36, 1001)
puts p.name # -> Ada (getter generated by attr_accessor)
p.age = 37 # setter generated by attr_accessor
puts p.age # -> 37
puts p.id # -> 1001 (attr_reader: readable)
# p.id = 2 # would raise NoMethodError — no setter exists
# attr_writer would give a setter only (write-only), e.g. attr_writer :passwordYour turn. Add the right accessors to a Product class so its name is editable but its SKU is read-only. Complete the TODOs, then run it.
# 🎯 YOUR TURN — replace each TODO, then run with: ruby product.rb
class Product
# 1) Make :name readable AND writable, and :sku read-only
attr_accessor :name, :price # 👉 TODO: name and price read/write
attr_reader :sku # 👉 TODO: sku read-only
def initialize(name, price, sku)
@name = name
@price = price
@sku = sku
end
def discount(percent)
@price = (@price * (1 - percent / 100.0)).round(2)
end
end
item = Product.new("Mug", 12.0, "MUG-01")
item.name = "Travel Mug" # uses the generated setter
item.discount(25) # apply 25% off
puts "#{item.name} (#{item.sku}): $#{item.price}"
# ✅ Expected output:
# Travel Mug (MUG-01): $9.0Mini-Challenge: A Guarded Setter
Write a custom temperature= setter that validates its input — the real payoff of writing accessors yourself. Run with ruby thermostat.rb.
# 🎯 MINI-CHALLENGE: a Thermostat with a guarded setter.
# Save as thermostat.rb and run with: ruby thermostat.rb
#
# 1. Define a class Thermostat with attr_reader :temperature.
# 2. In initialize, set @temperature to a starting value (e.g. 20).
# 3. Write your OWN setter "temperature=" that refuses values below 0,
# keeping the old value and printing "Too cold!" instead.
# 4. Test it: set a valid value, then try -5, and print the temperature.
#
# ✅ Example output:
# Too cold!
# Temperature: 22
# your code hereCommon Errors (and the fix)
- "undefined method 'name='" — You used attr_reader (read-only) but tried to assign. Use attr_accessor.
- Passing strings instead of symbols — It's attr_accessor :name, with a leading colon.
- Setter not firing inside the class — Inside a method, write self.name = x; a bare name = x creates a local variable.
- Forgetting to set @ in initialize — Accessors expose @name; you still must assign it somewhere (usually initialize).
- Custom setter shadowed by attr_accessor — Don't declare the same attribute in attr_accessor if you hand-write its setter; use attr_reader plus your setter.
Pro Tips
- 💡 Least access first: default to attr_reader; only add a setter when callers truly need to change the value.
- 💡 Validate in setters: a custom value= is the perfect place to reject bad input.
- 💡 Group declarations: keep your attr_* lines at the top of the class for readability.
📋 Quick Reference — Attributes
| Concept | Ruby Syntax |
|---|---|
| Read + write | attr_accessor :name |
| Read only | attr_reader :id |
| Write only | attr_writer :pw |
| Custom getter | def name; @name; end |
| Custom setter | def name=(v) |
| Set inside class | self.name = v |
🎉 Lesson Complete!
- ✅ Instance variables are private; getters/setters expose them
- ✅ attr_accessor generates both for you
- ✅ attr_reader = read-only; attr_writer = write-only
- ✅ Setters are methods named with a trailing =
- ✅ Hand-write accessors when you need validation or computation
Practice quiz
What does attr_accessor :name generate?
- Only a getter named name
- Only a setter named name=
- Both a getter (name) and a setter (name=)
- An instance variable named name
Answer: Both a getter (name) and a setter (name=). attr_accessor writes both the getter and the setter for you at class-definition time.
Which macro creates a READ-ONLY attribute (getter only)?
- attr_reader
- attr_writer
- attr_accessor
- attr_const
Answer: attr_reader. attr_reader generates just the getter, so the value can be read but not written from outside.
If a class uses attr_reader :id, what happens when you call obj.id = 5?
- It silently does nothing
- It sets @id to 5
- It creates a new attribute
- It raises NoMethodError because no setter exists
Answer: It raises NoMethodError because no setter exists. attr_reader makes no setter, so assigning raises NoMethodError (undefined method 'id=').
How is a setter method named in Ruby?
- set_name
- name= (with a trailing equals sign)
- name!
- @name
Answer: name= (with a trailing equals sign). Ruby routes obj.name = x to the method name=(x); the trailing = is what makes it a setter.
You must pass attributes to attr_accessor as:
- Symbols, like :name
- Strings, like "name"
- Instance variables, like @name
- Local variables
Answer: Symbols, like :name. Accessor macros take symbols: attr_accessor :name, :age.
Inside an instance method, how do you correctly call the generated setter for name?
- name = x
- @name = x only
- self.name = x
- set name x
Answer: self.name = x. A bare name = x makes a local variable; self.name = x routes through the setter method.
attr_writer :password generates which method(s)?
- A getter only
- A setter only (write-only)
- Both getter and setter
- Neither
Answer: A setter only (write-only). attr_writer makes only the setter, useful for write-only values you don't want to expose.
Why would you hand-write a setter instead of using attr_accessor?
- It runs faster
- attr_accessor is deprecated
- Hand-written setters are required by Ruby
- To add validation or side effects when the value is set
Answer: To add validation or side effects when the value is set. A custom value= setter lets you validate or transform input — the payoff of writing it yourself.
Instance variables (the @-prefixed ones) are, by default:
- Public and readable from anywhere
- Private to the object — exposed only through methods
- Shared across all instances
- Automatically frozen
Answer: Private to the object — exposed only through methods. Instance variables are private; getters and setters are how you expose them to the outside.
Best practice for choosing accessors is usually to:
- Always use attr_accessor
- Never use accessor macros
- Start with the least access (e.g. attr_reader) and widen only when needed
- Use attr_writer for everything
Answer: Start with the least access (e.g. attr_reader) and widen only when needed. Default to read-only and add a setter only when callers truly need to change the value.
Continue this course
- Previous: Classes & Objects
- Next: Modules & Mixins — Share behaviour across classes and namespace your code
- Quick reference: Ruby cheat sheet › Classes & Modules
Frequently asked questions
What does attr_accessor actually do?
attr_accessor is a shortcut that writes getter and setter methods for you at the moment the class is defined. attr_accessor :name generates a name method that returns @name and a name= method that assigns it, so you never write that boilerplate by hand. It's not magic syntax — it's a regular method (a class macro) that defines other methods. attr_reader generates only the getter, and attr_writer only the setter.
When should I use attr_reader, attr_writer, or attr_accessor?
Choose based on the access you want to allow. Use attr_reader for values that should be readable but never changed from outside, like an id or a creation timestamp. Use attr_writer (rarely) for write-only values like a password you set but don't expose. Use attr_accessor when callers may both read and update the value, like a person's name. Starting with the least access and widening only when needed keeps your objects well-encapsulated.
Why is the setter method written with an = in its name?
Ruby has special syntax support for method names ending in =. When you write person.name = "Bo", Ruby actually calls the method name=("Bo"). That's why a setter is defined as def name=(value). This is how Ruby makes attribute assignment look like simple variable assignment while still routing through a method — which means you can add validation or side effects inside your own custom setter.
Can I write my own getter or setter instead of using attr_accessor?
Absolutely, and you should whenever you need logic. attr_accessor only gives you a plain pass-through. If you want a setter that validates input (rejecting a negative temperature, say) or a getter that computes or formats a value, write the method yourself — def temperature=(value); ... end. You can even mix the two: use attr_reader for the getter and hand-write just the setter. Custom accessors are where encapsulation earns its keep.