---
title: 'Class Definition and Instance Variables'
source: 'https://academia.sh/en/courses/object-orientation-and-metaprogramming/class-definition-and-instance-variables'
course: 'Object Orientation and Metaprogramming'
language: en
updated: '2026-10-21T05:03:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Classify the author of a method as programmer, ancestor, code or nobody'
    - 'Measure that instance variables live in the object and methods live in the class, using instance_variables and instance_methods'
    - 'Tell apart a method written with def from a method generated with attr_reader in the same class, using iseq'
---

# Class Definition and Instance Variables

Classifying who wrote a method with four authors; instance variables living in the object and methods in the class; initialize being private; how a method written with attr_reader differs from one written with def.

The previous course measured where a message finds its answer: in the receiver's class, in a mixed-in module, in a given block, or in the syntax itself (M13/K01). It closed with one question left open. `account.balance_text`, `point.x`, `settings.color` and `proxy.sort` are called in the same way, yet the bodies of the four come from four different routes. It is known that a method lives in a particular owner; it is not known who wrote that definition.

This course measures that question. The first lesson takes the smallest form of a class: where do instance variables live, where do methods live, and how is the author of a method classified?

## Four Authors and the Measurement Core

Every lesson of the course asks the same question: who wrote this method? The answer is one of four categories. The **programmer** is the person who wrote the body with `def` in the receiver's own class. An **ancestor** left the definition higher in the chain, in a superclass or a module. **Code** is the machinery that produces a definition at run time through a call: `attr_reader`, `define_method`, `Struct`. **Nobody** is the case where the chain holds no definition at all and the message is caught by `method_missing`. In the output these four names are written in ASCII: `programmer`, `ancestor`, `code`, `nobody`.

**CM1.** In this course the "author" is the category that says where the definition of a method came from. What makes the classification is a rule, not a label: if the receiver's singleton class has no definition, the author is nobody; if the owner of the definition is neither the receiver's class nor its singleton class, the author is an ancestor; if the `iseq` of the definition is `nil` or its label starts with `block`, the author is code; otherwise the author is the programmer. The rule reads the form of a definition, not its intent. This sentence is the common face of several bounding measurements in the course.

The rule lives in the file `author.rb`. It is not rewritten during the course; every lesson loads it in the same form. The order inside the file matters. First it asks whether the definition exists, because the owner of a method with no definition cannot be asked. Then it asks for the owner, because a definition whose owner is not the receiver's class has already come from somewhere else. Last it looks at how the definition was produced: only for definitions that sit in the receiver's own class is it necessary to separate a body written with `def` from the product of a call.

```ruby
# author.rb — a helper that classifies who wrote a method

# Classification asks in order: is there a definition in the receiver's chain at all,
# then does the receiver's own class carry it, then how was the definition produced.
def author(receiver, name)
  chain = receiver.singleton_class
  unless chain.method_defined?(name) || chain.private_method_defined?(name)
    return ["nobody", "no definition"]
  end

  definition = chain.instance_method(name)
  owner = definition.owner
  place = owner == chain ? "singleton class" : owner.to_s
  return ["ancestor", place] unless owner == receiver.class || owner == chain

  iseq = RubyVM::InstructionSequence.of(definition)
  return ["code", "#{place}, no ruby body"] if iseq.nil?
  return ["code", "#{place}, block body"] if iseq.label.start_with?("block")

  ["programmer", "#{place}, def body"]
end
```

`author(receiver, name)` returns an array of two values: the name of the author and the evidence. The evidence states the owner of the definition and how the body was written. The second file, which runs nine observations together, is `authors.rb`. Each observation is a method called in the same way; the difference lies only behind the call.

