Skip to content

Fix: Python dataclass Mutable Default Value Error (ValueError / TypeError)

FixDevs · (Updated: )

Part of:  Python Errors

Quick Answer

How to fix Python dataclass mutable default errors — why lists, dicts, and sets cannot be default field values, how to use field(default_factory=...), and common dataclass pitfalls with inheritance and ClassVar.

The Error

Defining a dataclass with a mutable default value raises:

from dataclasses import dataclass

@dataclass
class Config:
    tags: list = []  # ← Error here

# ValueError: mutable default <class 'list'> for field tags is not allowed:
# use default_factory

Or with a dict:

@dataclass
class Request:
    headers: dict = {}

# ValueError: mutable default <class 'dict'> for field headers is not allowed:
# use default_factory

Or, in Python 3.11+ with certain patterns:

TypeError: unhashable type: 'list'

Why This Happens

Python dataclasses prohibit mutable objects (lists, dicts, sets, custom objects) as direct default values. The reason is the same problem as Python’s infamous mutable default argument bug:

# The problem without dataclasses — same object shared across all instances
class Config:
    def __init__(self, tags=[]):  # Same list object for every instance!
        self.tags = tags

a = Config()
b = Config()
a.tags.append('python')
print(b.tags)  # ['python'] — b was unexpectedly modified!

Dataclasses detect this problem at class definition time and raise an error rather than silently sharing state. The fix is field(default_factory=...), which creates a new object for each instance.

The check works by inspecting each field’s default value at class-creation time. If the default is a list, dict, or set (any of which fail isinstance(x, (Hashable,))-style heuristics in CPython’s source), the dataclass machinery refuses to define the class. The error is loud and immediate — you cannot ship code with this bug because the module never imports. That is the design intent.

But the check is not exhaustive. Custom mutable classes, dataclass instances, and any object that does not match the list/dict/set heuristic slip past the check. In Python 3.11 and later, the check was extended to detect more cases (including instances of any mutable user-defined class via __hash__). Before 3.11, a custom mutable default would be accepted and silently shared across every instance — exactly the kind of bug that ships to production undetected, surfaces as “user A is seeing user B’s data,” and takes hours to track down.

In Production: Incident Lens

How the incident surfaces. The strict ValueError form is a deploy-time blocker — the application fails to import, the worker fails to start, and the orchestrator notices a startup health check failure. That is the lucky version. The dangerous version is the silent shared-state form (custom mutable class before 3.11, mutable instance attribute set in __post_init__, or any path that smuggles a shared object past the dataclass machinery): the app runs cleanly, requests succeed, but instance state leaks across requests. Symptoms include “every request seems to see the previous user’s data” or “a list grows by one item every time we deploy” — both are typical signatures of accidental class-level mutable state surviving across instances.

Blast radius. The strict-error case is bounded to “deploy blocked.” The silent-shared-state case is a data confidentiality incident: every user of the affected endpoint can see (or modify) data that belonged to a previous user. Even if the actual data leaked is innocuous (a feature flag list, a UI preference), the security review takes the same shape as any cross-tenant data exposure. The blast widens further if the shared mutable state is a permission list, a cart, or an audit log — at that point you have a critical incident with regulatory implications.

The monitoring signal that catches it. Static analysis catches it pre-merge: mypy --strict, pylint, and ruff all flag mutable defaults (rule B008 in flake8-bugbear, PLW0102 in pylint, B006 in ruff). The runtime signal for the silent variant is harder — alert on cross-user data correlation in support tickets (“user reports seeing wrong data”) or on a unit test that asserts “two fresh instances have independent state.” For ORMs (SQLAlchemy, Django), the equivalent bug is “default=” with a callable vs a value, and the production fingerprint is “values that should be fresh but keep growing.”

Recovery sequence. First, the strict-error variant is a fix-forward: change tags: list = [] to tags: list = field(default_factory=list) and redeploy. There is no rollback to consider because the broken code never reached steady state. Second, the silent-shared-state variant is harder. If you find it post-deploy, audit recent code for any dataclass with __post_init__ that calls self.x = self.x style copying, any base class with class-level mutable attributes, and any factory that returns the same object reference. Hot-fix by switching to default_factory. Then audit the data: query for affected rows that may have been corrupted by leaked state, mark them for manual review, and notify affected users if the leak crossed tenant boundaries.

