The initialize Method

Published by Programming Line in Ruby. Last updated .

Every time you create a new object in Ruby with SomeClass.new, Ruby quietly calls a special method named initialize on that fresh object. This is where you set up an object’s starting state: assigning instance variables, validating arguments, and doing whatever setup work the object needs before anyone can use it. Understanding initialize is the key to understanding how Ruby objects come into being, and it connects directly to instance variables, keyword arguments, and inheritance with super. By the end of this lesson you’ll know exactly what happens, and in what order, when you call .new.

Overview: What initialize Actually Is

In Ruby, almost everything is an object, including the numbers, strings, and even nil you work with every day. Objects are created from classes, and the class method responsible for creating them is new. You never define new yourself for ordinary classes; it is inherited all the way up from Ruby’s Class and Object machinery. What you do define is initialize, a regular instance method with one special power: Ruby automatically calls it, on your behalf, every single time .new runs.

Here is what actually happens when you write Person.new('Ava', 29). First, .new allocates a brand-new, blank object of the class: memory exists for it, and it already responds to methods defined on Person and its ancestors, but it has no instance variables set yet. Second, .new calls initialize on that blank object, forwarding along whatever arguments you passed to .new. Third, whatever initialize returns is thrown away entirely: .new always hands you back the object itself, never the return value of initialize. This is why you can put anything you like as the last line of initialize without it affecting what Person.new ultimately gives you.

Inside initialize, the usual job is to assign instance variables, names starting with @, that record the object’s starting state. An instance variable that has never been assigned simply evaluates to nil rather than raising an error, but by convention you assign every instance variable your object needs inside initialize, so the object is fully built in a valid, predictable state the moment .new returns.

initialize is also private by default: Ruby marks it private automatically so that code outside the object cannot call person.initialize directly and re-run setup on an already-built object. The only supported way to trigger it is indirectly, through .new.

When a class inherits from another class, method lookup matters. Ruby searches for a method starting at the object’s singleton class, then its class, then any modules included in that class (most recently included first), then the superclass, and so on up the ancestor chain until it reaches Object and finally BasicObject. initialize is looked up exactly the same way as any other method: if a subclass does not define its own initialize, the ancestor chain’s initialize runs instead. If a subclass does define its own, it is expected to call super to invoke the parent class’s initialize and let it do its share of the setup, otherwise the parent’s setup logic is silently skipped.

Syntax

class ClassName
  def initialize(positional_arg, optional_arg = default_value, keyword_arg:, keyword_with_default: default_value)
    @instance_var_one = positional_arg
    @instance_var_two = optional_arg
    @instance_var_three = keyword_arg
  end
end
  • class ClassName ... end is the class definition initialize lives inside.
  • def initialize(...): the method name must be exactly initialize for Ruby to call it automatically from .new.
  • Positional arguments (positional_arg) are required unless given a default with = value, in which case they become optional.
  • Keyword arguments (keyword_arg:) are passed by name at the call site, like ClassName.new(keyword_arg: 5), and are required unless given a default (keyword_with_default: value).
  • @instance_var: assigning to an instance variable inside initialize is how you store data on the object for its whole lifetime.
  • No explicit return is needed or typically used, and whatever initialize returns is ignored by .new anyway.

Examples

Example 1: A Basic Class with initialize

class Person
  def initialize(name, age)
    @name = name
    @age = age
  end

  def greet
    "Hi, I'm #{@name} and I'm #{@age} years old."
  end
end

person = Person.new("Ava", 29)
puts person.greet

Output:

Hi, I'm Ava and I'm 29 years old.

Calling Person.new("Ava", 29) allocates a new Person object and immediately calls initialize("Ava", 29) on it. Inside initialize, the parameters name and age are used to set @name and @age. Those instance variables stick around for the life of the object, so greet can reference them later even though it never receives them as arguments.

Example 2: Keyword Arguments and Defaults

class Book
  attr_reader :title, :author, :pages

  def initialize(title:, author:, pages: 0)
    @title = title
    @author = author
    @pages = pages
  end

  def summary
    "#{title} by #{author} (#{pages} pages)"
  end
end

book1 = Book.new(title: "Practical Object-Oriented Design", author: "Sandi Metz")
book2 = Book.new(title: "Eloquent Ruby", author: "Russ Olsen", pages: 448)

puts book1.summary
puts book2.summary

Output:

Practical Object-Oriented Design by Sandi Metz (0 pages)
Eloquent Ruby by Russ Olsen (448 pages)

Because title: and author: have no default, both are required at every call site, but the order they are passed in does not matter since they are keyword arguments. pages: has a default of 0, so book1 gets 0 pages automatically while book2 overrides it with 448. attr_reader generates simple getter methods for title, author, and pages so summary can read them without touching the @ variables directly.

Example 3: Inheritance and super

class Animal
  attr_reader :name

  def initialize(name)
    @name = name
    @energy = 100
  end

  def status
    "#{name} has #{@energy} energy."
  end
end

class Dog < Animal
  def initialize(name, breed)
    super(name)
    @breed = breed
  end

  def status
    "#{super} (breed: #{@breed})"
  end
end

dog = Dog.new("Rex", "Labrador")
puts dog.status

