The initialize Method
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 ... endis the class definitioninitializelives inside.def initialize(...): the method name must be exactlyinitializefor 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, likeClassName.new(keyword_arg: 5), and are required unless given a default (keyword_with_default: value). @instance_var: assigning to an instance variable insideinitializeis how you store data on the object for its whole lifetime.- No explicit
returnis needed or typically used, and whateverinitializereturns is ignored by.newanyway.
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:
- Ruby looks up
newstarting fromPerson's singleton class and up through its ancestors, finding the version inherited fromClass. newallocates a blankPersonobject internally, with no instance variables set.newcallsinitializeon that blank object, passing along"Ava"and29exactly as given to.new.- Inside
initialize, the local parametersnameandageare bound to"Ava"and29; the assignments@name = nameand@age = agestore that data on the object. initializefinishes executing. Its last expression,@age = age, evaluates to29, but this value is discarded.newreturns the same object it allocated in step 2, now fully populated, and that object is what gets assigned to the local variableperson.
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
initializefocused on setting up state; push heavier logic into separate methods thatinitializecan 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
initializethroughattr_readerorattr_accessorrather than letting outside code poke at@variablesdirectly. - Validate required data in
initializeandraise ArgumentErrorearly rather than silently accepting an invalid object. - When subclassing, call
superininitialize, 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;.newalways 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
initializeis an ordinary instance method that Ruby calls automatically whenever.newis used to create an object..newallocates a blank object first, then callsinitializeon it, and finally returns that object regardless of whatinitializereturns.- Instance variables assigned inside
initialize(like@name) hold an object's state for its entire lifetime. initializeis private by default and cannot be called directly from outside the object.- Positional arguments, defaults, and keyword arguments in
initializework exactly like in any other Ruby method. - In subclasses, call
superinsideinitializeto 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.
