Skip to content

Unify and fixLiteral/Final interaction - #2353

Open
srittau wants to merge 18 commits into
python:mainfrom
srittau:final-literal
Open

Unify and fixLiteral/Final interaction#2353
srittau wants to merge 18 commits into
python:mainfrom
srittau:final-literal

Conversation

@srittau

@srittau srittau commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator
  • 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.

I've decided to move the interaction section to the "qualifiers" chapters,
since Literal seems much closer linked to Final than vice versa.

Closes: #2351

- 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.
@srittau srittau added the topic: typing spec For improving the typing spec label Aug 31, 2026
Comment thread docs/spec/qualifiers.rst
Comment on lines -162 to -168
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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This got moved to the "Inference Rules" section.

Comment thread docs/spec/qualifiers.rst
Comment thread docs/spec/qualifiers.rst Outdated
Comment on lines +198 to +205
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

@srittau srittau Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The introductory part was reshuffled a bit from the original section to be a better fit here. The rest is identical.

@srittau

srittau commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

I just noticed that the introductory sentence about "normal inference" is actually important. I will change the PR when I get home.

@srittau

srittau commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

The spec used to say:

The typechecker should apply its usual type inference mechanisms to determine the type of ID (here, likely, int)"

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:

Type checkers are not obligated to understand any other uses of Final. For
example, whether or not the following program type checks is left unspecified: [...]

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 X: Final = cls(), Y: Final = 3.2, and remove the ambiguity surrounding Z: Final[int] = 3.

@carljm carljm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/spec/qualifiers.rst Outdated
Comment thread conformance/tests/qualifiers_final_annotation.py Outdated
Comment thread docs/spec/qualifiers.rst
Comment thread docs/spec/qualifiers.rst Outdated
Comment thread docs/spec/qualifiers.rst Outdated
Comment thread conformance/tests/qualifiers_final_annotation.py Outdated
Update conformance tests accordingly

Restore the `NamedTuple` section
@srittau

srittau commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

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 checkers

I'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 NamedTuple section in a dedicated "Interaction with NamedTuple" section and reworded it to apply to NamedTuples only for now, since the previous language was extremely vague. Please let me know if there are cases missing that should be added.

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 assert_type(X, float) and assert_type(X, float | int). I will extract that into a separate PR. The reworked conformance tests reveal a few instances of type checkers being more liberal in what type of literals they support (especially Literal[3.14], but pycroscope also accepts Literal[range(0, 3)]. I'm not sure what the best approach here is.

Comment thread docs/spec/qualifiers.rst Outdated
# > 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/spec/qualifiers.rst Outdated
Comment thread docs/spec/qualifiers.rst
Comment thread conformance/src/main.py Outdated
Comment thread docs/spec/qualifiers.rst
Comment thread docs/spec/qualifiers.rst Outdated
* 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]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think examples are still useful as a clarification, although two examples are not really necessary.

Comment thread docs/spec/qualifiers.rst Outdated
Comment thread docs/spec/qualifiers.rst
Comment on lines +226 to +227
Type checkers should use the following inference rules for ``Final`` with an
explicit type:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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:

@srittau srittau Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also uses the exact wording of the syntax section, which I think should be used.

Comment thread docs/spec/qualifiers.rst Outdated
Comment on lines 187 to 188

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docs/spec/qualifiers.rst Outdated
Comment on lines +209 to +210
* If the value is a literal value which is a valid parameter for
``Literal[...]``, type checkers should infer that ``Literal``. For example::

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
* 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::

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@carljm carljm Sep 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

topic: typing spec For improving the typing spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Finals and Literals: Spec contradiction

4 participants