Recent chats
ASKGEEZ Q&A GUIDETechnology & devices

Why is a mutable dataclass default rejected, and what pattern should I use?

Quick answer

A list is mutable, so a direct list default can be shared by dataclass instances. Use field(default_factory=list) to have a fresh list created when a default is needed. Python 3.11 changed dataclasses to reject unhashable defaults as an approximation for mutability; the exact rejection behavior depends on Python version. [S1]

LET’S WORK THROUGH IT

The question, the details, the next step.

Question

I wrote this dataclass, and Python rejects it. Why? Is the problem just that the list starts out empty?

from dataclasses import dataclass

@dataclass
class Job:
    tags: list[str] = []

Geez

The problem is not that the list is empty. A list is mutable: code can change it after an instance is created. If instances reused one list as their default, adding a tag through one Job could affect another Job too. Python 3.11 changed dataclasses to reject unhashable defaults as an approximation for mutability. Lists are unhashable, so this definition is rejected in versions with that rule. The check is not a universal test for mutability, and exact rejection behavior depends on Python version. [S1]

Use a factory to create the default when an instance needs it:

from dataclasses import dataclass, field

@dataclass
class Job:
    tags: list[str] = field(default_factory=list)

default_factory takes a zero-argument callable, and list is the documented pattern for a list field. The factory is called when a default value is needed, creating a fresh list for each instance. [S1]

Follow-up question

What would the shared-list bug look like in practice, and how can I check that the corrected version avoids it?

Geez

On a Python version that permits the direct list default, the unwanted behavior could look like this:

first = Job()
second = Job()
first.tags.append("urgent")
print(second.tags)  # Could show ['urgent'] with a shared default

That illustrates the risk; it does not mean current Python will accept the definition. With default_factory=list, check that each instance starts with its own list:

first = Job()
second = Job()
first.tags.append("urgent")

assert first.tags == ["urgent"]
assert second.tags == []

This checks independence of the lists themselves. If you later put mutable objects inside a list, those objects may have their own sharing considerations.

Follow-up question

Can I use a factory for a dictionary or a set, or do I need to create a special function each time?

Geez

You can use a zero-argument callable that creates the desired collection. For plain dictionaries and sets, their types can serve as factories:

from dataclasses import dataclass, field

@dataclass
class Settings:
    options: dict[str, str] = field(default_factory=dict)
    labels: set[str] = field(default_factory=set)

If the default needs initial contents, use a small zero-argument function that returns a newly created value:

def initial_options() -> dict[str, str]:
    return {"mode": "standard"}

@dataclass
class Settings:
    options: dict[str, str] = field(default_factory=initial_options)

A factory is called with no arguments when a default is needed, so this pattern does not pass constructor input into the factory. [S1]

Follow-up question

Can I set both a normal default and default_factory, and what should I review when changing an existing class?

Geez

Do not specify both default and default_factory for the same field; the dataclasses documentation says that combination is an error. [S1] Choose one: an appropriate fixed default value, or a factory when each instance needs a newly created value.

For an existing class, review how it is constructed and how callers use the field. Update the declaration, add a focused independence check if shared state was the concern, and run relevant tests under your project’s Python version. The factory pattern does not by itself establish that a change is compatible with every caller or test.

A practical checklist

  • Identify fields with mutable defaults, such as lists, dictionaries, or sets.

  • Use field(default_factory=...) when each instance needs a newly created value.

  • Use a zero-argument callable that returns the desired default.

  • Do not specify both default and default_factory for one field.

  • Check that changing one instance’s collection leaves another instance’s collection unchanged.

  • Run relevant tests with the Python version used by your project.

A few more useful answers

Why might the same definition work on an older Python version?

Python 3.11 changed the check to reject unhashable defaults as an approximation for mutability. Older versions may allow a direct list default, even though sharing it between instances remains a risk. [S1]

Does the unhashable-default check detect every mutable value?

No. The documentation describes it as an approximation for mutability, not a universal test. Use a factory when each instance needs its own mutable value. [S1]

Can a factory create a collection with custom initial contents?

Yes. Use a zero-argument callable that returns the desired default value, such as a function that creates a dictionary with initial entries. [S1]

Sources

References used for this guide.

  1. [S1] Python dataclasses — field defaults and default_factory
    docs.python.org · Checked

YOUR QUESTION. YOUR DETAILS.

Let’s work through your situation.

Get a tailored answer or help with your next task. Starts at $1, with a quote before you pay.

Ask your own question
← Back to Geez at Work