Skip to content
← Back to explainers

Python explainer

Dataclass Changes Across Python Versions

About 4 min to read. Allow extra time to work through examples.

See which dataclass features each Python version from 3.10 to 3.15 added, and which changes alter results without raising an error.

This article uses Python. You can make it your practice mode to browse related content across the site.

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. Use fields() 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

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.

step 28 of 28

Modelling Data with Dataclasses

You've reached the last step

Revisit the track to review its lessons and exercises, or explore another topic.

Explore more tracks