Fix: Python dataclass Mutable Default Value Error (ValueError / TypeError)
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.
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 and Fix: Python AttributeError NoneType Has No Attribute.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Flask Route Returns 404 Not Found
How to fix Flask routes returning 404 — trailing slash redirect, Blueprint prefix issues, route not registered, debug mode, and common URL rule mistakes.
Fix: Python requests.get() Hanging — Timeout Not Working
How to fix Python requests hanging forever — why requests.get() ignores timeout, how to set connect and read timeouts correctly, use session-level timeouts, and handle timeout exceptions properly.
Fix: AWS ECS Task Failed to Start
How to fix ECS tasks that fail to start — port binding errors, missing IAM permissions, Secrets Manager access, essential container exit codes, and health check failures.
Fix: Docker Multi-Stage Build COPY --from Failed
How to fix Docker multi-stage build errors — COPY --from stage not found, wrong stage name, artifacts not at expected path, and BuildKit caching issues.