spec: clarify interaction of Final and dataclass#1669
spec: clarify interaction of Final and dataclass#1669JelleZijlstra merged 2 commits intopython:mainfrom
Conversation
|
This will likely affect attrs too so I'm following the discussion. I'm personally rooting for option 2. A small nit:
That only seems to be true for non-slots dataclasses. If the dataclass is created using |
…rked `ClassVar`) within dataclass class bodies. This is consistent with the runtime and [this proposed change to the typing spec](python/typing#1669).
|
Thanks for the PR. Personally, I agree that option 2 is the preferred option, and I like the wording in the proposed typing spec change. I also agree that pyright's current error message is misleading (doesn't match the runtime behavior), so I've updated pyright accordingly so it doesn't mention If you don't get any further feedback on the PR within the next few days, the next step is to create an issue in the typing council repo asking for a formal decision about the spec update. If this is approved, we'll want to augment the compliance tests to cover the new cases. This could be done as part of the same PR or a separate PR. |
|
(btw, I am still planning to add conformance tests for this) |
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:
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:
It could be a class-only variable (implicit
ClassVar)C.xwith final value3. 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.It could be a dataclass field
xwith default value3, 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
xin 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
xis a class-and-instance variable, andC.x == 3.) If the assigned value is afield(...)call, dataclasses does not leave any attributexon the class at all, soxis 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
xto be a dataclass field which is included in the dataclass__init__method and set on instances in__init__. All three prohibit further assignments toxon 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.xis available and of typeint, whether we havex: Final[int] = 3orx: Final[int] = field(default_value=3). That is, none of the type checkers model the actual runtime behavior thatfield()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
xis a ClassVar, but otherwise behavior would remain as it is; errors would still be raised on exactly the same lines by all three type checkers.x: Final[int] = 3always means thatxwill always have value exactly3. In dataclass bodies this will be more nuanced: the class attribute will always be3, but it will be shadowed by an instance attribute that may have a different value on any given instance. On the other hand, it is already the case that the semantics of annotated assignments in dataclass bodies differ from other contexts:x: int = field(...)whenfield(...)does not returnintwould obviously be a type error anywhere else. This example illustrates that in dataclasses, the annotation generally does not apply to the immediate assignment, but to the type of the dataclass field (instance var) specified by the annotated assignment.__init__method manually instead of allowing dataclass to generate it, eliminating a key benefit of dataclasses.) If we select Option 2, it remains quite easy to have a final classvar on a dataclass, using aClassVar[Final[...]]annotation.