Output:

Rex has 100 energy. (breed: Labrador)

Dog defines its own initialize, so Animal's initialize would not run automatically. Calling super(name) explicitly invokes Animal#initialize with name, which sets @name and @energy, before Dog#initialize sets its own @breed. The same pattern appears in status: bare super (no parentheses) forwards along whatever arguments the current method received and calls Animal#status, and its result is woven into Dog#status's own string.

How initialize Works, Step by Step

Walking through Person.new("Ava", 29) from Example 1, line by line:

  1. Ruby looks up new starting from Person's singleton class and up through its ancestors, finding the version inherited from Class.
  2. new allocates a blank Person object internally, with no instance variables set.
  3. new calls initialize on that blank object, passing along "Ava" and 29 exactly as given to .new.
  4. Inside initialize, the local parameters name and age are bound to "Ava" and 29; the assignments @name = name and @age = age store that data on the object.
  5. initialize finishes executing. Its last expression, @age = age, evaluates to 29, but this value is discarded.
  6. new returns the same object it allocated in step 2, now fully populated, and that object is what gets assigned to the local variable person.

This is also why calling person.greet afterward works: by the time .new hands the object back, initialize has already run to completion, and every instance variable it set is already in place.

Common Mistakes

Mistake 1: Sharing a Mutable Default Through a Constant

It is tempting to reuse a constant as a default argument value, but if that constant is a mutable object like an array, every instance that relies on the default ends up sharing the very same array.

class ShoppingCart
  DEFAULT_ITEMS = []
  attr_reader :items

  def initialize(items = DEFAULT_ITEMS)
    @items = items
  end

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

cart1 = ShoppingCart.new
cart1.add("Apples")

cart2 = ShoppingCart.new
puts cart2.items.inspect

Output:

["Apples"]

Since cart1 and cart2 both used the default, @items on both of them points at the exact same DEFAULT_ITEMS array object. Mutating it through cart1.add with << changes it for every object that shares that reference, including cart2, which never had anything added to it directly. The fix is to give each object its own fresh array by default:

class ShoppingCart
  attr_reader :items

  def initialize(items = [])
    @items = items
  end

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

cart1 = ShoppingCart.new
cart1.add("Apples")

cart2 = ShoppingCart.new
puts cart2.items.inspect

Output:

[]

Ruby evaluates a default argument expression fresh on every call that needs it, so items = [] creates a brand-new, empty array each time initialize runs without an explicit argument. cart1 and cart2 now hold two completely separate arrays, and mutating one has no effect on the other.

Mistake 2: Assuming initialize's Return Value Matters

Because initialize looks like any other method, it is easy to assume that whatever it returns becomes the result of .new. It does not.

class Counter
  attr_reader :count

  def initialize(start)
    @count = start
    "this string is ignored"
  end
end

c = Counter.new(5)
puts c.class
puts c.count

Output:

Counter
5

Even though the last line of initialize evaluates to the string "this string is ignored", Counter.new(5) still returns a Counter instance, not that string. .new always returns the object it allocated, regardless of what initialize returns. Relying on initialize's return value for anything is a mistake; use instance variables and reader methods like count to expose data instead.

Best Practices

  • Keep initialize focused on setting up state; push heavier logic into separate methods that initialize can call.
  • Prefer keyword arguments once a class needs more than two or three constructor values, so call sites stay readable and order-independent.
  • Expose instance variables set in initialize through attr_reader or attr_accessor rather than letting outside code poke at @variables directly.
  • Validate required data in initialize and raise ArgumentError early rather than silently accepting an invalid object.
  • When subclassing, call super in initialize, usually as the first line, so the parent class's setup logic actually runs.
  • Give default values using Ruby's own default-argument syntax (param = []) rather than pointing at a shared mutable constant, since defaults like this are evaluated fresh on every call.
  • Never rely on initialize's return value for anything; .new always hands back the object itself.

Practice Exercises

1. Write a Rectangle class whose initialize takes keyword arguments width: and height:, storing both as instance variables, and add an area method that multiplies them. Rectangle.new(width: 4, height: 5).area should return 20.

2. Write a BankAccount class whose initialize takes an optional balance: keyword argument defaulting to 0, and raises an ArgumentError if a negative balance is given. Add a deposit method that increases the balance.

3. Create an Employee class that inherits from a Person class like the one in Example 1. Give Employee#initialize an extra salary parameter, call super to reuse Person's setup, and write a method that prints a status line combining the inherited name and age with the new salary.

Summary

  • initialize is an ordinary instance method that Ruby calls automatically whenever .new is used to create an object.
  • .new allocates a blank object first, then calls initialize on it, and finally returns that object regardless of what initialize returns.
  • Instance variables assigned inside initialize (like @name) hold an object's state for its entire lifetime.
  • initialize is private by default and cannot be called directly from outside the object.
  • Positional arguments, defaults, and keyword arguments in initialize work exactly like in any other Ruby method.
  • In subclasses, call super inside initialize to run the parent class's setup logic; skipping it leaves the parent's state uninitialized.
  • Default argument expressions are evaluated fresh on every call, so prefer param = [] over sharing a mutable constant as a default.