June 2, 2017·5 min read·engineeringpythondataclassesproposals

Python data classes begin with annotated fields

The early data-class prototype uses Python variable annotations to generate ordinary class methods, with defaults and equality still under active design.

A class that primarily stores values often repeats its field names in several methods. Construction assigns them, representation prints them, and comparison decides which ones matter. Repeating that information creates several descriptions of the same object that must change together. The early Python data-class proposal uses variable annotations to identify fields once and generate those routine methods from the declaration.

Eric V. Smith's June 2 design draft defines a data class through fields specified with PEP 526 annotations. The accompanying prototype implementation makes the idea concrete. This is a developing proposal, not a module supplied by the current Python standard library. Its open questions matter as much as the short examples the implementation permits.

I like the use of ordinary class declarations here. Python's object protocols are a reason to use the language, and generating the relevant methods can reduce repetitive code while preserving a recognizable object. The value depends on the generated behavior remaining easy to explain.

The declaration supplies the fields

The draft starts with annotated class variables. The prototype reads the class's __annotations__, obtains defaults from class attributes, and assembles field records. A decorator then generates methods and attaches them to the class. For the June 2 prototype, imported from its local dataclass.py file, a small example looks like this.

from dataclass import dataclass

@dataclass
class Point:
    x: int
    y: int

assert Point(2, 3) == Point(2, 3)

The example depends on the prototype, so the import is singular dataclass. It illustrates the current implementation's field and comparison behavior without promising that this spelling is a settled public API. The class body is still the place to add methods and a docstring, both of which the design draft explicitly wants to support.

The generated initializer receives an argument for each included field and assigns the values to the instance. The prototype constructs function source and executes it to produce the method. It therefore generates a method for the particular field list instead of routing every construction through one generic dictionary-based initializer. The source makes the generated assignment behavior inspectable.

Annotations identify fields, but this implementation does not insert runtime checks enforcing the annotated types during assignment. That distinction is visible in the initializer generator, which assigns argument values without comparing their types to the annotation. A concise declaration and a runtime validation framework solve different problems. The draft separately asks whether conversion and validation belong in an extension mechanism.

Equality should respect the class

The design draft explicitly rejects deriving these objects from tuples or lists. Its example distinguishes a two-dimensional point from a time value containing the same pair of numbers. Two objects can contain equal field values while representing different concepts, and a record mechanism should preserve that distinction when comparing them.

The prototype's generated comparison functions first test whether the other object's class is identical to the current object's class. They compare tuples of field values only after that check. Otherwise they return NotImplemented. This means that field-based comparison is conditional on the kind of object, instead of equating every pair of records with the same positional contents.

That policy becomes especially relevant because the draft also wants inheritance. The implementation collects fields from processed base classes in reverse method-resolution order, then adds the current class's fields. More derived definitions replace earlier field entries. Reusing a declaration through inheritance consequently raises questions about field ordering and initialization as well as which comparisons are meaningful.

The prototype provides separate switches for comparison and hashing. Its default hash=None sets the generated class's __hash__ to None, while an enabled hash option builds a hash from selected fields. The draft wants both value-based immutable records and uses where identity remains significant. These possibilities need explicit choices because generating equality alone does not establish that an instance is an appropriate dictionary key.

Defaults still contain design questions

Default values complicate the apparent simplicity of the field list. The draft says a field without a default cannot follow a field with one, corresponding to the generated initializer's argument ordering. It also asks how to support mutable defaults and whether a factory should create a fresh value for each instance.

The June 2 implementation experiments with copying defaults. When an initializer argument is omitted, the generated code uses the class default, copying built-in lists, dictionaries, and sets through their copy method. Other types can use copy.copy, while a selected set of immutable built-in types avoids copying. An explicitly supplied argument follows the direct-assignment path instead.

This is a specific prototype policy, not a conclusion that every mutable-default question has been settled. The design notes themselves ask whether shared mutable defaults should be supported and argue against silently detecting and copying them. Reading the draft alongside the code shows active experimentation. A reader should not infer a stable contract merely because one implementation currently runs.

I am wary of reducing visible class code by introducing rules that are harder to discover than the code they replace. The field-level options in this prototype at least make some choices explicit. A field can be excluded from representation, comparison, or initialization, so values such as passwords do not have to appear in generated representations merely because they are stored on the object.

The draft also asks how tools should detect data-class instances and whether conversion helpers should be methods or module-level functions. Those choices affect code that consumes records generically. A library reading field metadata needs a supported convention, while a method added to every record can conflict with an application's own method names. The questions follow directly from allowing ordinary classes to participate without requiring a special base class.

The draft identifies compatibility with namedtuple and attrs as non-goals. It wants a small initial feature set with room for extensions, and it leaves several implementation decisions open. One example is slots support, which the prototype handles by creating a new class because slots cannot be added after class creation. That behavior illustrates the remaining work in defining what a decorator may change. The proposal's useful starting point is a field declaration that can drive ordinary methods, while the exact rules for defaults and class transformation still need agreement.