```ruby
# authors.rb — nine observations: who wrote this method
require_relative "author"

module Reportable
  def report = "report: #{balance_text}"
end

class Account
  include Reportable

  def initialize(owner, balance)
    @owner = owner
    @balance = balance
  end

  def balance_text = "#{@owner} #{@balance}"
  def net = @balance - fee

  private

  def fee = 2
end

class TermDeposit < Account
end

module Lock
  def unlock = "lock opened, #{super}"
end

class Safe
  prepend Lock
  def unlock = "safe opened"
end

Point = Struct.new(:x, :y)

class Settings
  %i[color width].each do |name|
    define_method(name) { "#{name} default" }
  end
end

class Proxy
  def initialize(target)
    @target = target
  end

  def method_missing(name, *args, &block)
    return super unless @target.respond_to?(name)

    @target.public_send(name, *args, &block)
  end

  def respond_to_missing?(name, include_private = false) = @target.respond_to?(name, include_private) || super
end

class Model
  def self.field(name, default)
    define_method(name) { instance_variable_get("@#{name}") || default }
  end
end

class Product < Model
  field :name, "unnamed"
  field :price, 0
end

account = Account.new("amy", 100)
observations = {
  "state reading" => [account, :balance_text, -> { account.balance_text }],
  "superclass"    => [TermDeposit.new("tom", 50), :balance_text, -> { TermDeposit.new("tom", 50).balance_text }],
  "mixin"         => [account, :report, -> { account.report }],
  "private helper" => [account, :fee, -> { account.net }],
  "prepended"     => [Safe.new, :unlock, -> { Safe.new.unlock }],
  "member reader" => [Point.new(3, 4), :x, -> { Point.new(3, 4).x }],
  "generated"     => [Settings.new, :color, -> { Settings.new.color }],
  "proxy"         => [Proxy.new([3, 1, 2]), :sort, -> { Proxy.new([3, 1, 2]).sort }],
  "declaration"   => [Product.new, :price, -> { Product.new.price }],
}

tally = { "programmer" => 0, "ancestor" => 0, "code" => 0, "nobody" => 0 }

printf("%-15s%-11s%-27s%s\n", "observation", "author", "evidence", "result")
observations.each do |name, (receiver, message, call)|
  kind, evidence = author(receiver, message)
  tally[kind] += 1
  printf("%-15s%-11s%-27s%s\n", name, kind, evidence, call.call.inspect)
end

puts
tally.each { |kind, n| printf("%-11s%d\n", kind, n) }
puts "total observations: #{tally.values.sum}"
```

```
observation    author     evidence                   result
state reading  programmer Account, def body          "amy 100"
superclass     ancestor   Account                    "tom 50"
mixin          ancestor   Reportable                 "report: amy 100"
private helper programmer Account, def body          98
prepended      ancestor   Lock                       "lock opened, safe opened"
member reader  code       Point, no ruby body        3
generated      code       Settings, block body       "color default"
proxy          nobody     no definition              [1, 2, 3]
declaration    code       Product, block body        0

programmer 2
ancestor   3
code       3
nobody     1
total observations: 9
```

Of the nine observations, a programmer wrote 2 and an ancestor wrote 3; code produced 3; in 1 there was no definition and `method_missing` caught the message. These numbers do not change during the course. The programmer wrote the `state reading` and `private helper` rows: the bodies of both sit inside `Account` as a `def`. The ancestor wrote the `superclass`, `mixin` and `prepended` rows; the three definitions sit away from the receiver, in `Account`, in `Reportable` and in `Lock`. In the `member reader`, `generated` and `declaration` rows a call produced the definition. In the `proxy` row there is no definition and the result still came back.

The share of this lesson is the first programmer row: `state reading`. The following lessons open the ancestor, code and nobody authors in turn; this lesson measures the simplest structure a programmer writes, a class and its instance variables.

## State in the Object, Behavior in the Class

The `initialize` method of the `Account` class takes two values and assigns both to instance variables. An instance variable is a variable that starts with `@` and is bound to an object (M13/K01). The file below redefines a small slice of the class, because loading `authors.rb` prints the table of nine rows; this slice runs without repeating it.

```ruby
# state.rb — instance variables live in the object
class Account
  def initialize(owner, balance)
    @owner = owner
    @balance = balance
  end
end

account = Account.new("amy", 100)
p account.instance_variables
p account.instance_variable_defined?(:@balance)
p Account.instance_methods(false)
begin
  account.balance
rescue NoMethodError => e
  puts e.class
end
```

```
[:@owner, :@balance]
true
[]
NoMethodError
```