Postmortem-style preventive. The durable controls: (1) ruff (or flake8-bugbear) with B006 enabled in pre-commit + CI to catch mutable defaults on regular functions, which surfaces the same bug class outside dataclasses; (2) a unit test that instantiates each dataclass twice and asserts instance_a.mutable_field is not instance_b.mutable_field; (3) prefer frozen=True on dataclasses by default — frozen instances cannot be mutated, eliminating the shared-state failure mode entirely; (4) for ORM models with collection fields, explicitly use default_factory=list or default=lambda: [], never bare []; (5) Python 3.11+ minimum baseline so the language’s built-in detection covers more cases.

Fix 1: Use field(default_factory=…) for Mutable Defaults

from dataclasses import dataclass, field

# Before — raises ValueError
@dataclass
class Config:
    tags: list = []
    metadata: dict = {}
    aliases: set = set()

# After — correct
@dataclass
class Config:
    tags: list = field(default_factory=list)        # Creates [] for each instance
    metadata: dict = field(default_factory=dict)    # Creates {} for each instance
    aliases: set = field(default_factory=set)       # Creates set() for each instance

With type hints (recommended):

from dataclasses import dataclass, field
from typing import Any

@dataclass
class ServerConfig:
    host: str = 'localhost'
    port: int = 8080
    allowed_hosts: list[str] = field(default_factory=list)
    headers: dict[str, str] = field(default_factory=dict)
    options: dict[str, Any] = field(default_factory=dict)

# Each instance gets its own fresh list/dict
config1 = ServerConfig()
config2 = ServerConfig()
config1.allowed_hosts.append('example.com')
print(config2.allowed_hosts)  # [] — not affected

Pre-populate with default values using a lambda:

@dataclass
class APIClient:
    # Create a new list with default values for each instance
    base_headers: dict[str, str] = field(
        default_factory=lambda: {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        }
    )
    retry_codes: list[int] = field(default_factory=lambda: [429, 500, 502, 503])
    timeout: float = 30.0

Pro Tip: Use field(default_factory=lambda: [...]) when you want a mutable default that is pre-populated. Use field(default_factory=list) when you just want an empty list. The lambda creates a new copy of the default on every instantiation.

Fix 2: Fix Nested Dataclass Defaults

When a field’s default value is another dataclass instance (which is mutable):

@dataclass
class DatabaseConfig:
    host: str = 'localhost'
    port: int = 5432

@dataclass
class AppConfig:
    # Wrong — same DatabaseConfig instance shared across all AppConfig instances
    db: DatabaseConfig = DatabaseConfig()  # ValueError in Python 3.11+
                                           # Silently shared in earlier versions!

    # Correct — create a new DatabaseConfig for each AppConfig
    db: DatabaseConfig = field(default_factory=DatabaseConfig)

For nested dataclasses with custom defaults:

@dataclass
class AppConfig:
    db: DatabaseConfig = field(
        default_factory=lambda: DatabaseConfig(host='db.internal', port=5432)
    )

Fix 3: Fix ClassVar vs Instance Variables

If you want a class-level attribute (shared across all instances), use ClassVar — it is excluded from __init__ and not subject to the mutable default restriction:

from dataclasses import dataclass
from typing import ClassVar

@dataclass
class Registry:
    name: str
    value: int = 0

    # ClassVar — shared across all instances (not in __init__)
    # Mutable ClassVar is allowed — it's intentionally shared
    _instances: ClassVar[list['Registry']] = []
    _registry: ClassVar[dict[str, 'Registry']] = {}

    def __post_init__(self):
        Registry._instances.append(self)
        Registry._registry[self.name] = self

r1 = Registry('alpha', 1)
r2 = Registry('beta', 2)
print(Registry._instances)  # [Registry(name='alpha'), Registry(name='beta')]

Fix 4: Use post_init for Complex Initialization

When the default value depends on other fields or requires complex logic:

from dataclasses import dataclass, field
from pathlib import Path

@dataclass
class Project:
    name: str
    base_dir: Path = Path('.')

    # These cannot be set directly as defaults because they depend on other fields
    source_dir: Path = field(init=False)
    output_dir: Path = field(init=False)
    tags: list[str] = field(default_factory=list)

    def __post_init__(self):
        # Compute dependent fields after __init__ runs
        self.source_dir = self.base_dir / 'src'
        self.output_dir = self.base_dir / 'dist'

        # Validate fields
        if not self.name:
            raise ValueError('Project name cannot be empty')

        # Normalize types
        if isinstance(self.base_dir, str):
            self.base_dir = Path(self.base_dir)

