Classes in Ruby

Published by Programming Line in Ruby. Last updated .

A class in Ruby is a blueprint for creating objects that bundle together state (data) and behavior (methods). Almost everything you touch in Ruby — strings, arrays, even the number 5 — is an instance of some class, and understanding how classes work is the key to understanding Ruby itself. Once you can define your own classes, you can model real-world things — a bank account, a shopping cart, a game character — as objects with their own data and their own rules for how that data can be changed.

Overview: How Classes Work in Ruby

In Ruby, everything is an object, and every object is an instance of some class. Run 5.class in irb and you get Integer; run nil.class and you get NilClass; even true and false belong to their own classes. Classes are themselves objects (instances of the class Class), which is part of why Ruby feels so consistent: there is no special syntax carve-out for "primitives" the way some languages have.

You define a class with the class keyword and a constant (capitalized) name. Inside the class body you define methods with def. The most important method is initialize, which Ruby automatically calls whenever you create a new object with SomeClass.new(...). Whatever arguments you pass to .new are forwarded to initialize, and .new itself always returns the newly built object — not whatever initialize happens to return.

Inside instance methods, @name-style variables are instance variables: each object gets its own independent copy, and they default to nil if never assigned (Ruby does not raise an error for reading an unset instance variable, unlike an unset local variable or an undefined method). Instance variables are private to the object by default — there is no way to read @name from outside the object unless you define a method that exposes it, which is exactly what attr_reader, attr_writer, and attr_accessor do: they generate simple getter/setter methods for you so you don’t have to write boilerplate like def name; @name; end by hand.

Variables written with two @@ signs are class variables, shared by the class and every instance of it (and, more subtly, by every subclass too — a frequent source of bugs, covered in Common Mistakes below). Methods defined with def self.method_name are class methods, called on the class itself (like BankAccount.accounts_created) rather than on an instance.

When you call a method on an object, Ruby doesn’t just look in that object’s class — it walks up the ancestor chain: the object’s singleton class, then its class, then any modules mixed in with include (searched in reverse order of inclusion), then the superclass and its included modules, and so on up to Object, Kernel, and finally BasicObject. This is the mechanism that makes inheritance, mixins, and super all work: super simply resumes the method lookup one step further up this same chain.

Syntax

class ClassName < ParentClass
  attr_accessor :attribute_name

  def initialize(param1, param2 = default_value)
    @param1 = param1
    @param2 = param2
  end

  def instance_method
    # uses @param1, @param2
  end

  def self.class_method
    # called as ClassName.class_method
  end

  private

  def helper_method
    # only callable from inside other instance methods
  end
end
  • class ClassName – starts the definition; names must start with a capital letter (they are constants).
  • < ParentClass – optional; makes ClassName inherit methods and behavior from ParentClass. Omit it and Ruby uses Object automatically.
  • attr_accessor / attr_reader / attr_writer – auto-generate getter and/or setter methods for the named instance variables.
  • initialize – the constructor, run automatically by .new; supports positional, default, and keyword arguments like any method.
  • @instance_variable – per-object state, private by default.
  • def self.method_name – defines a class-level method.
  • private – everything defined below this line (until public/protected or the end of the class) can only be called without an explicit receiver, i.e. from inside other instance methods of the same object.

Examples

Example 1: A Basic Class

class Dog
  attr_accessor :name, :breed

  def initialize(name, breed)
    @name = name
    @breed = breed
  end

  def bark
    "#{@name} says Woof!"
  end
end

dog = Dog.new("Rex", "Labrador")
puts dog.bark
puts dog.name
dog.name = "Max"
puts dog.name

Output:

Rex says Woof!
Rex
Max

Calling Dog.new("Rex", "Labrador") runs initialize, storing both arguments as instance variables. attr_accessor :name, :breed generated both a name getter and a name= setter (and the same for breed), which is why dog.name = "Max" works even though we never wrote that method ourselves.

Example 2: Inheritance and super

class Animal
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def speak
    "#{@name} makes a sound."
  end

  def to_s
    "#<Animal: #{@name}>"
  end
