SECTION 1
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.
1967 • Simula Introduces Objects
1983 • C++ Formalizes Constructors
1991 • Python Is Born
2000s • Python Becomes Mainstream
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.
SECTION 2
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
KEY TAKEAWAY
__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.SECTION 3
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.
SECTION 4
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.
Common Mistake
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.SECTION 5
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.
SECTION 6
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.
Building a BankAccount Class
- 1
Step 1 — Define the Class and __init__
We start by creating the class and defining its__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 = balanceClass defined with two instance attributes:ownerandbalance - 2
Step 2 — Add Validation
A bank account shouldn't start with a negative balance. We add a check inside__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 = balanceInvalid data is caught immediately at object creation - 3
Step 3 — Add Methods That Use the Attributes
Now we adddepositandwithdrawmethods. These methods accessself.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.balanceMethods rely on the attributes__init__created - 4
Step 4 — Create and Use Objects
Now we create two separate accounts. Each one gets its ownownerandbalancethanks 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) # 700Alice's account starts with 500, Bob's starts with 0 (default). Each object is independent.
KEY TAKEAWAY
__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.SECTION 7
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 |
Comparison of manual attribute assignment vs. using __init__
BEST PRACTICE
__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.SECTION 8
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 |
How __init__ connects to advanced topics
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.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
self is needed as the first parameter of __init__. What would happen if you created two objects from the same class without self?Show answer
self parameter refers to the specific object being created. Without it, the __init__ method would have no way to attach attributes to the correct object. If you created two objects from the same class, their data would have nowhere to go — there would be no connection between the method and the particular instance. In short, self is the link that says 'store this data on THIS specific object, not some other one.'Question 2: BASIC CALCULATION
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.Show answer
class Circle:
def __init__(self, radius):
self.radius = radius
self.area = 3.14159 * radius ** 2. When you run c = Circle(5), Python calls __init__ with radius=5, computes area as 3.14159 × 25 = 78.53975, and stores both values. Accessing c.area returns 78.53975. This demonstrates the computed attribute pattern from Section 5.Question 3: INTERMEDIATE
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?Show answer
class Playlist:
def __init__(self, name):
self.name = name
self.songs = []
def add_song(self, title):
self.songs.append(title)
def count(self):
return len(self.songs). It's critical that songs is created inside __init__ with self.songs = [] because each Playlist needs its own separate list. If you instead put songs = [] at the class level, all Playlist objects would share the same list — adding a song to one playlist would add it to every playlist. This is a common and subtle bug.Question 4: APPLIED
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.Show answer
class GradeBook:
def __init__(self, course_name, max_students=30):
if max_students < 1 or max_students > 40:
raise ValueError("max_students must be 1-40")
self.course_name = course_name
self.max_students = max_students
self.students = {}
def enroll(self, name):
if len(self.students) >= self.max_students:
print("Class is full!")
else:
self.students[name] = []. This combines three patterns: required parameters (course_name), defaults (max_students=30), and validation (checking the range). The enroll method uses self.students and self.max_students, both of which were set up by __init__. This demonstrates how __init__ creates the foundation that all other methods depend on.Question 5: CRITICAL THINKING
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?Show answer
["roll over"] — even though we only taught the dog that trick! This happens because tricks = [] is a class variable, meaning it's shared by ALL Pet instances. When dog.add_trick modifies the list, cat sees the same change. The fix is to move tricks into __init__: def __init__(self, name):
self.name = name
self.tricks = []. Now each Pet gets its own separate tricks list created during initialization. This is a classic example of why putting mutable objects (like lists and dictionaries) in __init__ rather than at the class level is essential for correct behavior.SUMMARY
Lesson Summary
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.