Unify and fixLiteral/Final interaction - #2353
Conversation
- Add an "Inference Rules" subsection to the `Final` section. - Move the inference rules for class vars there. - Remove contradictory paragraph about literal handling from `Final` section. - Merge the "Interactions with Final" section from literals into the new "Inference Rules" section, add a link, and trim slightly.
| Type checkers should infer a final attribute that is initialized in a class | ||
| body as being a class variable, except in the case of :doc:`dataclasses`, where | ||
| ``x: Final[int] = 3`` creates a dataclass field and instance-level final | ||
| attribute ``x`` with default value ``3``; ``x: ClassVar[Final[int]] = 3`` is | ||
| necessary to create a final class variable with value ``3``. In | ||
| non-dataclasses, combining ``ClassVar`` and ``Final`` is redundant, and type | ||
| checkers may choose to warn or error on the redundancy. |
There was a problem hiding this comment.
This got moved to the "Inference Rules" section.
| In the example below, we know that ``foo`` will always be equal to | ||
| exactly ``3``. A type checker can use this information to deduce that ``foo`` | ||
| is valid to use in any context that expects a ``Literal[3]``:: | ||
|
|
||
| def expects_three(x: Literal[3]) -> None: ... | ||
|
|
||
| foo: Final = 3 | ||
| expects_three(foo) # Type checks, since 'foo' is Final and equal to 3 |
There was a problem hiding this comment.
The introductory part was reshuffled a bit from the original section to be a better fit here. The rest is identical.
|
I just noticed that the introductory sentence about "normal inference" is actually important. I will change the PR when I get home. |
|
The spec used to say:
before I replaced that sentence with a link to the new inference rules section. The problem is that this contradicts the explicit guidance in the section I copied over from the literals spec:
My suggestion: We leave this original sentence out for now, reverting back to "left unspecified" for now. But I did plan to open a discuss thread anyway to define a few more supported cases, like |
carljm
left a comment
There was a problem hiding this comment.
I think combining these sections makes sense. Left some inline comments.
I think this is a clarification, not a substantive spec change, so I don't know that we need the full process including DPO post here. But it's a hefty enough rewording / rearrangement that I do think we should try to get at least most of the typing council to approve it.
Update conformance tests accordingly Restore the `NamedTuple` section
|
I've now completely rewritten the inference rules section (except the part finals in class bodies). I think I captured the intent of the various bits and pieces previously scattered around the specs. I'm using concrete language better suited for a spec than the previous "PEP" language that was more explanatory. Personally, I'd prefer the "explicit type" section to just read "Type checkers should use an explicit type if one is provided.", but that would be a direct contradiction to the previous example that type checkers may accept: bar1: Final[int] = 3
expects_three(bar1) # May or may not be accepted by type checkersI've gone with the old vague language for now, but I think there is very much room for improvement here in the future. I've also restored the Finally, I've reworked the conformance tests to follow the reworked section. I've introduced a new way to mark result groups as requiring at least one success (as pyrefly accepts both |
| # > In all other cases, type checkers should use standard inference rules. | ||
|
|
||
| bare3: Final = 3.14 # infer bare3 as Final[float] or Final[float | int] | ||
| assert_type(bare3, float) # E[bare3!]: either float or float | int |
There was a problem hiding this comment.
These exact-type assertions reject checkers that are applying their normal inference, just as the spec requires.
We should just not use float as an example, because the float/int special case is not well specified, so we can't make very many assertions around it that are clearly supported by the spec.
There was a problem hiding this comment.
Inferring Literal[3.14] is against the spec, which explicitly forbids float literals. That said, this is more of an issue that should be covered by the Literal test cases, so changing it to a list is the better option.
| * In all other cases, type checkers should use standard inference rules. | ||
| For example:: | ||
|
|
||
| bare3: Final = 3.14 # infer bare3 as Final[float] or Final[float | int] |
There was a problem hiding this comment.
I think we should stay away from floats here.
I don't actually think it's useful to even include examples for "type checkers should use standard inference rules", I would just leave them out entirely.
There was a problem hiding this comment.
I think examples are still useful as a clarification, although two examples are not really necessary.
| Type checkers should use the following inference rules for ``Final`` with an | ||
| explicit type: |
There was a problem hiding this comment.
| Type checkers should use the following inference rules for ``Final`` with an | |
| explicit type: | |
| Type checkers should use the following inference rules for ``Final[SomeType]`` annotations: |
There was a problem hiding this comment.
This also uses the exact wording of the syntax section, which I think should be used.
There was a problem hiding this comment.
As written, this conflicts with the requirements stated below, because x: Final[int] = 3 is still "a final name that was initialized with a literal", but we explicitly say below that type checkers aren't required to infer x as Literal[3] in that case. So I think we at the very least need an update to the text here to fix that inconsistency.
| Type checkers should treat uses of a name annotated with bare ``Final`` and initialized | |
| with a literal as if it was replaced by the literal. For example, the |
There was a problem hiding this comment.
Maybe "... annotated with a Final where a Literal is inferred"? This would mean consistent handling between these cases, and we don't need to repeat the rules here directly.
| * If the value is a literal value which is a valid parameter for | ||
| ``Literal[...]``, type checkers should infer that ``Literal``. For example:: |
There was a problem hiding this comment.
I think saying "is a literal value" here introduces some ambiguity that wasn't present in the previous wording. Are -4, +1, and Color.RED "literal values"? Unclear; none of them are represented in the AST as a literal value. But they are all valid Literal arguments.
Ideally those cases might also be covered in the tests, though arguably it's fine to leave these edge cases out of the Final tests if the Literal tests cover them, as long as the wording here is clear.
At the same time, we should take care not to imply that Alias = Literal[1]; x: Final = Alias invokes this rule, even though Literal[Alias] would be a valid type expression. The old wording was potentially subject to this mis-interpretation.
I think the clearest option here is probably just to outline the relevant cases explicitly:
| * If the value is a literal value which is a valid parameter for | |
| ``Literal[...]``, type checkers should infer that ``Literal``. For example:: | |
| * If ``value`` is an integer literal (optionally prefixed with unary ``+`` or ``-``), | |
| a string or bytes literal, ``True``, ``False``, ``None``, or an enum-member | |
| reference such as ``Color.RED``, type checkers should infer ``Literal[value]``. | |
| For example:: |
There was a problem hiding this comment.
I don't think we should repeat the literal rules here. If we just slightly reformulate this as "If the value would be a valid parameter for Literal[...], ..." it is unambiguous that -4, +1, and Color.RED are allowed.
There was a problem hiding this comment.
I agree that would address the inclarity with -4, +1, and Color.RED. But as mentioned in my comment, Literal[1] is also a valid parameter for Literal[...], and so is Alias, where Alias = Literal[1]. But x: Final = Alias where Alias = Literal[1], and x: Final = Literal[1], do not trigger this rule. So "would be a valid parameter for Literal[...]" seems not sufficiently precise. That's why my suggestion here does not actually just repeat the literal rules, it specifies a subset of them which apply in this case.
Finalsection.Finalsection.I've decided to move the interaction section to the "qualifiers" chapters,
since
Literalseems much closer linked toFinalthan vice versa.Closes: #2351