Your library supports Python 3.11 to 3.15. You give a field a docstring
with doc=, the tests pass on your laptop's Python 3.14, and the 3.12 job
in CI fails:
from dataclasses import dataclass, field
@dataclass
class Job:
name: str = field(default="", doc="The job's display name.")
# Python 3.12: TypeError: field() got an unexpected keyword argument 'doc'
A failure like this is easy to spot, because it raises an error. This article lists what each version added, then the changes that alter a result without raising anything.
What each version added
| Python | What changed |
|---|---|
| 3.10 | slots=True, kw_only=True, KW_ONLY and a generated __match_args__ |
| 3.11 | weakref_slot=True. Any default without a hash raises ValueError, not only a list, dict or set. slots=True leaves out slots that a base class already has. |
| 3.12 | frozen_default on typing.dataclass_transform, and module= on make_dataclass |
| 3.13 | copy.replace. The generated __eq__ compares fields one at a time. |
| 3.14 | field(doc=...), make_dataclass(decorator=...), annotations that Python evaluates only when code asks for them, and zero-argument super() in a dataclass with slots=True |
| 3.15 | MISSING and KW_ONLY are sentinel objects. Empty field metadata is a frozendict. |
Two of these change how you write code on every version:
- Because of the 3.11 slots change,
__slots__does not list every field. Usefields()to list them. - The 3.11 check rejects only defaults without a hash. A mutable object
that has a hash is still shared by every instance, so give each mutable
default a
default_factory.
If you use a feature from the table, raise your lowest supported version to match, or keep a fallback for the older versions.
Equality changed in 3.13
Python 3.12 and earlier compared two records as tuples of their fields.
A tuple comparison counts an element as equal to itself before it calls
==. From 3.13, the generated __eq__ compares each field with ==.
That changes the result for a value that is not equal to itself, such as
NaN:
from dataclasses import dataclass
@dataclass(order=True)
class Reading:
value: float
nan = float("nan")
left, right = Reading(nan), Reading(nan)
print(left == right, left <= right)
# Python 3.12: True True
# Python 3.13 and later: False True
Ordering still compares tuples, so <= stays True. The example needs
one NaN object in both records. Two separate NaN objects are unequal on
every version.
Check your understanding Your tests pass on Python 3.12. On 3.13, a test that compares two records holding the same NaN object fails. What changed? Show answer Hide answer
From 3.13, the generated __eq__ compares each field with ==, and NaN
is not equal to itself, so the records are unequal. Python 3.12 compared
tuples of fields, and a tuple counts an object as equal to itself without
calling ==. Decide whether your records should treat NaN as equal. If
they should, write __eq__ yourself.
super() in a slotted dataclass
With slots=True, the decorator builds a new class. Before the fix,
zero-argument super() in a method still pointed at the old class, and
calling the method raised TypeError. Python 3.14 has the fix, and so do
late 3.13 releases such as 3.13.14. Python 3.12 still raises. If you
support 3.12, name the class in the call:
from dataclasses import dataclass
class Base:
def label(self) -> str:
return "base"
@dataclass(slots=True)
class Child(Base):
def label(self) -> str:
return super(Child, self).label() + "!"
print(Child().label()) # base!
Building the object works on every version. The error appears only when the method runs, so your tests must call it.
3.15 changed the MISSING marker
dataclasses.MISSING is the marker that a field's default holds when
you gave none. In 3.15 it is a sentinel object, and the private class
dataclasses._MISSING_TYPE is gone. Code that imports that class fails
on 3.15. Check f.default is MISSING instead, which works on every
version.
Test on the oldest and newest version you support
Run your tests in CI on the oldest and the newest Python you support, and
on any version in between whose change you rely on. Test more than
construction. Call inherited methods, compare records, and use
replace() and fields(). The exercise Resolve Dataclass Field Types in
Python 3.14 covers the change to annotations.
Going further
The dataclasses documentation
marks each change with the version that made it. The
Python 3.15 release notes
describe the new sentinel and frozendict built-ins.