would be enough? I’m absolutely not a typing expert, but my understanding is that variable types are ignored at runtime altogether, so there would be no runtime effect. Type checkers obviously would need to be enhanced, but inferring literal values feels like a relatively simple task.
This case covers a really narrow case where it’s a typing.Literal with a single value. Plenty of people (including me) use typing.Literal to type a finite set of constants instead:
class Button(BaseModel):
label: str
size: Literal["small", "medium", "large"] = "medium"
I think what you actually want is typing.Final:
from typing import Final, reveal_type, Literal
from enum import StrEnum
mode: Final = "read"
reveal_type(mode)
class Token:
NAME: Final = "NAME"
ARGUMENT: Final = "ARGUMENT"
reveal_type(Token.NAME)
reveal_type(Token.ARGUMENT)
Type checkers would then automatically infer the literal value. Here’s pyright for example but mypy, pyrefly, and ty all agree on this.
Type of "mode" is "Literal['read']"
Type of "Token.NAME" is "Literal['NAME']"
Type of "Token.ARGUMENT" is "Literal['ARGUMENT']"
For constants like this, you should use StrEnum which is well supported by all typecheckers (along with any other enum.Enum).
Thanks for the Final recommendation and explanation. It probably will work well in general and it will work especially well with constants. The only downside I see is that, at least for a casual typing user like me, it isn’t obvious that if a function accept Literal, you can actually pass Final to it as well. This may be covered in some typing documentation, but the API docs of Final and Literal don’t mention the connection at all.
Although supporting
a: Literal = "a"
wouldn’t add any new functionality, adding support for it ought to also be trivial when
a: Final = "a"
is already supported. Possibly the former could be an alias for the latter, but having more than one way to spell the same thing would have its own downsides as well. Probably enhancing typing docs would be the best way to clarify this.
The proposed StrEnum is certainly a good option in various situations, but changing a large code base that currently uses just strings to use it is a rather large task and I don’t see enough benefits over Literal/Final. Another problem is that StrEnum requires Python 3.11 and we support also older versions.
There are more details about Final here, in the typing spec: Type qualifiers — typing documentation but this actually doesn’t seem to mandate that the Final trick must work.
The idea behind the Final trick is that if you write something like
x: Final = 1
the type checker can infer the narrowest possible type (because the value of x will never change), which in this case is Literal[1].
However, the typing spec currently says this:
With no type annotation. Example:
ID: Final = 1
The typechecker should apply its usual type inference mechanisms to determine the type of ID (here, likely, int).
But it shouldn’t infer int. It should infer Literal[1]…
And I know you said you don’t want to re-write your whole code (which is completely understandable), but I’d say the long-term proper solution to this is probably sentinels from PEP 661, which was recently accepted for Python 3.15, but there will also be a backport to older Pythons.