`instance_variables` listed the two variables in the order of assignment: `@owner` and `@balance`. The variables live not in the `Account` class but in the `account` object. `Account.instance_methods(false)`, on the other hand, returned an empty array: the class has no public method. `initialize` is private, and the `false` argument asks only for the class's own definitions. That is why the call `account.balance` raised `NoMethodError`: the variable is inside the object, but because no method was written to read it, it is not visible from outside.

**CM2.** In this lesson an "instance variable" is a variable that is bound to an object and starts with `@`. That a variable exists in an object does not mean that a method reading it exists. State lives in the object and behavior lives in the class; the two sides are tied together through a name such as `@balance`.

An instance variable does not come into being in the object at all until it is assigned. The file below measures this: the variable that is read has not been assigned yet.

```ruby
# unassigned.rb — an unassigned instance variable reads as nil and is not listed
class Account2
  def read_value = @missing
  def write_value(value) = @missing = value
end

object = Account2.new
p object.read_value
p object.instance_variables
object.write_value(5)
p object.read_value
p object.instance_variables
```

```
nil
[]
5
[:@missing]
```

The unassigned variable was read as `nil`, but it did not enter the `instance_variables` list. After the assignment, the variable was both read and listed. Reading creates nothing in the object; only assignment does. This behavior also creates a difference among the objects of a class such as `Account2`: an object on which `write_value` has been called carries the variable `@missing`, and an object on which it has not been called does not. Even when two objects come from the same class, their lists of instance variables need not be the same; the list reflects the history of the object, not of the class. A read that raises no error can for this reason hide a typing mistake: if `@balanse` is written, Ruby quietly gives `nil`.

## Two Objects, One Class

That state lives in the object and behavior in the class is measured with two objects. The file below produces two accounts from the same class and compares two things: the owner of the method and the names of the instance variables.

```ruby
# two.rb — two objects share the same method, they do not share the values
class Account
  def initialize(owner, balance)
    @owner = owner
    @balance = balance
  end

  def balance_text = "#{@owner} #{@balance}"
end

a = Account.new("amy", 100)
b = Account.new("tom", 50)
puts a.balance_text
puts b.balance_text
p a.method(:balance_text).owner == b.method(:balance_text).owner
p a.instance_variables == b.instance_variables
p Account.instance_of?(Class)
p a.class
```

```
amy 100
tom 50
true
true
true
Account
```

The same method gave two separate results on two objects: `amy 100` and `tom 50`. The owner of the method is the same for both objects, and the names of their instance variables are the same too; only the values differ. The method body is written once, in the class, and on every call it reads the variables of the object that receives the call. `Account.instance_of?(Class)` gave `true`: the line `class Account` sets up a constant named `Account`, and what the constant points to is an object of type `Class`. `a.class` returns this object. A class is the place where methods live, and it is also an object that produces objects.

This distinction has a practical consequence. The number of methods does not grow with the number of objects: when a thousand accounts are created, `balance_text` still sits in a single place, once. What multiplies per object is only the values of the instance variables. Asking for the author of a method therefore always points at the class or at the chain; the object itself carries only state, and the measurement that asks for the author does not look at its variables.

## initialize Is Private and Checks the Arity

The call `Account.new("amy", 100)` produces a new object and calls the `initialize` method on this object. It is `new` that calls `initialize`, not code outside. The class keeps it as a private method; code cannot call it by writing `account.initialize(...)`. The file below uses the full definition of `Account` from the core, because the private method `fee` is measured here as well.

```ruby
# private_init.rb — initialize is a private method
require_relative "author"

class Account
  def initialize(owner, balance)
    @owner = owner
    @balance = balance
  end

  def balance_text = "#{@owner} #{@balance}"
  def net = @balance - fee

  private

  def fee = 2
end

account = Account.new("amy", 100)
p account.respond_to?(:initialize)
p Account.private_instance_methods(false).sort
p author(account, :initialize)
begin
  Account.new("a")
rescue ArgumentError => e
  puts e.class
end
```

```
false
[:fee, :initialize]
["programmer", "Account, def body"]
ArgumentError
```

