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.
| Type checkers should treat uses of a final name that was initialized | ||
| with a literal as if it was replaced by the literal. For example, the | ||
| following should be allowed:: | ||
|
|
||
| from typing import NamedTuple, Final | ||
|
|
||
| X: Final = "x" | ||
| Y: Final = "y" | ||
| N = NamedTuple("N", [(X, int), (Y, int)]) |
There was a problem hiding this comment.
This got removed completely as it's redundant.
There was a problem hiding this comment.
I don't think this is redundant or should be removed. The position of X and Y in this NamedTuple declaration is not an ordinary "call expects a literal type" position; it's syntax-sensitive type-factory metadata that is special-cased by type checkers; they must be able to understand as describing a specific field name. I don't think the rules below are sufficient to clarify that this example must work.
Also the conformance tests still quote this deleted paragraph.
There was a problem hiding this comment.
I've restored this section for now, although it's really vague and ambiguous. That makes it sound as if Final works like C's #define. Maybe the best idea would be to have a dedicated "Interaction with NamedTuple" section, since this seems to be a special case.
| 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.
|
|
||
| The typechecker should apply its usual type inference mechanisms to | ||
| determine the type of ``ID`` (here, likely, ``int``). Note that unlike for | ||
| The typechecker should apply the inference mechanisms |
There was a problem hiding this comment.
The rules below don't attempt to cover every possible right-hand-side of an assignment to a Final annotated attribute; they are specific to valid literal values. So I think we still need the "typechecker should apply its usual type inference mechanisms" default fallback language here, with the literal rules below as additional constraints. I don't think we can just refer to the rules below as though they fully specify inference of all Final assignments.
Currently this seems to drop any requirement for type checkers to support e.g. x: Final = C() or x: Final = ['a', 'b'], because those right-hand-sides are not valid Literal values.
| Type checkers should treat uses of a final name that was initialized | ||
| with a literal as if it was replaced by the literal. For example, the | ||
| following should be allowed:: | ||
|
|
||
| from typing import NamedTuple, Final | ||
|
|
||
| X: Final = "x" | ||
| Y: Final = "y" | ||
| N = NamedTuple("N", [(X, int), (Y, int)]) |
There was a problem hiding this comment.
I don't think this is redundant or should be removed. The position of X and Y in this NamedTuple declaration is not an ordinary "call expects a literal type" position; it's syntax-sensitive type-factory metadata that is special-cased by type checkers; they must be able to understand as describing a specific field name. I don't think the rules below are sufficient to clarify that this example must work.
Also the conformance tests still quote this deleted paragraph.
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 |
|
|
||
| bare2: Final = 1 + 2 # may infer bare2 as Final[Literal[3]] or Final[int] | ||
|
|
||
| * In all other cases, type checkers should use standard inference rules. |
There was a problem hiding this comment.
This is technically not correct, because immutable containers are just fine inferring literals, e.g.
x: Final = (1,)
should IMO infer tuple[Literal[1]]. It's at least what Mypy and Pyright do. I haven't checked the other type checkers.
Finalsection.Finalsection.I've decided to move the interaction section to the "qualifiers" chapters,
since
Literalseems much closer linked toFinalthan vice versa.Closes: #2351