Fix: Python dataclass Mutable Default Value Error (ValueError / TypeError)
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_factoryOr with a dict:
@dataclass
class Request:
headers: dict = {}
# ValueError: mutable default <class 'dict'> for field headers is not allowed:
# use default_factoryOr, 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 instanceWith 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 affectedPre-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.0Pro Tip: Use
field(default_factory=lambda: [...])when you want a mutable default that is pre-populated. Usefield(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/srcFix 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 argumentFix — 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 defaultFix 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 dataclassesFix 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 defaultsFor 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 --versionUse 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 ValueErrorFor 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Kafka Consumer Not Receiving Messages, Connection Refused, and Rebalancing Errors
How to fix Apache Kafka issues — consumer not receiving messages, auto.offset.reset, Docker advertised.listeners, max.poll.interval.ms rebalancing, MessageSizeTooLargeException, and KafkaJS errors.
Fix: Python Packaging Not Working — Build Fails, Package Not Found After Install, or PyPI Upload Errors
How to fix Python packaging issues — pyproject.toml setup, build backends (setuptools/hatchling/flit), wheel vs sdist, editable installs, package discovery, and twine upload to PyPI.
Fix: Celery Beat Not Working — Scheduled Tasks Not Running or Beat Not Starting
How to fix Celery Beat issues — beat scheduler not starting, tasks not executing on schedule, timezone configuration, database scheduler, and running beat with workers.
Fix: OpenTelemetry Not Working — Traces Not Appearing, Spans Missing, or Exporter Connection Refused
How to fix OpenTelemetry issues — SDK initialization order, auto-instrumentation setup, OTLP exporter configuration, context propagation, and missing spans in Node.js, Python, and Java.