`respond_to?(:initialize)` gave `false`: a private method is not visible when asked from outside. `private_instance_methods(false)` listed the two private methods of the class: `fee` and `initialize`. For `initialize`, `author` found the programmer, because a `def` in the class wrote the body. When `Account.new("a")` was called with a single argument it raised `ArgumentError`: `initialize` expects two arguments, and Ruby checks the count at call time.

**CM3.** In this lesson the "arity" is the number of arguments a method accepts; `Account.new` takes this number from the `initialize` definition. `initialize(owner, balance)` asks for two arguments; that is why `Account.new("a")` raised `ArgumentError` and returned no object. Ruby checks the number before the body runs, so the `@owner` assignment did not happen either. The arity thus also bounds the first state of the object: a constructor that sets up two instance variables does not run without two values. A variable that is not assigned inside `initialize`, and not assigned later by another method, does not exist in that object at all; the constructor of the class is therefore the place where the first state of the object is set up and the most reliable point for seeing the variable names together.

Reopening a class means writing one more `class` block with the same class name. The existing definition is not deleted; the new one is added (M12/K03). The definition of a class therefore does not have to sit in a single place; the measurement below reopens the class.

## The Bounding Measurement

Two authors can live together in the same class, in the same owner. `Account` is reopened and `attr_reader :owner` is added. `attr_reader` generates a reader method: the `owner` method returns `@owner`. In the earlier courses (M08/K01) it was measured what the compiler adds to a class. Here the question is different: of the two methods sitting in the class, one was written by a `def` and the other was produced by a call.

```ruby
# reopened.rb — two authors in the same class: def and attr_reader
require_relative "author"

class Account
  def initialize(owner, balance)
    @owner = owner
    @balance = balance
  end

  def balance_text = "#{@owner} #{@balance}"
  def net = @balance - 2
end

class Account
  attr_reader :owner
end

account = Account.new("amy", 100)
p author(account, :owner)
p author(account, :balance_text)
p Account.instance_methods(false).sort
[:owner, :balance_text].each do |name|
  definition = Account.instance_method(name)
  puts "#{name} #{definition.owner} #{definition.source_location.nil?} #{RubyVM::InstructionSequence.of(definition).nil?}"
end
```

```
["code", "Account, no ruby body"]
["programmer", "Account, def body"]
[:balance_text, :net, :owner]
owner Account false true
balance_text Account false false
```

`author(account, :owner)` found code, and `author(account, :balance_text)` found the programmer. The owner of both is `Account`, and both appear in the `instance_methods(false)` list: `[:balance_text, :net, :owner]`. Knowing the owner does not tell the author. The last loop shows where the distinction comes from. `source_location` is not `nil` for either method: the line of the `attr_reader` call is recorded as its source location. The distinction is made by `iseq`. For `balance_text`, written with `def`, there is an `iseq`; for `owner`, generated with `attr_reader`, it is `nil`. The rule therefore looks at `iseq`, not at the source location.

This measurement shows that the owner does not determine the author: two methods sat in the same owner and came out with two different authors. The same measurement also shows that a class is not an identity card. `Account` does not carry a single author; the author of each method is measured separately. At the class level there is no category that says "the programmer wrote this class".

## Summary

- The measure of the course is who wrote a method: the author may be the programmer, an ancestor, code or nobody, and the decision is made by the rule in `author.rb`.
- Of the nine observations, a programmer wrote 2 and an ancestor wrote 3; code produced 3; in 1 there was no definition and `method_missing` caught the message.
- Instance variables live in the object and methods live in the class; if there is no reader, `account.balance` raises `NoMethodError`, and an unassigned variable reads as `nil` and does not enter `instance_variables`.
- `initialize` is a private method and checks the arity; `Account.new("a")` raises `ArgumentError`.
- The author of this lesson is the programmer: in the `state reading` row, a `def` inside `Account` wrote `balance_text`.
- In the same class, a method written with `def` comes out as the programmer and a method generated with `attr_reader` comes out as code; the owner of the two is the same, and `iseq` makes the distinction.

## Next Step

`balance_text` is written in `Account`, yet an empty subclass named `TermDeposit` can also call the same method and get an answer. There is not a single line of definition in the subclass. Who brings the method to the subclass, and what changes if the same method is rewritten in the subclass? The next lesson, Inheritance and `super`, measures this.
