Treatment of `Final` attributes in dataclass-likes

I can draft a typing spec change PR for this. At the risk of repeating what I’ve already said on the GitHub issue linked above, here is a summary of the change I will propose, and the rationale for it. (I plan to consider this the pre-requisite discussion thread before filing a typing spec change PR.)

The proposed change to the spec (at Type qualifiers — typing documentation) is to clarify that within a dataclass (or dataclass-like via dataclass_transform) body, just as the int annotation in x: int = field(...) applies to the type of the instance attribute, likewise the Final in x: Final[int] = 3 or x: Final[int] = field(...) also applies to the instance attribute, therefore it does not imply ClassVar, it creates a dataclass field whose instance attribute x cannot be assigned to outside of __init__ (with a default value of 3, in the first case.) This implies that Final[ClassVar[...]] should also be allowed in dataclass bodies, so that it remains possible to specify a final classvar on a dataclass.

I will also probably include an example of this in the Dataclasses section of the spec, in my PR.

The rationale for the spec clarification is as follows:

  1. The proposed behavior matches the existing runtime behavior of the dataclasses library, in which x: Final[int] = 3 creates a normal dataclass field with a default value of 3; it is not treated as a ClassVar. (If it were, that would necessarily imply that the dataclass would not have a field named x, since ClassVars are excluded from consideration as fields.) Changing this runtime behavior would be a serious backwards-compatibility break.

  2. Mypy (playground) and Pyre (playground) both already implement the proposed behavior (and AFAIK always have). Pyright (playground) currently seems a bit unclear about the status of x in this example. It accepts (in checking calls to the dataclass constructor, at least) that x is a field in the dataclass (which it should not be, if it were treated as a ClassVar), and throws an error only on the same line as Mypy and Pyre do (an attempt to re-assign the x attribute on an already-constructed dataclass), but the error message suggests that Pyright believes that x is a ClassVar.

  3. The proposed behavior is more consistent with the general treatment of type annotations in dataclass bodies, in which the annotation always applies to the type of the eventual instance attribute, not to the immediate assignment. (Otherwise x: int = field(...) could not work.) In other words, annotated assignments in a dataclass body form part of the mini-language for specifying the shape of instances of the dataclass; they are not treated in the same way as annotated assignments in a normal class body.

  4. The proposed behavior, in addition to being more consistent and more compatible with the de facto status quo, is also more flexible, because it permits a clear spelling for both final instance attributes and final classvars on dataclasses. The alternative interpretation suggested by the current spec would make it impossible to have a final instance attribute on a dataclass (barring a verbose workaround like hand-writing the __init__ method, which defeats the point of using a dataclass.)

Feedback welcome, before I actually draft the typing spec PR!

6 Likes