project = Project(name='my-app', base_dir=Path('/projects/my-app'))
print(project.source_dir)  # /projects/my-app/src

Fix 5: Fix Dataclass Inheritance Issues

Dataclass inheritance has a specific limitation — subclasses cannot define fields without defaults if the parent class has fields with defaults:

@dataclass
class Base:
    name: str = 'default'   # Field with default

@dataclass
class Child(Base):
    age: int  # ← TypeError: non-default argument 'age' follows default argument

Fix — give the child field a default too, or restructure:

# Option A — give child field a default
@dataclass
class Child(Base):
    age: int = 0  # Now has a default

# Option B — put required fields in the parent
@dataclass
class Base:
    name: str  # Required — no default

@dataclass
class Child(Base):
    age: int   # Also required — no default
    extra: list = field(default_factory=list)  # Optional with default

child = Child(name='Alice', age=30)

# Option C — use field(kw_only=True) in Python 3.10+
@dataclass
class Base:
    name: str = 'default'

@dataclass
class Child(Base):
    age: int = field(kw_only=True)  # Keyword-only — avoids ordering conflict

child = Child(age=30)  # name uses default

Fix 6: Fix Frozen Dataclasses with Mutable Fields

Frozen dataclasses (frozen=True) cannot be modified after creation — but they can still contain mutable objects:

from dataclasses import dataclass, field

@dataclass(frozen=True)
class ImmutableConfig:
    name: str
    values: tuple = ()       # Use tuple (immutable) instead of list
    options: frozenset = field(default_factory=frozenset)  # frozenset instead of set

# Attempting to modify raises FrozenInstanceError
config = ImmutableConfig(name='test')
config.name = 'changed'     # FrozenInstanceError: cannot assign to field 'name'

# But the contained list IS still mutable (if you used list):
@dataclass(frozen=True)
class BadFrozen:
    items: list = field(default_factory=list)

bad = BadFrozen()
bad.items.append(1)  # No error — list itself is mutable even if the reference is frozen
# Use tuple for truly immutable sequences in frozen dataclasses

Fix 7: Convert to/from Dict and JSON

from dataclasses import dataclass, field, asdict, astuple
import json

@dataclass
class User:
    id: int
    name: str
    email: str
    roles: list[str] = field(default_factory=list)
    metadata: dict = field(default_factory=dict)

user = User(id=1, name='Alice', email='[email protected]', roles=['admin'])

# Convert to dict
user_dict = asdict(user)
print(user_dict)
# {'id': 1, 'name': 'Alice', 'email': '[email protected]', 'roles': ['admin'], 'metadata': {}}

# Convert to JSON
user_json = json.dumps(asdict(user))

# Create from dict
data = {'id': 2, 'name': 'Bob', 'email': '[email protected]'}
user2 = User(**data)  # Works — roles and metadata use defaults

For nested dataclasses, asdict recursively converts:

@dataclass
class Address:
    city: str
    country: str = 'US'

@dataclass
class Person:
    name: str
    address: Address

person = Person(name='Alice', address=Address(city='New York'))
print(asdict(person))
# {'name': 'Alice', 'address': {'city': 'New York', 'country': 'US'}}

Still Not Working?

Check Python version. Some dataclass features (kw_only, slots) require Python 3.10+. The match statement for dataclasses requires Python 3.10+:

python --version

Use dataclasses.fields() to inspect a dataclass at runtime:

from dataclasses import dataclass, field, fields

@dataclass
class Config:
    name: str
    values: list = field(default_factory=list)

for f in fields(Config):
    print(f.name, f.type, f.default, f.default_factory)

Consider attrs or Pydantic for more complex validation needs:

# Pydantic — dataclass alternative with built-in validation
pip install pydantic

from pydantic.dataclasses import dataclass  # Drop-in replacement with validation

@dataclass
class User:
    name: str
    age: int
    tags: list[str] = []  # Pydantic handles mutable defaults automatically

user = User(name='Alice', age=30)  # Works — no ValueError

For related Python issues, see Fix: Python TypeError Missing Required Argument, Fix: Python AttributeError NoneType Has No Attribute, Fix: Python KeyError, and Fix: Python TypeError NoneType Not Subscriptable.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles