spec: clarify interaction of Final and dataclass by carljm · Pull Request #1669 · python/typing (original) (raw)

See discussion thread at https://discuss.python.org/t/treatment-of-final-attributes-in-dataclass-likes/47154 and issue at python/cpython#89547

Consider a dataclass with a Final-annotated initialized assignment in the class body:

@dataclass
class C:
    x: Final[int] = 3

This is currently under-specified in the typing spec. Neither PEP 591 or PEP 681 clearly specified this composition.

There are two possible consistent interpretations:

  1. It could be a class-only variable (implicit ClassVar) C.x with final value 3. In this interpretation, it should not be a dataclass field, because dataclass fields are inherently set on instances, they cannot be ClassVar. This is the interpretation suggested by PEP 591 (and thus also the current typing spec for Final), which say that Final with an assigned value in a class body is always implicitly a ClassVar.
  2. It could be a dataclass field x with default value 3, which cannot be reassigned on an instance after it is initialized in the generated __init__ method.

This pull request specifies option 2.

I will also provide an update to the conformance suite for this, if the Typing Council accepts the spec change.

Current runtime behavior

The stdlib dataclasses module considers x in this example to be a dataclass field.

If the assigned value is a default value (as in the example above), dataclasses leaves that default value in place as a class attribute (so x is a class-and-instance variable, and C.x == 3.) If the assigned value is a field(...) call, dataclasses does not leave any attribute x on the class at all, so x is purely a dataclass field / instance variable.

Current type-checker behavior

Playground links:

mypy
pyre
pyright

All three agree with the runtime behavior of dataclasses, considering x to be a dataclass field which is included in the dataclass __init__ method and set on instances in __init__. All three prohibit further assignments to x on instances, noting that it is a final attribute. Pyright also mentions that it is a ClassVar, which is confusing given that "ClassVar" and "dataclass field" are (or should be) mutually exclusive.

All three assume that C.x is available and of type int, whether we have x: Final[int] = 3 or x: Final[int] = field(default_value=3). That is, none of the type checkers model the actual runtime behavior that field() objects are removed from the class and not replaced with anything. (Arguably this behavior is just a bug/inconsistency in the dataclasses runtime implementation.)

Considerations