__init__ is the method Python calls right after a new instance is created. Its job is to fill in the instance's initial state: set attributes from the arguments, establish default values, validate invariants, allocate resources. Everything downstream in the class assumes __init__ already did its job, so a little care here prevents a lot of “why is self.xyz None here?” debugging later.
The signature is always def __init__(self, ...). The arguments after self are whatever you want callers to pass when they write MyClass(...). Typical patterns: required arguments first, optional keyword arguments with defaults afterwards, and a docstring that documents the contract.
Defaults deserve special care. Do not use a mutable object as a default argument: def __init__(self, items=[]) shares the same list across every instance that doesn't pass items, and bugs follow. Use None and assign inside: items = items if items is not None else []. For dataclasses, field(default_factory=list) is the safe form.
Sometimes you need more than one way to construct an object. A classmethod that returns a configured instance is the idiomatic pattern: @classmethod def from_dict(cls, d): return cls(**d). The main __init__ stays focused; alternative constructors live alongside it as named methods.
A well-formed __init__
Validate inputs early with raise ValueError/TypeError. Set every attribute the rest of the class uses; avoid “optional attributes” added later in random methods. Keep the method short — if it does real work, factor helpers out and call them.
If the class owns a resource (file handle, connection), acquire it in __init__ only if cleanup is guaranteed elsewhere (e.g. a close method and the with protocol). Otherwise defer the acquisition to a method or context manager.
Alternative constructors via classmethod
@classmethod takes cls as the first argument, which means it also does the right thing for subclasses. cls(...) instead of MyClass(...) inside the classmethod keeps the call polymorphic.
Examples: Path.home(), datetime.now(), dict.fromkeys(). These classmethods are recognizable alternative constructors: they return an instance of the class they belong to.
The initialization toolbox.
| Tool | Purpose |
|---|---|
__init__(self, ...)method | Sets up instance state right after creation. |
@classmethoddecorator | Alternative constructors take cls. |
field(default_factory=list)function | Safe mutable defaults in dataclasses. |
__post_init__method | Validation hook after dataclass init. |
__new__(cls, ...)method | Rare: customize instance allocation itself. |
raise ValueError(...)statement | Signal invalid inputs. |
type hintsfeature | Documents expected argument and attribute types. |
copy.copy(obj)function | Shallow copy without calling __init__. |
Initializing Objects code example
The script below builds a User class with strict validation and two alternative constructors.
# Lesson: Initializing Objects
from dataclasses import dataclass, field
@dataclass
class User:
username: str
email: str
roles: list[str] = field(default_factory=list)
def __post_init__(self) -> None:
if not self.username.isidentifier():
raise ValueError(f"invalid username: {self.username!r}")
if "@" not in self.email:
raise ValueError(f"invalid email: {self.email!r}")
@classmethod
def from_dict(cls, payload: dict) -> "User":
return cls(
username=payload["username"],
email=payload["email"],
roles=list(payload.get("roles", [])),
)
@classmethod
def anonymous(cls) -> "User":
return cls(username="anon", email="anon@example.com")
u = User("ana", "ana@example.com", roles=["admin"])
print(u)
# Alternative constructors
v = User.from_dict({"username": "ben", "email": "ben@ex.com", "roles": ["viewer"]})
print(v)
w = User.anonymous()
print(w)
# Validation fires in __post_init__
try:
User("123bad", "x@y")
except ValueError as err:
print("rejected:", err)
# Default list is per-instance (thanks to default_factory)
a = User("ana", "a@b")
b = User("ben", "b@c")
a.roles.append("admin")
print("a.roles, b.roles:", a.roles, b.roles)
Highlights:
1) `default_factory=list` avoids the shared-mutable-default trap.
2) `__post_init__` runs after dataclass fills in fields; perfect for validation.
3) `from_dict` is a classmethod so it also works for any future subclass.
4) `anonymous` is another alternative constructor; the main `__init__` stays simple.
Build a class with one alternative constructor.
class Point:
def __init__(self, x: float, y: float):
self.x, self.y = x, y
@classmethod
def origin(cls) -> "Point":
return cls(0, 0)
@classmethod
def from_tuple(cls, xy: tuple[float, float]) -> "Point":
return cls(*xy)
def __repr__(self) -> str:
return f"Point({self.x}, {self.y})"
print(Point.origin())
print(Point.from_tuple((3, 4)))
Verify per-instance defaults.
from dataclasses import dataclass, field
@dataclass
class D:
items: list = field(default_factory=list)
a, b = D(), D()
a.items.append(1)
assert a.items == [1] and b.items == []
assert a.items is not b.items
Running prints:
User(username='ana', email='ana@example.com', roles=['admin'])
User(username='ben', email='ben@ex.com', roles=['viewer'])
User(username='anon', email='anon@example.com', roles=[])
rejected: invalid username: '123bad'
a.roles, b.roles: ['admin'] []