end

class Cat < Animal
  def initialize(name, indoor: true)
    super(name)
    @indoor = indoor
  end

  def speak
    "#{super} Specifically, #{@name} says Meow."
  end
end

cat = Cat.new("Whiskers")
puts cat.speak
puts cat
puts cat.is_a?(Animal)

Output:

Whiskers makes a sound. Specifically, Whiskers says Meow.
#<Animal: Whiskers>
true

Cat < Animal makes Cat a subclass. Inside Cat#initialize, super(name) explicitly calls Animal#initialize with name, so @name gets set by the parent’s logic before Cat adds its own @indoor state. Inside Cat#speak, bare super (used inside a string interpolation) calls Animal#speak and returns its string, which Cat#speak then extends. Since Cat never defines its own to_s, puts cat falls back to the one it inherited from Animal — exactly the ancestor-chain lookup described above.

Example 3: Class Variables, Class Methods, and Private Methods

class BankAccount
  @@accounts_created = 0

  attr_reader :owner, :balance

  def initialize(owner, balance: 0)
    @owner = owner
    @balance = balance
    @@accounts_created += 1
  end

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

  def withdraw(amount)
    raise ArgumentError, "Insufficient funds" if amount > @balance
    @balance -= amount
    self
  end

  def self.accounts_created
    @@accounts_created
  end

  private

  def audit_log
    "Audit: #{@owner} has #{@balance}"
  end
end

account = BankAccount.new("Priya", balance: 100)
account.deposit(50).withdraw(30)
puts account.balance
puts BankAccount.accounts_created

second = BankAccount.new("Arjun")
puts BankAccount.accounts_created

Output:

120
1
2

Both deposit and withdraw return self, which is what makes the chained call account.deposit(50).withdraw(30) possible: deposit returns the same account object, and withdraw is then called on that result. The class variable @@accounts_created is shared by the class itself, so every call to BankAccount.new — regardless of which object it produces — increments the same counter, and BankAccount.accounts_created (a class method, note the self.) reports the running total. audit_log is private, so it can only ever be called from inside another instance method of BankAccount, never as account.audit_log from the outside.

Example 4: Endless Methods for Computed Attributes

class Circle
  def initialize(radius)
    @radius = radius
  end

  def area = Math::PI * @radius**2
end

circle = Circle.new(2)
puts circle.area.round(2)

Output:

12.57

Modern Ruby (3.0+) lets you write a one-line method body with def name(args) = expression — an "endless method". It’s ideal for small, computed, read-only attributes like area here, where writing a full def ... end block would just add noise. The value of the expression becomes the method’s return value, exactly as if it were the last line of a normal method.

How It Works Step by Step

When you write Dog.new("Rex", "Labrador"), several things happen in order:

  • Ruby allocates a new, blank object and tags it as an instance of Dog.
  • Ruby looks up the initialize method by walking Dog‘s ancestor chain (its own definition is found immediately here) and calls it on the new object, passing along "Rex" and "Labrador".
  • Inside initialize, @name = name and @breed = breed attach instance variables directly to that specific object — no other Dog instance is affected.
  • initialize finishes; its return value is discarded, and .new returns the object itself.
  • Later, dog.bark triggers another ancestor-chain search, this time for bark: Ruby checks dog‘s singleton class, then Dog, finds it there, and runs it with self bound to dog — which is how @name inside bark resolves to "Rex".

For the Cat < Animal example, the same lookup happens for speak, but this time Ruby finds it on Cat first (since Cat is earlier in the chain than Animal); the call to super inside it explicitly resumes the search one link further up, landing on Animal#speak.

Common Mistakes

Mistake 1: Reading an instance variable without an accessor

class Point
  def initialize(x, y)
    @x = x
    @y = y
  end
end

point = Point.new(1, 2)
puts point.x

This raises NoMethodError: undefined method 'x' for an instance of Point. Instance variables are not automatically readable from outside the object — only methods are callable on an object, and initialize never defined an x method. Fix it with attr_reader (or attr_accessor if you also need a setter):

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end
end

