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.
| 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.
| expects_one(ID4) # E?: May or may not be accepted by type checkers | ||
|
|
||
| ID5: Final = range(1) | ||
| assert_type(ID5, range) # E?: May or may not be inferred by type checkers |
There was a problem hiding this comment.
All three of these tests are kind of useless as conformance tests, but the first two I don't mind, since I think they still provide useful clarification that this behavior is not specified either way. But this third one is a really strange test, since we assert that range(1) may or may not be inferred as range -- which is not even a literal type anyway. So it really feels like this test is saying nothing at all. I would just remove it.
| 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.
| ``Literal[...]``, type checkers should understand that ``var`` may be used in | ||
| any context that expects a ``Literal[value]``. | ||
|
|
||
| Type checkers are not obligated to understand any other uses of Final. For |
There was a problem hiding this comment.
I find this sentence confusing, so if we're clarifying here, I think we should clarify it. Obviously type checkers are generally expected to understand any use of Final as requiring finality (i.e. not accepting later reassignments). So why would we say they are "not obligated to understand any other uses of Final"? It seems like what we really mean is more like this:
| Type checkers are not obligated to understand any other uses of Final. For | |
| Type checkers are not obligated to understand any other uses of Final as implying a Literal type. For |
| Type checkers are expected to | ||
| support this shortcut. Specifically, given a variable or attribute assignment | ||
| of the form ``var: Final = value`` where ``value`` is a valid parameter for | ||
| ``Literal[...]``, type checkers should understand that ``var`` may be used in |
There was a problem hiding this comment.
This wording is a bit vague. Should we just say that type checkers must infer Literal[...]?
|
|
||
| ID1: Final = 1 | ||
| assert_type(ID1, Literal[1]) | ||
| expects_one(ID1) |
There was a problem hiding this comment.
This seems redundant with the assert_type.
Finalsection.Finalsection.I've decided to move the interaction section to the "qualifiers" chapters,
since
Literalseems much closer linked toFinalthan vice versa.Closes: #2351