Historical Context & Motivation
Before programming languages had object-oriented programming (OOP), developers wrote code as long sequences of instructions with little structure. As software grew more complex in the 1960s and 1970s, programmers needed a way to bundle data and behavior together into reusable units. The concept of a constructor — a special piece of code that runs automatically when an object is created — became essential to this new paradigm.
The central question this lesson addresses is: how does a Python object get its starting data the moment it's created? Without a reliable way to set initial values, every programmer would have to manually assign attributes after creating an object — leading to bugs, forgotten steps, and messy code. The __init__ method solves this problem elegantly.
Core Principles & Definitions
In Python, a class is a blueprint for creating objects, and an object (also called an instance) is a specific item built from that blueprint. The __init__ method is the special function inside a class that Python calls automatically every time you create a new instance. It lets you give each object its own personalized starting data, known as instance attributes.
Class
__init__ Method
self Parameter
Instance Attributes
Constructor vs. Initializer
__init__ like a registration form you fill out when you sign up for a new account online. The moment you click "Create Account," the system automatically collects your name, email, and password and stores them in your profile. You don't have to go back later and add each field one by one — it all happens at creation time. That's exactly what __init__ does for Python objects.Visual Explanation
__init__ (purple box), call the class like a function (gold box), Python automatically runs __init__ (cyan box), and a fully initialized object is produced (green box).The diagram above illustrates a critical point: you never call __init__ directly. Instead, you call the class name as if it were a function — like Dog("Rex", "Lab") — and Python handles the rest behind the scenes. First, Python creates a blank object using __new__, then it passes that blank object as self to your __init__ method along with any arguments you provided. By the time __init__ finishes, the object has all its data and is ready to use.
How __init__ Works Under the Hood
When you write my_dog = Dog("Rex", "Lab"), Python executes a precise sequence of steps internally. Understanding this mechanism helps you debug problems and write cleaner classes.
The Object Creation Sequence
- Step 1 — Call the class: Python sees
Dog("Rex", "Lab")and recognizes you are asking to create a new Dog instance. - Step 2 — __new__ creates a blank object: Python's built-in
__new__method allocates memory and creates an empty object. You almost never override this. - Step 3 — __init__ initializes: Python calls
__init__(self, "Rex", "Lab"), whereselfis the blank object. Inside __init__, you attach data usingself.name = name. - Step 4 — Return the object: The fully initialized object is returned and stored in your variable
my_dog. Note that __init__ must never return a value — it should always returnNone(which it does by default).
The self Parameter Explained
The parameter self is what connects your __init__ code to the specific object being created. Every time you write self.something = value, you're saying "attach this data to this particular object." This is why two Dog objects can have different names — each one has its own self during initialization. The word self is a strong convention in Python, not a keyword. You could technically name it anything, but you should always use self to keep your code readable and consistent with the Python community.
self as the first parameter of __init__ is one of the most common beginner errors. If you write def __init__(name, breed): instead of def __init__(self, name, breed):, Python will assign the object itself to name and crash with a TypeError about missing arguments.Common __init__ Patterns
Once you understand the basics of __init__, you'll encounter several common patterns in real-world Python code. These patterns demonstrate the flexibility of constructors and how they can handle different situations.
Pattern 1 (required parameters) is the most straightforward and what you'll use most often as a beginner. Pattern 2 (default parameters) is extremely useful when some attributes have sensible fallback values. Notice how default values go at the end of the parameter list, just like with any Python function. Pattern 3 (computed attributes) shows that you can create attributes inside __init__ that aren't directly passed as arguments — they're derived from the inputs. Pattern 4 (validation) demonstrates one of the most powerful uses of __init__: ensuring that objects are always created with valid data.
Worked Example: Building a BankAccount Class
Let's build a complete class step by step, demonstrating how __init__ fits into a realistic example. We'll create a BankAccount class that initializes with an owner name and an optional starting balance.
__init__ method. The owner is required, but the balance defaults to 0 if not provided.
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balanceowner and balance__init__ to prevent invalid data.
class BankAccount:
def __init__(self, owner, balance=0):
if balance < 0:
raise ValueError("Balance cannot be negative")
self.owner = owner
self.balance = balancedeposit and withdraw methods. These methods access self.balance — the attribute that was set up by __init__.
def deposit(self, amount):
self.balance += amount
return self.balance
def withdraw(self, amount):
if amount > self.balance:
print("Insufficient funds!")
else:
self.balance -= amount
return self.balance__init__ createdowner and balance thanks to __init__.
acct1 = BankAccount("Alice", 500)
acct2 = BankAccount("Bob")
print(acct1.owner) # Alice
print(acct1.balance) # 500
print(acct2.balance) # 0 (used default)
acct1.deposit(200)
print(acct1.balance) # 700__init__ method is like the foundation of a house. Before you can add rooms (methods), furniture (data manipulation), or decorations (string representations), you need a solid foundation that defines the structure. Every other method in your class will rely on the attributes that __init__ sets up.With vs. Without __init__
To appreciate why __init__ matters, let's compare code that uses it against code that doesn't. You can technically create a class without __init__ and add attributes manually after the object is created, but this approach is error-prone and hard to maintain.
| Feature | Without __init__ | With __init__ |
|---|---|---|
| Attribute setup | Must manually set each attribute after creation: dog.name = "Rex" | All attributes set automatically at creation: Dog("Rex") |
| Consistency | Easy to forget an attribute, leading to AttributeError later | Every object is guaranteed to have the same set of attributes |
| Validation | No automatic validation — bad data can slip in unnoticed | Can check data immediately and raise errors before the object is used |
| Readability | Attributes scattered across the codebase | One place to see all attributes — the __init__ method |
| Lines of code | More lines overall, repeated for each object | Fewer lines — constructor handles setup once |
__init__ method in your classes, even if it only sets one or two attributes. This ensures that every object starts life in a known, predictable state. Professional Python developers consider a class without __init__ to be a red flag in code reviews.Connection to Advanced OOP Concepts
The __init__ method is your gateway to more advanced object-oriented programming concepts. As you progress, you'll see how this foundational idea connects to inheritance, polymorphism, and design patterns.
| What You Know Now | Where It Leads |
|---|---|
Writing __init__ in a single class | Inheritance: Child classes can extend or override a parent's __init__ using super().__init__() |
Setting instance attributes with self.x = x | Properties: Use @property decorators to add getter/setter logic around attributes initialized in __init__ |
Using __init__ as the constructor | __new__ vs. __init__: Learn the true constructor (__new__) for advanced patterns like singletons and immutable objects |
Validating data inside __init__ | Dataclasses: Python's @dataclass decorator auto-generates __init__ from type annotations, reducing boilerplate |
When you study inheritance, you'll learn that a child class can have its own __init__ that calls the parent's __init__ using super().__init__(). This lets you build on existing classes without rewriting all the setup code. For example, a SavingsAccount could inherit from BankAccount and add an interest_rate attribute while reusing the parent's owner and balance setup. These advanced topics build directly on the __init__ skills you're learning now.
Practice Problems
self is needed as the first parameter of __init__. What would happen if you created two objects from the same class without self?Circle with an __init__ that takes a radius and also computes and stores the area as an attribute (use π ≈ 3.14159). Then create a Circle with radius 5 and access its area.Playlist whose __init__ takes a name (required) and initializes an empty list called songs. Add an add_song method that appends a song title to the list, and a count method that returns the number of songs. Why is it important that songs is initialized as self.songs = [] inside __init__ rather than as a class variable?GradeBook class for a teacher. The __init__ should take course_name and max_students (default 30). It should initialize an empty dictionary called students and validate that max_students is between 1 and 40. Write the complete __init__ and an enroll method that adds a student name with an empty list of grades, but only if the class isn't full.class Pet:
tricks = []
def __init__(self, name):
self.name = name
def add_trick(self, trick):
self.tricks.append(trick)
dog = Pet("Buddy")
cat = Pet("Whiskers")
dog.add_trick("roll over")
print(cat.tricks)
What does print(cat.tricks) output, and why? How would you fix this class so each pet has its own independent tricks list?Lesson Summary
The __init__ method is Python's initializer — it runs automatically every time you create a new instance of a class. Inside __init__, you use the self parameter to attach instance attributes to the specific object being created. You never call __init__ directly — Python calls it for you when you write something like Dog("Rex", "Lab").
Key patterns include required parameters, default parameters, computed attributes, and validation inside __init__. Always initialize mutable objects like lists and dictionaries inside __init__ (not at the class level) so each object gets its own copy. As you advance, __init__ connects directly to inheritance with super().__init__() and to modern tools like dataclasses.