point = Point.new(1, 2)
puts point.x
puts point.y

Output:

1
2

Mistake 2: Confusing == with equal?

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end
end

a = Point.new(1, 2)
b = Point.new(1, 2)
puts a == b
puts a.equal?(b)

Output:

false
false

Both print false because the default Object#== (inherited since Point doesn’t override it) checks object identity, exactly like equal? does — a and b are two distinct objects that merely happen to hold equal-looking data. If you want == to mean "same data", you must define it yourself:

class Point
  attr_reader :x, :y

  def initialize(x, y)
    @x = x
    @y = y
  end

  def ==(other)
    other.is_a?(Point) && x == other.x && y == other.y
  end
end

a = Point.new(1, 2)
b = Point.new(1, 2)
puts a == b
puts a.equal?(b)

Output:

true
false

Note that equal? still correctly reports false — it always checks true object identity and, unlike ==, should almost never be overridden.

Mistake 3: Using a class variable where you meant an instance variable

class ShoppingCart
  @@items = []

  def add(item)
    @@items << item
  end

  def items
    @@items
  end
end

cart1 = ShoppingCart.new
cart2 = ShoppingCart.new
cart1.add("Book")
puts cart1.items.inspect
puts cart2.items.inspect

Output:

["Book"]
["Book"]

Adding an item through cart1 also shows up in cart2! Because @@items is a class variable, there is only ever one array, shared by every ShoppingCart instance (and any subclass). What was intended as per-object state should instead be an instance variable, initialized fresh inside initialize:

class ShoppingCart
  def initialize
    @items = []
  end

  def add(item)
    @items << item
  end

  def items
    @items
  end
end

cart1 = ShoppingCart.new
cart2 = ShoppingCart.new
cart1.add("Book")
puts cart1.items.inspect
puts cart2.items.inspect

Output:

["Book"]
[]

Best Practices

  • Use attr_reader by default; only reach for attr_accessor when external code genuinely needs to reassign the attribute, and attr_writer is rare outside special cases.
  • Keep initialize focused on assigning state — avoid heavy computation or I/O in a constructor.
  • Prefer keyword arguments (def initialize(name:, age: 0)) once a class has more than one or two constructor parameters; it makes call sites self-documenting.
  • Reach for private to mark implementation details that callers shouldn’t depend on — it keeps the public interface small and lets you refactor internals freely.
  • Remember that only nil and false are falsy in Ruby — a brand-new object, an empty array attribute, or a balance of 0 are all truthy, so if @balance is not a safe way to check "is the balance zero?".
  • Use endless methods (def area = ...) for small, single-expression computed attributes; use full def...end for anything with multiple statements or branching.
  • Override == (and, if needed, hash and eql?) whenever "equal" should mean "same data" rather than "same object".

Practice Exercises

  • Write a Rectangle class with width and height passed to initialize. Add instance methods area and perimeter. Expected: for a 3 by 4 rectangle, area is 12 and perimeter is 14.
  • Write a Vehicle class with a speak method, then a Car < Vehicle subclass that overrides speak but calls super inside it to reuse the parent’s message. Print the combined result.
  • Add a class-level counter to your Rectangle class (similar to BankAccount.accounts_created) that tracks how many Rectangle objects have been created so far, exposed through a class method.

Summary

  • A class is defined with class Name ... end; initialize is the constructor, automatically called by .new.
  • @instance_variables hold per-object state and are private unless exposed via attr_reader/attr_writer/attr_accessor.
  • @@class_variables are shared across the class and all its instances (and subclasses) — a common source of surprising bugs.
  • Methods defined with def self.name are class methods, called directly on the class.
  • Inheritance uses class Child < Parent; super calls the same-named method further up the ancestor chain.
  • Method calls are resolved by walking the ancestor chain: singleton class, class, included modules, then superclass, up to Object and BasicObject.
  • The default == checks object identity, same as equal?; override == to compare by data instead.
  • Only nil and false are falsy in Ruby — everything else, including 0 and empty collections, is truthy.