Hello again! Welcome back to our module on Object-Oriented Python.
In our last lesson, we introduced the fundamental concepts of classes and objects. We saw that a class is a blueprint and an object is an instance built from that blueprint. We ended by briefly touching on the __init__ method as the proper way to set an object's initial state.
Today's lesson is dedicated entirely to mastering that special method. Our learning outcome is to use the __init__ special method to initialize object instances. We'll move from just knowing what it is to understanding precisely how it works and why it's the cornerstone of creating clean, predictable, and robust objects. This is a crucial skill, as __init__ is used to create every single data model in a Django applicationโfrom users and tasks to comments and projects.
1. The Constructor: Automating Object Setup
In the previous lesson, we first created an empty object and then assigned attributes to it manually. This is clunky and error-prone. The __init__ method solves this by acting as an initializer or constructor. It's a special method that Python calls automatically every single time an object is created. Its job is to get the object ready for use by setting its initial attributes.
To see this in action, let's watch a short, focused video.
#50 Python Tutorial for Beginners | __init__ method
This video from Telusko clearly demonstrates the core behavior of the __init__ method. It shows that the method is called automatically and how to pass arguments to it when you create an object.
Watch the entire video (it's short!). Pay close attention to these key points: The __init__ method is defined with two underscores on each side. It's called once for each object created (e.g., com1, com2). The arguments you provide when creating an object (e.g., Computer('i5', 16)) are passed directly to __init__. The self.cpu = cpu line is what actually assigns the passed-in value to the object's attribute.
As the video explained, __init__ streamlines object creation. Instead of multiple lines to create and then configure an object, you do it all in one step.
Let's refactor the Task class we started in the last lesson to properly use this pattern.
class Task:
# __init__ is the initializer. It runs when we call Task(...).
# The 'title' and 'description' parameters receive the arguments passed during creation.
def __init__(self, title, description):
print(f"__init__ called! Creating a new task named '{title}'.")
# We assign the passed-in values to the instance using 'self'.
# These become the object's attributes.
self.title = title
self.description = description
self.status = "Not Started" # We can also set default attributes inside __init__.
# --- Using the class ---
# When this line runs, Python does two things:
# 1. Creates an empty Task object.
# 2. Calls the __init__ method on it, passing "Learn Django ORM" and "Read about models" as arguments.
task1 = Task("Learn Django ORM", "Read about models.")
# The same thing happens here for a completely separate object.
task2 = Task("Create first model", "Define the Task model in models.py.")
print("\n--- Accessing attributes ---")
# The attributes are now available on the objects.
print(f"Task 1 Title: {task1.title}")
print(f"Task 2 Status: {task2.status}")
# Expected Output:
# __init__ called! Creating a new task named 'Learn Django ORM'.
# __init__ called! Creating a new task named 'Create first model'.
#
# --- Accessing attributes ---
# Task 1 Title: Learn Django ORM
# Task 2 Status: Not Started
2. The Anatomy of an Initialized Object
Let's break down exactly what's happening. The process relies on the special self parameter.
selfis a reference to the instance being created. When you calltask1 = Task(...), Python creates aTaskobject in memory and implicitly passes that object to__init__as theselfargument.self.attribute = valuestores data on that specific instance. It's like telling Python, "Take the object thatselfis pointing to and attach this piece of data to it."
For someone with an IT background, it can be helpful to visualize what's happening internally. When an object is created, Python often stores its attributes in a special dictionary called __dict__. The self parameter is the bridge that allows __init__ to populate this dictionary.

To see a complete, practical example that ties everything together, let's review a short article.
Python OOP Practice | Define a Simple Class
This page from apxml.com provides a very clear, hands-on example of defining a class with __init__ and then using it. It reinforces the concepts we just discussed with a new example.
Read the sections 'Initializing Objects with init', 'Creating and Using Dog Objects (Instances)', and review the final 'Full Code Example'. Focus on how the arguments in Dog("Buddy", ...) flow into the __init__(self, name, ...) method and are assigned via self.name = name.
3. Making __init__ More Powerful
The __init__ method isn't just for simple assignments. You can make it more robust and flexible. Two common techniques are setting default values and performing input validation.
- Default Values: You can make arguments optional by providing a default value, just like with regular Python functions. This is perfect for attributes that have a common starting value, like the
statusof a new task. - Input Validation:
__init__is the ideal place to check if the initial data is valid. This prevents you from creating "broken" objects and helps catch bugs early.
The following reading explains these techniques clearly.
Python __init__() Function: Syntax, Usage, and Examples
This article from Mimo's glossary covers several practical uses for __init__. We'll focus on two sections that are immediately useful for building our Task Manager application.
Please read the sections titled 'Using Default Values in init()' and 'Validating Input with Python init()'. These show how to make your object creation more flexible and safer.
Let's apply these ideas to a User class for our application. We'll give new users an is_active status that defaults to True and validate that the provided email looks like a real email address.
class User:
def __init__(self, username, email, is_active=True):
# --- Input Validation ---
# Ensure username is not empty.
if not username:
raise ValueError("Username cannot be empty.")
# A simple check for the email format.
if "@" not in email or "." not in email:
raise ValueError("Invalid email format provided.")
# --- Attribute Assignment ---
self.username = username
self.email = email
self.is_active = is_active # Uses the default value if not provided.
def display_profile(self):
status = "Active" if self.is_active else "Inactive"
print(f"User: {self.username} ({self.email}) - Status: {status}")
# --- Using the enhanced class ---
# Creating a user with default 'is_active' status
user1 = User("jane_doe", "jane.doe@example.com")
user1.display_profile()
# Creating a user and explicitly setting 'is_active' to False
user2 = User("john_smith", "john.s@example.org", is_active=False)
user2.display_profile()
# Now, let's try to create an invalid user.
# This will stop execution and raise a ValueError.
try:
invalid_user = User("testuser", "not-an-email")
except ValueError as e:
print(f"\nError creating user: {e}")
# Expected Output:
# User: jane_doe (jane.doe@example.com) - Status: Active
# User: john_smith (john.s@example.org) - Status: Inactive
#
# Error creating user: Invalid email format provided.
Test your understanding!
Let's enhance our Task class. Modify its __init__ method to include a priority attribute.
- The
priorityshould be an integer. - It should have a default value of
1. - Add input validation inside
__init__to ensure that the priority is between 1 and 5 (inclusive). If it's not, raise aValueError.
Create one valid task with a default priority and another valid task with a priority of 4. Then, use a try...except block to show that creating a task with a priority of 6 fails.
Show answer
Here is a possible solution:
class Task:
def __init__(self, title, description, priority=1):
# Input Validation for priority
if not 1 <= priority <= 5:
raise ValueError("Priority must be between 1 and 5.")
self.title = title
self.description = description
self.status = "Not Started"
self.priority = priority # Assign the validated priority
def display_task(self):
print(f"'{self.title}' [Priority: {self.priority}] - Status: {self.status}")
# --- Testing the implementation ---
# Task with default priority
task_a = Task("Review code", "Review the latest pull request.")
task_a.display_task()
# Task with a specific, valid priority
task_b = Task("Deploy to staging", "Push the new version to the staging server.", priority=4)
task_b.display_task()
# Attempting to create a task with an invalid priority
try:
invalid_task = Task("Plan weekly sprint", "Organize the backlog.", priority=6)
except ValueError as e:
print(f"\nFailed to create task. Reason: {e}")
# Expected Output:
# 'Review code' [Priority: 1] - Status: Not Started
# 'Deploy to staging' [Priority: 4] - Status: Not Started
#
# Failed to create task. Reason: Priority must be between 1 and 5.
Conclusion
Excellent work! You've now done a deep dive into one of the most important methods in Python's object-oriented paradigm. Properly using __init__ is the key to creating objects that are reliable, predictable, and easy to work withโa non-negotiable skill for building complex applications like the one you're aiming for.
Key Takeaways:
- The
__init__method is the initializer for a class, running automatically every time a new object is created. - The first parameter is always
self, which is a reference to the specific instance being created. - You assign attributes to the instance using the
self.attribute = valuesyntax inside__init__. - You can make
__init__more robust by providing default values for parameters and by adding input validation to prevent the creation of invalid objects.
Preview of the Next Lesson:
In this lesson, all the attributes we created (like self.title and self.priority) are instance variablesโeach object gets its own copy. But what if you wanted to have a variable that is shared by all objects of a class? In the next lesson, we will differentiate between class variables and instance variables, exploring how and why you would use each type.