Every object in your program has a birth moment, and __init__ is where it happens. Get that moment right and the whole class can trust its own data; get it wrong and the bug surfaces three files away. This guide covers the Python init constructor patterns that matter: defaults, validation, factory methods, and the mutable default trap.
“Beware of bugs in the above code; I have only proved it correct, not tried it.”
Donald Knuth, 1977 memo
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Intermediate | Reading Time: 17 minutes
Think of the Python __init__ constructor as the setup checklist for a new hire on their first day. Before they can do any real work, someone hands them a laptop, an email address, and an ID badge. That setup happens once, right at the start. The __init__ method does the same thing for an object: the moment you create it, Python runs __init__ to fill in the object’s starting details so it is ready to use.
Say a user named Rahul signs up on your site. When you write user = User("Rahul", "rahul@technoscripts.com"), Python builds a fresh, empty object and then calls __init__ on it. Inside that method you assign values to attributes, like self.name and self.email, and those become the object’s data. The first parameter is always self, which is just the new object itself. You do not pass self in by hand; Python wires it up for you.
You already know how to write a class and make objects from it. The trouble starts the moment real life shows up: some parameters should be optional, some values need checking before you trust them, and sometimes you want a few different ways to build the same kind of object. This post walks through every __init__ pattern you will reach for, from a plain constructor to factory methods that handle messier setup. One small warning lives in here too: the mutable default trap, a bug that catches almost every Python developer at least once.
Table of Contents
Basic __init__: The Python Init Constructor
The diagram traces Python’s two-step object creation. First __new__ sets aside memory and hands back a raw, empty object. Then __init__ receives that object as self and fills in its attributes. In day-to-day Python you only write __init__, because __new__ comes free from object. The split matters later, when you meet singletons, immutable types, or metaclasses. For now, keep your eyes on the __init__ part of the flow: it is like a blank ID card coming out of the printer, then someone writing your name and photo onto it before handing it over.
📄 basic_init.py: required parameters
class User:
def __init__(self, name, email):
self.name = name
self.email = email
self.is_active = True # Default attribute, not a parameter
self.login_count = 0
def login(self):
self.login_count += 1
return f"{self.name} logged in (total: {self.login_count})"
user = User("Rahul", "rahul@technoscripts.com")
print(user.login())
print(user.login())
print(f"Active: {user.is_active}")
▶ Output
Rahul logged in (total: 1) Rahul logged in (total: 2) Active: True
What happened here: __init__ takes self (the new object) plus whatever arguments you pass in. The line self.name = name copies the parameter onto the object as an attribute, so it sticks around after the method ends. Notice self.is_active = True: that one is not a parameter at all, just a sensible starting value every new user gets for free. Here is the habit worth forming early. If a method anywhere in the class is going to read an attribute, give that attribute a value in __init__. Then the object is fully formed the instant it exists, and you never trip over a missing attribute three methods later.
Default and Optional Parameters
Not every object needs every detail spelled out, and a good Python init constructor reflects that. A product always has a name and a price, but its category and discount can fall back to sensible defaults. Give a parameter a default value in __init__ and it becomes optional: callers can skip it, or pass their own value to override it. It is like a form where some fields are pre-filled and you only change the ones that differ.
📄 default_params.py: flexible initialization
class Product:
def __init__(self, name, price, category="General", discount=0):
self.name = name
self.price = price
self.category = category
self.discount = discount
def final_price(self):
return self.price * (1 - self.discount / 100)
def __repr__(self):
return f"Product('{self.name}', {self.price}, '{self.category}')"
# All valid, defaults fill in what you skip
p1 = Product("Laptop", 85000)
p2 = Product("Keyboard", 2500, "Electronics")
p3 = Product("Book", 500, "Education", 10)
for p in [p1, p2, p3]:
print(f"{p.name}: Rs.{p.final_price():.0f} ({p.category})")
▶ Output
Laptop: Rs.85000 (General) Keyboard: Rs.2500 (Electronics) Book: Rs.450 (Education)
What happened here: Parameters with a default value, like category="General", are optional. The laptop skipped both category and discount, so it took the defaults. The keyboard set a category but kept the zero discount. The book set everything. One __init__, three different ways to call it. There is one rule to remember: parameters with defaults must come after the required ones in the signature, exactly like any other Python function. The constructor follows the same argument rules you already know.
The Mutable Default Trap
This is the one that catches everybody. It looks harmless: give tasks a default of an empty list, so a new task list starts empty. The catch is that the default list is created once, when Python first reads the def line, and then every object that skips that argument shares the exact same list. Picture a single shared whiteboard in an office that everyone is told is “their own.” Two colleagues, Vinay and Anvay, each believe the board belongs to them. Vinay writes a note, Anvay walks over, and Vinay’s note is sitting there on what Anvay thought was his blank board. Same trap, different room.
📄 mutable_default_trap.py: the bug that bites every Python developer
# WRONG: mutable default argument
class TaskListBad:
def __init__(self, owner, tasks=[]):
self.owner = owner
self.tasks = tasks # All instances share the SAME list!
t1 = TaskListBad("Vinay")
t2 = TaskListBad("Anvay")
t1.tasks.append("Write report")
print(f"Anvay's tasks: {t2.tasks}") # Vinay's task appears here!
# CORRECT: use None and create the list inside __init__
class TaskListGood:
def __init__(self, owner, tasks=None):
self.owner = owner
self.tasks = tasks if tasks is not None else []
t3 = TaskListGood("Vinay")
t4 = TaskListGood("Anvay")
t3.tasks.append("Write report")
print(f"Anvay's tasks: {t4.tasks}") # Empty, as expected
▶ Output
Anvay's tasks: ['Write report'] Anvay's tasks: []
What happened here: Default arguments are built once, at the moment Python reads the def line, not fresh on every call. So a default of [] is a single list that every default-using object points at. Vinay appends to his list, and because Anvay’s object never got its own list, Anvay sees Vinay’s task too. The fix is the same every time: make the default None, then inside __init__ create a brand new list when nothing was passed. The good version gives each owner a separate whiteboard, so Anvay’s stays empty. Burn this pattern into memory; you will use it for lists, dicts, and sets for the rest of your Python life.
Validation in __init__
The init constructor is a great place to slam the door on bad data. Think of __init__ as the bouncer at the entrance: check the input the moment it arrives, and if it is wrong, refuse to let the object exist at all. A temperature below absolute zero is not a real reading, so a Temperature object should never hold one. Catch it here and you save yourself a confusing crash somewhere far away.
📄 validation_init.py: reject bad data at creation time
class Temperature:
def __init__(self, celsius):
if not isinstance(celsius, (int, float)):
raise TypeError(f"Temperature must be a number, got {type(celsius).__name__}")
if celsius < -273.15:
raise ValueError(f"Temperature can't be below absolute zero (-273.15C), got {celsius}")
self.celsius = celsius
@property
def fahrenheit(self):
return self.celsius * 9/5 + 32
def __repr__(self):
return f"Temperature({self.celsius}C / {self.fahrenheit:.1f}F)"
t1 = Temperature(100)
print(t1)
t2 = Temperature(-40)
print(t2)
try:
bad = Temperature(-300)
except ValueError as e:
print(f"Error: {e}")
try:
bad = Temperature("hot")
except TypeError as e:
print(f"Error: {e}")
▶ Output
Temperature(100C / 212.0F) Temperature(-40C / -40.0F) Error: Temperature can't be below absolute zero (-273.15C), got -300 Error: Temperature must be a number, got str
What happened here: The two valid temperatures sailed through and printed fine. The -300 reading tripped the absolute-zero check and raised a ValueError, so that object was never built. The "hot" string failed the type check and raised a TypeError. That is the whole idea: validate up front and fail fast, with a message that says exactly what went wrong. Reject the bad value while you still know what it was, instead of letting it slip in and blow up three methods later with an error that points nowhere useful.
Computed Attributes in __init__
Sometimes an attribute is not something the caller passes in; it is something you work out from what they did pass. You give a Person their birth year, and the object figures out their age. Doing that math once inside __init__ means every method afterward can just read self.age without recalculating it. It is like writing your age on the top of a form right after you fill in your date of birth, so you never have to count it again. Below, two friends named Prathamesh and Anvi hand over only their birth years, and the constructor fills in their ages.
📄 computed_attrs.py: attributes worked out from other attributes
from datetime import date
class Person:
def __init__(self, name, birth_year):
self.name = name
self.birth_year = birth_year
self.age = date.today().year - birth_year # Computed
def __repr__(self):
return f"Person('{self.name}', age={self.age})"
prathamesh = Person("Prathamesh", 1998)
anvi = Person("Anvi", 2000)
print(prathamesh)
print(anvi)
▶ Output
Person('Prathamesh', age=28)
Person('Anvi', age=26)
What happened here: We passed in a name and a birth year, and __init__ stored both, then computed self.age from the current year. The numbers above are from a run in 2026, so 1998 gives 28 and 2000 gives 26. Run it next year and the ages bump up by one, because date.today() always reads the real date. That is the small catch with computing a value once: the age is frozen at the moment the object was created, not recalculated as time passes. For a quick snapshot that is fine. If you need an age that always stays current, compute it in a method or a @property instead, which the post on property decorators covers.
Factory Methods with @classmethod
Sometimes a single Python init constructor is not enough. Your data arrives in different shapes: a neat set of values today, a dash-separated string from a file tomorrow, a shortcut for the common case after that. Rather than cram all of it into __init__ with a pile of if checks, you add a @classmethod that prepares the data its own way and then calls the normal constructor. These are factory methods, and they read like named recipes: Employee.from_string(...) tells you exactly how that employee is being built.
Think of a coffee shop with one espresso machine but a labelled button for each drink. In the example below, three new employees join the company: Niranjan is hired directly with full details, Pravin’s record arrives as a dashed string from a file, and Viraj comes in through the intern shortcut.
📄 factory_methods.py: alternative constructors
class Employee:
def __init__(self, name, age, department, salary):
self.name = name
self.age = age
self.department = department
self.salary = salary
@classmethod
def from_string(cls, data_string):
"""Create from 'name-age-dept-salary' format"""
name, age, dept, salary = data_string.split("-")
return cls(name, int(age), dept, int(salary))
@classmethod
def intern(cls, name, age, department):
"""Create intern with fixed salary"""
return cls(name, age, department, salary=15000)
def __repr__(self):
return f"{self.name} ({self.department}) - Rs.{self.salary}"
# Three ways to create the same type of object
e1 = Employee("Niranjan", 27, "Engineering", 95000)
e2 = Employee.from_string("Pravin-30-Design-85000")
e3 = Employee.intern("Viraj", 24, "Marketing")
print(e1)
print(e2)
print(e3)
▶ Output
Niranjan (Engineering) - Rs.95000 Pravin (Design) - Rs.85000 Viraj (Marketing) - Rs.15000
What happened here: A @classmethod gets the class itself as its first argument, written as cls, instead of an instance. So calling cls(...) inside the method runs the regular __init__, exactly like writing Employee(...) would. from_string split the dashed text into pieces, turned the numbers into ints, and handed them to the constructor. intern filled in a fixed salary so you do not repeat it every time. All three objects came out of the same __init__, just reached by different doors. Using cls rather than hardcoding Employee also means subclasses get the right type back for free. The from_string style shows up constantly in real codebases that read rows from files or Application Programming Interfaces (APIs).
__new__ vs __init__: What Actually Happens
Here is the part people get tangled up on, so let us slow it down. Creating an object is really two steps, not one, and the init constructor is only the second. First __new__ builds an empty object and hands it back. Then __init__ takes that object and fills it in. Compare it to moving into a flat: __new__ hands you the keys to an empty room, and __init__ moves the furniture in. The little class below prints a line at each stage so you can watch the order with your own eyes.
📄 new_vs_init.py: the two-step creation process
class Traced:
def __new__(cls, *args, **kwargs):
print(f" __new__: Allocating memory for {cls.__name__}")
instance = super().__new__(cls)
print(f" __new__: Instance created (id: {id(instance)})")
return instance
def __init__(self, name):
print(f" __init__: Setting name to '{name}'")
self.name = name
print(f" __init__: Initialization complete")
print("Creating object...")
obj = Traced("Rahul")
print(f"Result: {obj.name}")
▶ Output
Creating object... __new__: Allocating memory for Traced __new__: Instance created (id: 2935871473040) __init__: Setting name to 'Rahul' __init__: Initialization complete Result: Rahul
What happened here: Read the output top to bottom and you can see the order plainly. __new__ runs first and builds the empty object (step 1). Then __init__ runs and sets self.name (step 2). Only after both finish does obj.name hold “Rahul”. The big number is the object’s id(), roughly its address in memory; yours will be a different number on every run and every machine, which is completely normal. The takeaway: nearly all the time you write __init__ only and never touch __new__. You reach for __new__ in rare cases like singletons, immutable types, or metaclass tricks. Skip it and Python quietly uses object.__new__ for you, which is exactly what you want.
Common Mistakes
Mistake 1: Returning a value from __init__
📄 mistake_return.py
class Bad:
def __init__(self, x):
self.x = x
# return self.x # TypeError: __init__() should return None, not 'int'
class Good:
def __init__(self, x):
self.x = x
# __init__ returns None on its own, which is correct
Strictly speaking, __init__ is an initializer, not the thing that creates the object; __new__ already did that. __init__ just sets up attributes on an object that already exists, so it has nothing to return. Try to return any value other than None and Python raises TypeError: __init__() should return None. Leaving out a return entirely is exactly right, because a function with no return hands back None on its own.
Mistake 2: Not setting all attributes in __init__
📄 mistake_missing_attr.py
# BAD: attribute created in a method, not in __init__
class UserBad:
def __init__(self, name):
self.name = name
def load_profile(self):
self.bio = "Some bio" # Created here, not in __init__
# user.bio raises AttributeError if load_profile() was never called
# GOOD: every attribute set in __init__
class UserGood:
def __init__(self, name):
self.name = name
self.bio = None # Exists from the start
def load_profile(self):
self.bio = "Some bio"
In the bad version, bio only springs into existence if someone happens to call load_profile() first. Read user.bio before that and Python raises AttributeError, because the attribute genuinely does not exist yet. The good version sets self.bio = None right in __init__, so the attribute is always there from the very first moment, even when it is empty. The fix costs one line and removes a whole class of “sometimes it works” bugs.
Best Practices
- DO set every attribute in
__init__, even if it starts asNone - DO validate parameters early and raise descriptive errors
- DO use
Noneas default for mutable arguments (lists, dicts, sets) - DO use
@classmethodfor alternative constructors instead of overloading__init__ - DON’T return values from
__init__ - DON’T do heavy I/O (file reads, API calls) in
__init__; use a separate method or a factory instead
Conclusion
The Python init constructor, __init__, is where an object comes to life. Set every instance attribute here, even the ones that start as None. Check your inputs early and fail loudly when they are wrong. Use None as the default for any list, dict, or set, so each object gets its own copy instead of sharing one. When you need more than one way to build an object, write a @classmethod factory rather than stuffing extra logic into __init__. And __new__ is there for the rare singleton or immutable type, so you can happily set it aside for now.
Now that classes and constructors feel comfortable, the next step is the three kinds of methods. The class methods tutorial walks through instance methods, class methods, and static methods: what each one receives, when to use it, and why the difference actually matters in real code. And if you want to jump around or see everything this series covers, browse the full Python + AI/ML tutorial series home.
Practice Exercises
- Exercise 1: Write a
BankAccountclass. The__init__takes an owner name and an optional starting balance that defaults to 0. Set every attribute in the constructor, then create two accounts to confirm they hold separate balances. - Exercise 2: Add validation to that
BankAccount: reject a non-numeric balance withTypeErrorand a negative starting balance withValueError. Wrap a couple of bad calls intry/exceptand print the error messages. - Exercise 3: Give the class a
@classmethodfactory namedfrom_csv_rowthat takes a string like"Rahul,5000", splits it, converts the balance to a number, and returns a built account. This is the factory pattern from this post combined with the validation you added in Exercise 2.
Frequently Asked Questions
What is __init__ in Python?
__init__ is the Python init constructor, the initializer method that runs automatically when you create an object. It sets up instance attributes on the newly created object. It’s often called the ‘constructor’ but technically __new__ creates the object and __init__ initializes it.
What is the difference between __init__ and __new__ in Python?
__new__ creates and returns the empty object (allocates memory). __init__ receives that object as self and sets its attributes. You almost never need to override __new__; it is used only for singletons, immutable types, and metaclasses.
Why can’t I use a list as a default parameter in __init__?
Default arguments are evaluated once at function definition time, not at each call. A default [] creates one list shared by all instances. Use None as default and create a new list inside __init__: self.items = items if items is not None else [].
Can __init__ return a value?
No. __init__ must return None (implicitly). Returning anything else raises TypeError. __init__ initializes an object that already exists; it does not create or return objects.
What is a factory method in Python?
A factory method is a @classmethod that creates instances through alternative logic. For example, Employee.from_string('Rahul-28-Engineering-95000') parses a string and calls the regular constructor. It provides named, readable ways to create objects from different data formats.
Should I validate parameters in __init__?
Yes. Validate early, fail fast. If someone passes an invalid email or negative age, raise ValueError or TypeError in __init__ before the object exists. It’s much harder to debug bad data that surfaces three methods later.
Interview Questions on Python Constructors
Try each one aloud first. The phrasing you produce under mild pressure is what interviews measure.
Q: Every object of your class seems to share state: one user adds a task and a completely different user sees it in their list. What do you check first?
Two usual suspects. First, look for a mutable default argument like def __init__(self, tasks=[]): that list is created once at definition time and shared by every instance that relies on the default. Second, check whether the attribute was defined at class level (tasks = [] directly under class) instead of inside __init__, because class attributes are shared too. The fix in both cases is the same: default to None and create a fresh list per object inside __init__.
Q: Your __init__ makes a database call to load the user’s profile, and now unit tests are slow and flaky. How would you restructure it?
Get the I/O out of the constructor. Keep __init__ limited to assigning attributes, then either load the profile lazily in a separate method like load_profile(), or provide a @classmethod factory such as User.from_database(user_id) that does the fetch and then calls the plain constructor. Tests can then build a User directly with fake data and never touch the database. As a bonus, objects become cheap to create, which matters when you build thousands of them.
Q: Python has no method overloading, so how do you offer multiple ways to construct the same class?
Combine default parameter values with @classmethod factory methods. Defaults handle the “same data, some pieces optional” case in a single __init__. For genuinely different input shapes, write named factories like Employee.from_string(...) or Date.fromtimestamp(...) that convert their input and call cls(...). This is more readable than one overloaded constructor because the method name documents where the data comes from.
Q: A subclass defines its own __init__ but forgets to call super().__init__(). What actually goes wrong?
The parent’s __init__ simply never runs, so every attribute the parent would have set does not exist on the object. Nothing fails at creation time; the crash comes later as an AttributeError when a parent method reads an attribute it expected to find. The rule: if a subclass overrides __init__, it should call super().__init__(...) first and then add its own attributes.
Q: When would you genuinely need to override __new__ instead of __init__?
When the object’s value must be fixed at creation, or when you need to control whether a new object is created at all. Subclassing an immutable type like str or tuple requires __new__, because by the time __init__ runs the value is already frozen. The singleton pattern is the other classic case: __new__ can return an existing instance instead of allocating a new one. Everything else belongs in __init__.
Q: Is __init__ called when an object is copied or unpickled?
No. Both copy.copy() and pickle.load() rebuild the object by creating it through __new__-level machinery and restoring its __dict__ directly, skipping __init__ entirely. This surprises people who put side effects in the constructor, like registering the object somewhere or opening a connection, because those side effects never happen for copies. It is one more reason to keep __init__ a plain attribute-setting method.
Further reading: the official Python documentation is the authoritative source on this.
Related Posts
Previous: Python: OOP Concepts, Classes, Objects, Why OOP
Next: Python: Instance Methods, Class Methods, Static Methods
Series Home: Python + AI/ML Tutorial Series

No comment