Instance variables are private¶
Level: 101 · a Python programmer meeting @name for the first time
One line: An @variable belongs to one object and can be read or written only by that object's own methods — from outside there is nothing to call until the class defines a reader or a writer.
In Ruby, @owner is not an attribute of the object in the Python sense. It is a slot that only code running as that object (self) can name; acct.owner is a method call, and if the class has not defined a method called owner the call fails with NoMethodError, exactly as any other unknown method would. attr_reader :owner writes that one-line method for you, attr_writer writes owner=, and attr_accessor writes both. The privacy is not a lock: instance_variable_get(:@owner) reads any slot from outside, instance_variable_set writes one, and instance_variables lists them — but you have to ask for the back door by name, so an accidental read cannot happen. Two more facts fall out of the same design: an @variable that was never assigned reads as nil rather than raising, and any object accepts a new @variable at any moment, because a slot is created by its first assignment. The program below builds an Account and adds the readers one at a time.
Verified output of instance_variables_are_private_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
1. acct.owner with no reader defined NoMethodError: undefined method 'owner' for an instance of Account
2. after attr_reader :owner, acct.owner "ann"
3. after attr_accessor :balance, acct.balance = 10 10
4. acct.owner = "bob" with no writer NoMethodError: undefined method 'owner=' for an instance of Account
5. the back door: instance_variable_get(:@owner) "ann"
6. acct.instance_variables [:@owner, :@balance]
7. unset @nickname read inside a method nil (instance_variable_defined? false)
8. @__secret is stored under its own name :@__secret
9. a new ivar added after the fact true (no way to forbid it)
10. two instances, two sets of ivars ann / bob (balance of other: nil)
Reading the output¶
Row 1 is the claim: @owner is set, but acct.owner is a method call and no such method exists yet, so Ruby reports undefined method 'owner'. Rows 2 and 3 reopen the class and add attr_reader :owner and attr_accessor :balance; the same calls now answer. Row 4 shows the other half — with a reader but no writer, acct.owner = "bob" is a call to a method named owner=, which does not exist either.
Rows 5 and 6 are the back door: instance_variable_get(:@owner) reads the slot without a reader, and instance_variables lists the slots in the order they were first assigned. Row 7 reads @nickname, which no method ever assigned: the value is nil, no warning, no error, and instance_variable_defined? says false. (Ruby 2.x printed a warning for this under -w; 3.0 removed it.) (Not machine-checked here.)
Row 8 is for the reader coming from Python: a leading double underscore means nothing to Ruby — @__secret is stored under its own name. Row 9 shows that a new slot can be added after construction, and that there is no __slots__-style way to forbid it. Row 10 confirms that each instance has its own set: other was built after the class gained balance, but its @balance was never assigned, so it reads nil.
Compared with Python¶
Python attributes are public: the twin's row 1 reads acct._owner from outside and gets 'ann'. The leading underscore is a convention that tools and readers respect, not something the interpreter enforces. The reader Python adds when it wants control is @property (row 2), and assigning to a property that has no setter is an AttributeError (row 4). No declaration is needed for a plain attribute (row 3), and the back door is simply the instance dictionary, vars(acct) (rows 5 and 6). The two languages diverge on an unset name: Python raises AttributeError where Ruby returns nil (row 7). Python does have a mechanism that looks like privacy: a name with two leading underscores is mangled inside a class body, so self.__secret is stored as _Account__secret (row 8) — still reachable, just hard to hit by accident. And where Ruby can never forbid a new slot, a class with __slots__ refuses one (row 9). The twin prints exception types only, because CPython rewords the messages between releases.
Verified output of instance_variables_are_private_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. acct._owner with no reader defined 'ann'
2. the reader: @property owner, acct.owner 'ann'
3. no declaration needed, acct.balance = 10 10
4. acct.owner = "bob" on a property with no setter AttributeError
5. the back door: vars(acct)["_owner"] 'ann'
6. list(vars(acct)) ['_owner', 'balance']
7. unset acct.nickname read from outside AttributeError (hasattr False)
8. self.__secret is stored under a mangled name '_Account__secret'
9. a new attribute added after the fact True on Account; AttributeError on a __slots__ class
10. two instances, two sets of attributes ann / bob (balance of other: AttributeError)
The Python tutorial's own words on this are under Private Variables ↗; @property is documented at property ↗.
Try it¶
- Write a
Temperatureclass that stores@celsiuswith a reader, a computedfahrenheitreader, and acelsius=writer that raisesArgumentErrorfor a value below absolute zero. Printinstance_variables, then show that the writer rejects-300and thatfahrenheit=does not exist. - Reimplement
attr_readeras a class methodmy_reader(*names)usingdefine_methodandinstance_variable_get, and check withinstance_methods(false)that it defines the same methods the real one does. (define_methodhas the tools.)
Solution to 1
instance_variables_are_private_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.
# Exercise 1: a Temperature that keeps @celsius private, exposes celsius and a
# computed fahrenheit reader, and refuses a value below absolute zero.
class Temperature
ABSOLUTE_ZERO = -273.15
attr_reader :celsius
def initialize(celsius)
self.celsius = celsius # go through the writer so the check runs
end
def celsius=(value)
raise ArgumentError, "#{value} is below absolute zero" if value < ABSOLUTE_ZERO
@celsius = value
end
def fahrenheit = @celsius * 9.0 / 5 + 32
def inspect = "#<Temperature #{@celsius} C>"
end
t = Temperature.new(25)
puts "celsius: #{t.celsius}"
puts "fahrenheit: #{t.fahrenheit}"
puts "instance_variables: #{t.instance_variables.inspect}"
t.celsius = 100
puts "after t.celsius = 100: #{t.fahrenheit}"
begin
t.celsius = -300
rescue ArgumentError => e
puts "t.celsius = -300: #{e.class}: #{e.message}"
end
puts "still: #{t.inspect}"
begin
t.fahrenheit = 0
rescue NoMethodError => e
puts "t.fahrenheit = 0: #{e.class} (no writer was defined)"
end
Verified output of instance_variables_are_private_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.
See also¶
privatemeans "no receiver" — the rule for methods, which is a different rule from the one for@variables- Class variables are shared —
@@countand a class-level@count, the two other kinds of variable that start with@ - Setters return the argument — what
acct.balance = 10evaluates to, and whyself.balance =is needed inside the class - Introspection —
instance_variables,instance_variable_getand the rest of the back door, in one place to_s,inspectandp— why the program definesinspect(the default one prints an address)repris notstr↗ — the Python side of the same__repr__the twin defines- Modules and visibility ↗ — Rust draws the privacy line around a module, not around an object
selfis the implicit receiver — Every Ruby call has a receiver, and when you write none it isselfThread.current[:x]is fiber-local, not thread-local —Thread.current[:x]is scoped to the fiber that set it- Twenty-three Ruby traps for a Python programmer — The twenty-three places where a Python reflex gives the wrong answer in Ruby
- There is more than one way, and one obvious way — Ruby's idioms