Initializing Objects

__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.

ToolPurpose
__init__(self, ...)
method
Sets up instance state right after creation.
@classmethod
decorator
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 hints
feature
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'] []