Back to Blog
Python

How Python Dataclass Default Values Work

python dataclass default values: Learn how Python dataclass default values work, why mutable defaults raise an error, and when to use default_factory instead of default.

pythondataclassesdefault_factorymutable-defaultsfrozen-dataclasses
Illustration of Python dataclass default values showing a shared mutable list versus per-instance default_factory lists.

Python dataclass default values look simple at first: assign a literal to the field and move on. The syntax is straightforward for immutable types:

from dataclasses import dataclass @dataclass class Config: retries: int = 3 timeout: float = 30.0 label: str = "default"

This works because int, float, and str are immutable. The default is evaluated once at class definition time, and every instance that omits the argument shares that same default object. Since the value cannot be mutated, sharing is harmless.

The problem appears the moment you try to use a mutable default like a list or dict:

@dataclass class Task: tags: list = [] # raises ValueError

This raises a ValueError at class definition time, because the default would be shared across all instances — mutating task1.tags would also change task2.tags. Dataclasses reject this rather than letting the bug surface later in production.

Using default_factory for Mutable Values

The fix is field(default_factory=...):

from dataclasses import dataclass, field @dataclass class Task: tags: list = field(default_factory=list)

The factory is called once per instance, so each Task receives its own empty list. You can pass any callable that returns the default value:

from datetime import datetime @dataclass class Task: tags: list = field(default_factory=list) metadata: dict = field(default_factory=dict) created_at: datetime = field(default_factory=datetime.now)

Note that datetime.now is passed as the callable, not datetime.now(). Passing the result of the call would evaluate it once at class definition time, which reintroduces the shared-state problem.

When the Default Is Evaluated

The timing difference is the core of the behavior. field(default=...) evaluates the default once, when the class is defined. field(default_factory=...) evaluates the factory each time an instance is created.

For immutable defaults, the single evaluation is fine because the value cannot be mutated. For mutable defaults, the factory must run per instance to give each object independent state.

There is a subtle interaction with init=False:

@dataclass class Task: created_at: datetime = field(init=False, default_factory=datetime.now) tags: list = field(default_factory=list)

Here created_at is not accepted as a constructor argument, but the factory still runs at instance creation, so each Task records its own creation time without the caller supplying one.

Choosing Between default and default_factory

SituationWhat to use
Immutable value such as int, str, float, tupledefault=...
Mutable value such as list, dict, setdefault_factory=...
Value that must be computed per instancedefault_factory=...
Value intentionally shared across instancesdefault=... (rare, usually a mistake)

A tuple is immutable, so field(default=()) is safe. If you need a list-like default but want to avoid a factory, a tuple can be a simpler alternative, though it changes the field's type.

Common Failure: Passing the Call Result

A frequent mistake is writing field(default_factory=[]) or field(default_factory=datetime.now()). In both cases, the expression is evaluated at class definition time, and the resulting object — a list or a datetime — is stored as the factory. When an instance is created, the dataclass tries to call that object, and Python raises a TypeError such as 'list' object is not callable.

The fix is to pass the callable itself. If you need to parametrize the factory, use a lambda or a named function:

@dataclass class Task: tags: list = field(default_factory=lambda: ["untagged"])

Lambdas work, but a named factory function is easier to test and reuse when the logic grows beyond a single expression.

Frozen Dataclasses and Defaults

In a frozen dataclass, frozen=True prevents attribute assignment after construction. Defaults still behave the same way, but the shared-state risk is lower because instances cannot reassign their fields. That does not make mutable defaults safe: a frozen dataclass can still contain a list that is mutated through a method, so default_factory remains the correct choice for mutable fields.

Operational and Maintainability Considerations

The choice between default and default_factory affects more than correctness. A factory that performs I/O or heavy computation at instance creation adds cost to every construction. If the default is expensive, consider whether it belongs in the dataclass at all, or whether a lazy property is more appropriate.

Factories also make serialization behavior explicit. If you use asdict() to convert a dataclass to a dict, the default values appear in the output. That is usually fine, but if a default is derived from a global setting, the serialized value may not match the current configuration at deserialization time.

One more maintainability point: do not use a mutable default to mean "shared state on purpose." If two instances must share a list, pass the same list into both constructors rather than relying on a class-level default. The dataclass machinery deliberately prevents the implicit shared default, and working around it with a custom __post_init__ only obscures the intent.

python dataclass default values: Practical Usage and Code Ex | RYUSLOG DEV