Mastering `__init__` in Python
OOP Core
The __init__ Method
`__init__` sets up each instance. Validate inputs, wire dependencies, and keep state consistent.
Initializer, not constructor
__init__ does not create the object — it receives an already-created one and fills in its attributes. The actual construction happens in __new__, which allocates the instance and passes it to __init__ as self. You can watch the hand-off:
class Demo:
def __new__(cls):
print('__new__: create the object')
return super().__new__(cls)
def __init__(self):
print('__init__: initialize it')
d = Demo()
# __new__: create the object
# __init__: initialize it
In practice you override __new__ almost never and __init__ all the time. Calling it a "constructor" is common shorthand, but "initializer" is what it actually is — a distinction that explains several of the errors below.
Signatures
__init__ takes parameters like any function: required, with defaults, or keyword-only after a *:
class Invoice:
def __init__(self, *, total: float, customer: str, currency: str = "USD"):
if total < 0:
raise ValueError("Total must not be negative")
self.total = total
self.customer = customer
self.currency = currency
- Use keyword-only parameters for clarity.
- Avoid heavy business logic; keep initialization focused.
The * forces callers to name every argument, which keeps call sites readable and prevents swapped-argument bugs:
inv = Invoice(total=1200.0, customer='Ada')
print(inv.total, inv.currency) # -> 1200.0 USD
Invoice(1200.0, 'Ada')
# TypeError: Invoice.__init__() takes 1 positional argument but 3 were given
Validating in __init__ means an invalid object can never exist — the exception fires before anyone gets a reference to it:
Invoice(total=-5, customer='Ada')
# ValueError: Total must not be negative
Common bugs
Forgetting self. Python always passes the instance as the first argument; if the signature has no slot for it, the call blows up with a confusing count:
class Greeter:
def greet(): # missing self
print('hi')
Greeter().greet()
# TypeError: Greeter.greet() takes 0 positional arguments but 1 was given
Returning a value from __init__. The initializer must return None — the instance is already made, and Demo() will return it regardless. An explicit return of anything else is an error:
class Broken:
def __init__(self):
return self # __init__ must return None
Broken()
# TypeError: __init__() should return None, not 'Broken'
Mutable default arguments. Defaults are evaluated once, at function definition — so a default list is shared by every instance that relies on it:
class Playlist:
def __init__(self, songs=[]): # one list, shared across calls
self.songs = songs
a = Playlist()
b = Playlist()
a.songs.append('Blue in Green')
print(b.songs) # -> ['Blue in Green'] — b was never touched
Use None as the sentinel and build the list inside the body:
class Playlist:
def __init__(self, songs=None):
self.songs = list(songs) if songs is not None else []
a = Playlist()
b = Playlist()
a.songs.append('Blue in Green')
print(b.songs) # -> []
This is the same trap as mutable default arguments in plain functions, and a close cousin of the shared class attribute bug.
Calling super()
class PriorityInvoice(Invoice):
def __init__(self, *, priority_level: int, **kwargs):
super().__init__(**kwargs)
self.priority_level = priority_level
- Always call
super().__init__when inheriting to ensure base initialization runs.
Forwarding **kwargs means the subclass does not need to repeat the parent's parameter list. Using the Invoice class from above:
inv = PriorityInvoice(priority_level=1, total=250.0, customer='Ada')
print(inv.total, inv.priority_level) # -> 250.0 1
Skip the super().__init__ call and the parent's attributes are simply never set — the first access raises AttributeError. See inheritance for how this composes across deeper hierarchies.
Frequently Asked Questions
Is __init__ a constructor?
Strictly, no. __new__ constructs (allocates) the instance; __init__ then initializes the already-existing object it receives as self. The distinction rarely matters day to day — you write __init__ and get a working class — but it explains why __init__ cannot return a different object and why returning anything but None is a TypeError.
Can __init__ return something?
Only None (which a bare return or falling off the end does implicitly). The value of MyClass() is the new instance, produced by __new__ — __init__ has no say in it. Explicitly returning any other value raises TypeError: __init__() should return None.
class Broken:
def __init__(self):
return 42
Broken()
# TypeError: __init__() should return None, not 'int'
Do I always need to define __init__?
No. If a class needs no per-instance setup — it only has methods, or class-level defaults cover everything — omit it and Python uses object.__init__, which does nothing. Add __init__ the moment instances need their own state.
class Flag:
enabled = False # class-level default, no __init__ needed
f = Flag()
print(f.enabled) # -> False