You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Temporary moratorium on literal constants (until the end of 2022) #7258
I've encountered cases where the changes from int to a literal type in stubs causes regressions for mypy users. Here's one (simplified) example:
importloggingclassC:
def__init__(self) ->None:
self.level=logging.INFOdefenable_debug(self) ->None:
# Error: expression has type "Literal[10]", variable has type "Literal[20]"self.level=logging.DEBUG
Here's another example:
importloggingclassBase:
level=logging.INFOclassDerived(Base):
level=logging.DEBUG# Error -- incompatible with base class
Previously the types were int, so no errors were generated. The code in the above examples doesn't look buggy to me, and the behavior of mypy also seems as expected.
There could well be other problematic cases like this. Having more precise types is generally useful, but in the case of literal types, the regressions such as the above may make them not worth the potential false positives (which are hard to predict, since in most use cases literal types are fine). I wonder if there are concrete examples where the literal types bring concrete benefits that could offset the false positives.
There were several false positives like the above in a big internal codebase when using a recent typeshed. Working around the issues is simple by annotating the attributes as int, but this could be quite non-obvious for non-expert users who aren't familiar with literal types (a somewhat advanced typing feature).
Thoughts? I'm leaning towards preferring to only use literal types when there is a concrete use cases where they help, since the fallout from using them seems difficult to predict.
At least in the sort term we should revert in cases where literals cause problems and in the future only use literals where it's unlikely that the literal gets assigned to a mutable variable.
In the long term we should rethink how type checkers handle literals. For example,
FOO: Literal[1] =1BAR: Literal[2] =2x=FOO# `x` should probably have the type `int`, not `Literal[1]`
In #7259 pytype currently fails with "ParseError: Default value for CRITICAL: $external$typing_extensions.Final can only be '...', got LITERAL(50)", so we'd at least need to fix that and of course flake8-pyi.
This to me feels much more expressive than writing X: Literal[1], so I'd be in favour of allowing this in stubs whatever the case.
I worry, however, that there's not much point in annotating a variable as Literal or Final if it's automatically inferred as something less precise as soon as it's assigned to a different variable.
I worry, however, that there's not much point in annotating a variable as Literal or Final if it's automatically inferred as something less precise as soon as it's assigned to a different variable.
Using Final in the assignment will propagate the literal value:
Y: Final=X
Since a variable with a literal type is effectively a constant, using Final seems logical to me. In Python all variables are mutable by default, so making something effectively final (i.e. literal) but not quite seems a bit unexpected to me. Using literal types is fairly rare, in my experience, so requiring an explicit annotation when something has a literal type seems more usable than requiring an int annotation when something is not a literal (int seems about 100x as common as all literal types, in one big codebase at least). "Make the common case easy and the uncommon case possible" seems to apply here, and it was an important principle when literal types were designed.
I think this is a perfect example of "soft literal" type that I've mentioned several times in different discussions.
So, basically we have two cases:
When we have a "strict literal", like x: Literal[1] = 1. You can't reassign x to anything other than 1
When we have a "soft literal", when value can be changed. Like x: Literal[1]; self.value = x; self.value = 2. I think, that this should not be an error at all
I know that @Akuli was interested in this as well.
Just double-checking that I understand correctly: x: Final = 3 would mean that the type of x is Literal[3], but if we write y = x, the type of y will be int. This is to prevent false positives in mypy caused by assigning a name to multiple Literal values. Is that right?
If so:
No objections to allowing x: Final = 3 in stubs; I think it reads nicely. Changing pytype to not complain about this should be pretty straightforward, too.
However, I don't love the fact that it means something slightly different from x: Literal[3] = 3, and it's not obvious from the annotations which one is stricter than the other, so to speak. If anything, mypy's behavior with the Final annotation makes more sense to me, and I would want it to behave that way for Literal, too.
Reacted by Alex Waygood, Jelle Zijlstra and Nils K
I've encountered cases where the changes from int to a literal type in stubs causes regressions for mypy users.
I'm not seeing any errors when I run mypy on these code samples. Are you saying that mypy infers that the type of self.level is Literal[20] in this case? That's not what I'm seeing.
Type stub files should be as unambiguous as possible. The statement x: Final = 3 tells a type checker that x is read-only, but it doesn't specify its type. A type checker must infer its type in this case. Since type inference rules are not specified in any PEP, this is ambiguous. It's probably safe to say that most (all?) Python type checkers will infer a type of Literal[3] in this case, but why take a chance? Explicit is better than implicit.
So I would recommend against using the x: Final = 3 form in any typeshed stubs.
x: Literal[3] # x is a read/write variable that can be assigned only a value of 3x: Final[Literal[3]] # x is a read-only variable that has a value of 3x: Final=3# x is a read-only variable that most (all?) type checkers will infer to have the type Final[Literal[3]]
Edit: reading this section of PEP 586 makes it pretty clear that x: Final = 3 should be interpreted the same as x: Final[Literal[3]], so I guess it is safe to assume that all type checkers will infer the same type in this case.
If a symbol has a declared (annotated) type, its type is dictated by the annotation, and all assignments to that symbol will be validated against the declared type
If a symbol doesn't have a declared type, its type is inferred based on the union of all assignments, but literals are widened to their corresponding type; (note: for class and instance variables, only assignments within the class implementation are considered)
Within a local execution scope, the type of a symbol can be further narrowed based on assignments or type guards
Here's how these rules apply to the examples above:
importloggingclassC:
def__init__(self) ->None:
reveal_type(self.level) # intself.level=logging.INFOreveal_type(self.level) # Literal[20] (narrowed via assignment)defenable_debug(self) ->None:
reveal_type(self.level) # intself.level=logging.DEBUGreveal_type(self.level) # Literal[10] (narrowed via assignment)c=C()
reveal_type(c.level) # intc.level=1reveal_type(c.level) # Literal[1] (narrowed via assignment)c.level="hi"# Error because inferred type of `C.level` is `int`
FOO: Literal[1] =1x=FOOreveal_type(x) # Literal[1] (narrowed via assignment)x=3# This is not an error because the type of x was not declaredreveal_type(x) # Literal[3] (narrowed via assignment)x="hi"# This is not an error because the type of x was not declaredreveal_type(x) # Literal["hi"] (narrowed via assignment)
@sobolevn, I don't understand what you mean by a "soft literal". If you declare a variable with the type Literal[1], that means it can accept only values of Literal[1]. If it is meant to accept any int value, then it should be annotation as int.
The statement x: Final = 3 tells a type checker that x is read-only, but it doesn't specify its type. A type checker must infer its type in this case. Since type inference rules are not specified in any PEP, this is ambiguous.
Literal types are only supported for simple types such as int, bool, str and a few others. I assume that all type checkers can infer these types without problems. We could restrict the use of x: Final = y in stubs to cases where the right hand side is one of the literals supported by Literal.
I agree that this would a little inconsistent with how we define other things in stubs, but it would be consistent with how you'd define the variable in non-stub code. With Final, this looks nice to me:
A: Final= 'some-valueB: Final=A
If we'd use explicit literal types in non-stub code, this would look something like the following:
We had to duplicate the literal value three times. Even though this is more explicit, I much prefer the shorter version. If we'd support Final with an initializer in stubs, the stub and non-stub use cases would be consistent.
It's probably safe to say that most (all?) Python type checkers will infer a type of Literal[3] in this case, but why take a chance? Explicit is better than implicit.
Mypy doesn't infer Literal[3] as the type, as discussed above, but something a little different, as implied in PEP 586 (but the relevant section in the text is not super clear, I have to admit). PEP 586 was based on the implementation in mypy, so if something can be interpreted in multiple ways, the mypy interpretation seems implicitly like a valid one (though not necessarily the only one!). Most PEPs can't explain every detail -- that's one reason why it's good to have a prototype/reference implementation, as it can "fill in the gaps".
Edit: reading this section of PEP 586 makes it pretty clear that x: Final = 3 should be interpreted the same as x: Final[Literal[3]], so I guess it is safe to assume that all type checkers will infer the same type in this case.
Okay, that section apparently can be understood in multiple ways. Here the intention was that 'effectively literal' is different from just plain 'literal'.
The key part is this: "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 any context that expects a Literal[value].".
If this would be exactly the same as a literal type, the above passage would feel badly/confusingly written to me. Instead, it would be better written as e.g. "var: Final = value causes var to have the type Literal[value]". So it means that var can be used as a literal type if the context expects a literal type [but not necessarily in other contexts]. Actually, this is how mypy behaves. If you have var: Final = 1, var is treated as having the type int in non-literal contexts.
For something with a "real" literal type (i.e. not "effectively literal"), mypy treats literal types like any other type. It is propagated via type inference, etc. Mypy treats explicit literal types similarly to TypedDict types. The same constructor (e.g. string literal or dict expression) can generate values of multiple types, depending on the type context. Here's an example:
deff(a: Literal['x', 'y']) ->None:
b=a# b has type Literal['x', 'y'], propagated from rhs which has an explicit literal typec='x'# c has type str, since no explicit annotationd: Literal['x', 'y'] ='x'# d has type Literal['x', 'y']defg(a: SomeTypedDict) ->None:
b=a# b has type SomeTypedDictc= {'k': 5, ...} # c has type dict[str, ...], since no explicit annotationd: SomeTypedDict= {'k': 5, ...} # d has type SomeTypedDict
A type like Literal['x'] behaves consistently with a union of literal types in the above example, but I decided to use a union since the non-union case is used pretty rarely.
Anyway, if different type checkers have interpreted PEP 586 differently, it seems to me that we should avoid using literal types in stubs in contexts where their semantics aren't consistent, at least until we can figure out a convention that works for all major tools. Clearly part of the reason for the confusion is that PEP 586 was kind of vague and open to multiple interpretations, some of which were even explicitly allowed in the PEP. Perhaps we could narrow the semantics down bit, at least for stubs.
Another way to explain the mypy behavior is that explicit literal types are "sticky"/"strict" and don't get implicitly "weakened" into the underlying runtime type such as str. This allows precise type checking of some common use cases, such as using a union of literal types to represent Python 2 style enums:
# can achieve many things that real enums can do, but require some extra type annotationsEnumLike=Literal['x', 'y', 'z', ...]
In contrast, x: Final = y are "weak"/"leaky"/"soft" literals, since Final is used for many other things beyond literal types, and inferring a proper literal type for x would be too restrictive/inconvenient, if we also want to support "strict" literal types use cases (which we want).
The type Literal['x'] without a union is treated kind of like an enum with only one item -- i.e. it follows the same rules but is rarely useful, similarly how enums typically have multiple values.
Technically, it would be easy to change mypy to support x: Literal[42] as an alias for x: Final = 42 in stubs, but I prefer the Final approach, since it's consistent with how this would be written outside stubs (and personally using the Literal annotation feels against the spirit of PEP 586).
As the author of #6610 who introduced these literals in question to typeshed, I figured I'd leave my thoughts on the matter here. It's kinda a shame I only stumbled upon this issue and discussion after 6 months since it started, but it's better late than never, especially since it's still open.
Personally, I agree with @srittau about typeshed eventually committing to the Literal syntax instead of int. The reason is quite simple - these values are logging constants that are never supposed to change, so them having an int type makes their type definition unnecessarily broad. Whether or not it's gonna be later done via Literal, or Final that gets translated to a Literal, it doesn't really matter to me personally.
Now, as far as the issue itself goes, the starting sentence of:
I've encountered cases where the changes from int to a literal type in stubs causes regressions for mypy users.
has immediately caught my attention, since I've personally provided a solution to this exact same problem that was described right under it, in the PR of mine. And I disagree with the:
could be quite non-obvious for non-expert users who aren't familiar with literal types
statement, as this really isn't that complicated. In fact, it follows one of the "common issues" MyPy documentation describes. Now I know a Literal isn't really a "supertype", but it can be treated as one in this exact context: https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=invariance#declaring-a-supertype-as-variable-type Note that this paragraph is one of the shortest on the page, so there's shouldn't be much problem with understanding it.
Now, I know that MyPy isn't the only type checker typeshed provides the typing information for, but it'd only be a case of adding a similar explanation of this exact situation in other type checker documentations, in case it wouldn't be there already, possibly also mentioning that the same logic applies to Literals. If the user would like to avoid "weird" errors of having a variable with a literal type overwritten by another literal, they should use either a union of literals, or just int per the documentation (the prerequisites of existence of which I described above).
I think that sufficiently solves the problem already. If desirable, the inference logic could be improved to try constructing a union of literals, if it detects an unannotated variable being assigned a literal, and a different literal later.
I haven't reread the whole thread, so I might be missing something from the discussion. But from what I understand, we can't really use literals for enum- or flag-like constants, because when assigning a literal to a variable, at least in mypy the variable now has a literal type that can't be changed later:
FLAG1: Literal[1]
FLAG2: Literal[2]
_Flag: TypeAlias=Literal[1, 2]
a=FLAG1# type is now "Literal[1]"a=FLAG2# errorclassFoo:
flag=FLAG1classBar:
flag=FLAG2# error
Would typing such constants in typeshed like this work?
As far as I understand, this would automatically get us "enum-like" type checking automatically. What would probably not work at the moment is code like this, although type checkers could probably make it work when they would special case the idiom above:
deffoo(x: Literal[1]): ... # we only accept a specific set of flagsfoo(X)
we can't really use literals for enum- or flag-like constants, because when assigning a literal to a variable, at least in mypy the variable now has a literal type that can't be changed later
@srittau You can though, and I kinda explained this twice already, not sure why it's still unclear. Here's a practical explanation using your example:
There is something to be said about how MyPy infers the type without an annotation, forcing only the first literal it gets assigned to, and not a Literal[1, 2] - this is mostly what this thread is about. The final type being int from my solution isn't a union of said literals, but it surely does it's job at telling MyPy that this variable is okay to change later on. If you'd want to be really specific about it, you'd have to specify the union of literals yourself, but if you think about it and consider the fact that those literals are currently defined as an int already, you'll see that making such a change has no real impact, other than maybe requiring the user to add some explicit int annotations in affected places - that's it.
This discussion also seems to argue about whether or not one should use x: Literal[42] = 42 or x: Final = 42 for typeshed and stub annotations, in places that have them defined as int right now, such as the logging module:
As far as the semantics go, either of the two translates to a literal value to me, so it's really whatever as long as it's usable as a literal in the end. The supposed problem arises from the fact that people would often assign those literals to a variable, which would infer the literal type, and then throw an error upon a reassignment attempt, like so:
importloggingverbose=False# read from somewherelog_level=logging.ERRORifverbose:
log_level=logging.INFO# error
My PR which added Literals as the logging levels, was reverted because "it was giving false positives", at least according to the very first comment on this issue, despite me clearly providing a solution of "just slapping" an int annotation on the affected variables and calling it a day. Those were, as I've mentioned, already defined as int's before and them being int's now gains and losses exactly nothing, while more diligent people could actually use those literal types being defined properly from them on - that's the main reason I made that PR in the first place. But it's been completely disregarded and I sort of can't understand why it's been reverted and made into such a big deal. Is there a place where adding an int annotation wouldn't solve the false positives, or am I missing something?
If this literal inference issue is really so bad to require the user to add an explicit annotation to, MyPy could change the inference logic to using int or a union of literals automatically when that happens - int would work just fine for beginner typed code IMO.
Happy new year! With the end of 2022, what is the status on this? If I can specify the value of constants in the stubs I've contributed, I'd like to do it, and do it right :)
My preference is that literal constants are only/mainly used in stubs when the value of the literal is documented or the value is otherwise semantically significant somehow. #9367 has some context. Otherwise we risk unnecessarily leaking implementation details in the stub, which is intended as a description of the (public) interface of a module.
If we want to expose the value of a particular constant, I propose that we standardize on a Final declaration with an initializer but no explicit type, like this:
# This would work identically in both a stub and the implementationPY3: Final=True
It should be easy to add support for this, even If some type checkers don't support this currently in stubs. Mypy already supports this, and PY3 above would be treated as "effectively literal" with the value True. I think other type checkers would also have no issues with type inference if they follow this section in PEP 586: https://peps.python.org/pep-0586/#interactions-with-final
My reasoning is that it's better for the stub definition to be similar to the implementation. Writing the definition like this outside a stub would look out of place and redundant to me:
PY3: Literal[True] =True
I think that this is analogous to function definitions. We don't declare a function like this in a stub:
func: Callable[[int], None]
Instead the stub looks similar to the actual definition, even if this is sometimes more verbose than the prior option:
deffunc(__x: int) ->None: ...
I believe that adding type aliases that map to literal unions and using them as "fake enums" is a clever idea but ultimately too confusing for end users to be adopted as a best practice. I'm thinking of something like this, as suggested by @srittau above:
I discussed this in #9367, but here are my main arguments:
Using the alias requires the use of if TYPE_CHECKING, since it doesn't exist at runtime, and this looks kind of ugly and is confusing for new users.
The alias isn't easily discoverable, as it's only defined in the stub and not documented in library documentation.
The alias may expose internal details the might change in the future (the values of constants).
The alias looks like an internal definition, since the convention is to prefix them with an underscore.
Using a real enum avoids all the issues and I think that we should recommend that libraries switch to enums instead (e.g. IntEnum). The non-enum literal constant use case is mostly important for legacy code, and over time the need should diminish.
Activity
srittau commentedon Feb 18, 2022
At least in the sort term we should revert in cases where literals cause problems and in the future only use literals where it's unlikely that the literal gets assigned to a mutable variable.
In the long term we should rethink how type checkers handle literals. For example,
srittau commentedon Feb 18, 2022
Actually, at least in mypy the following already works:
So maybe we should allow
X: Final = 1in stubs?srittau commentedon Feb 18, 2022
Cc @rchen152 @erictraut @pradeep90 for feedback on using
X: Final = 3in stubs.srittau commentedon Feb 18, 2022
Also Cc @sobolevn as "
Literalmaster".srittau commentedon Feb 18, 2022
In #7259 pytype currently fails with "ParseError: Default value for CRITICAL: $external$typing_extensions.Final can only be '...', got LITERAL(50)", so we'd at least need to fix that and of course flake8-pyi.
AlexWaygood commentedon Feb 18, 2022
This to me feels much more expressive than writing
X: Literal[1], so I'd be in favour of allowing this in stubs whatever the case.I worry, however, that there's not much point in annotating a variable as
LiteralorFinalif it's automatically inferred as something less precise as soon as it's assigned to a different variable.JukkaL commentedon Feb 18, 2022
Using
Finalin the assignment will propagate the literal value:Since a variable with a literal type is effectively a constant, using
Finalseems logical to me. In Python all variables are mutable by default, so making something effectively final (i.e. literal) but not quite seems a bit unexpected to me. Using literal types is fairly rare, in my experience, so requiring an explicit annotation when something has a literal type seems more usable than requiring anintannotation when something is not a literal (intseems about 100x as common as all literal types, in one big codebase at least). "Make the common case easy and the uncommon case possible" seems to apply here, and it was an important principle when literal types were designed.PEP 586 has some relevant discussion:
A common use case for explicit literal types is as function arguments, and using a
Finalvariable works for this purpose:Similarly, this is fine:
sobolevn commentedon Feb 18, 2022
Thanks for letting me know.
I think this is a perfect example of "soft literal" type that I've mentioned several times in different discussions.
So, basically we have two cases:
x: Literal[1] = 1. You can't reassignxto anything other than1x: Literal[1]; self.value = x; self.value = 2. I think, that this should not be an error at allI know that @Akuli was interested in this as well.
rchen152 commentedon Feb 18, 2022
Just double-checking that I understand correctly:
x: Final = 3would mean that the type ofxisLiteral[3], but if we writey = x, the type ofywill beint. This is to prevent false positives in mypy caused by assigning a name to multiple Literal values. Is that right?If so:
x: Final = 3in stubs; I think it reads nicely. Changing pytype to not complain about this should be pretty straightforward, too.x: Literal[3] = 3, and it's not obvious from the annotations which one is stricter than the other, so to speak. If anything, mypy's behavior with theFinalannotation makes more sense to me, and I would want it to behave that way forLiteral, too.Allow `<name>: Final = <Literal value>` in pyi files.
erictraut commentedon Feb 19, 2022
I'm not seeing any errors when I run mypy on these code samples. Are you saying that mypy infers that the type of
self.levelisLiteral[20]in this case? That's not what I'm seeing.Type stub files should be as unambiguous as possible. The statement
x: Final = 3tells a type checker thatxis read-only, but it doesn't specify its type. A type checker must infer its type in this case. Since type inference rules are not specified in any PEP, this is ambiguous. It's probably safe to say that most (all?) Python type checkers will infer a type ofLiteral[3]in this case, but why take a chance? Explicit is better than implicit.So I would recommend against using the
x: Final = 3form in any typeshed stubs.Edit: reading this section of PEP 586 makes it pretty clear that
x: Final = 3should be interpreted the same asx: Final[Literal[3]], so I guess it is safe to assume that all type checkers will infer the same type in this case.erictraut commentedon Feb 19, 2022
Pyright follows these rules consistently:
Here's how these rules apply to the examples above:
@sobolevn, I don't understand what you mean by a "soft literal". If you declare a variable with the type
Literal[1], that means it can accept only values ofLiteral[1]. If it is meant to accept anyintvalue, then it should be annotation asint.JukkaL commentedon Feb 21, 2022
Literal types are only supported for simple types such as
int,bool,strand a few others. I assume that all type checkers can infer these types without problems. We could restrict the use ofx: Final = yin stubs to cases where the right hand side is one of the literals supported byLiteral.I agree that this would a little inconsistent with how we define other things in stubs, but it would be consistent with how you'd define the variable in non-stub code. With
Final, this looks nice to me:If we'd use explicit literal types in non-stub code, this would look something like the following:
We had to duplicate the literal value three times. Even though this is more explicit, I much prefer the shorter version. If we'd support
Finalwith an initializer in stubs, the stub and non-stub use cases would be consistent.Mypy doesn't infer Literal[3] as the type, as discussed above, but something a little different, as implied in PEP 586 (but the relevant section in the text is not super clear, I have to admit). PEP 586 was based on the implementation in mypy, so if something can be interpreted in multiple ways, the mypy interpretation seems implicitly like a valid one (though not necessarily the only one!). Most PEPs can't explain every detail -- that's one reason why it's good to have a prototype/reference implementation, as it can "fill in the gaps".
Okay, that section apparently can be understood in multiple ways. Here the intention was that 'effectively literal' is different from just plain 'literal'.
The key part is this: "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 any context that expects a Literal[value].".
If this would be exactly the same as a literal type, the above passage would feel badly/confusingly written to me. Instead, it would be better written as e.g. "
var: Final = valuecausesvarto have the typeLiteral[value]". So it means thatvarcan be used as a literal type if the context expects a literal type [but not necessarily in other contexts]. Actually, this is how mypy behaves. If you havevar: Final = 1,varis treated as having the typeintin non-literal contexts.For something with a "real" literal type (i.e. not "effectively literal"), mypy treats literal types like any other type. It is propagated via type inference, etc. Mypy treats explicit literal types similarly to TypedDict types. The same constructor (e.g. string literal or dict expression) can generate values of multiple types, depending on the type context. Here's an example:
A type like
Literal['x']behaves consistently with a union of literal types in the above example, but I decided to use a union since the non-union case is used pretty rarely.Anyway, if different type checkers have interpreted PEP 586 differently, it seems to me that we should avoid using literal types in stubs in contexts where their semantics aren't consistent, at least until we can figure out a convention that works for all major tools. Clearly part of the reason for the confusion is that PEP 586 was kind of vague and open to multiple interpretations, some of which were even explicitly allowed in the PEP. Perhaps we could narrow the semantics down bit, at least for stubs.
JukkaL commentedon Feb 21, 2022
Another way to explain the mypy behavior is that explicit literal types are "sticky"/"strict" and don't get implicitly "weakened" into the underlying runtime type such as
str. This allows precise type checking of some common use cases, such as using a union of literal types to represent Python 2 style enums:In contrast,
x: Final = yare "weak"/"leaky"/"soft" literals, sinceFinalis used for many other things beyond literal types, and inferring a proper literal type forxwould be too restrictive/inconvenient, if we also want to support "strict" literal types use cases (which we want).The type
Literal['x']without a union is treated kind of like an enum with only one item -- i.e. it follows the same rules but is rarely useful, similarly how enums typically have multiple values.8 remaining items
JukkaL commentedon Feb 22, 2022
Technically, it would be easy to change mypy to support
x: Literal[42]as an alias forx: Final = 42in stubs, but I prefer theFinalapproach, since it's consistent with how this would be written outside stubs (and personally using theLiteralannotation feels against the spirit of PEP 586).Replace some literal types in logging with int
Replace some literal types in logging with int (#7354)
Typeshed cherry-pick: Replace some literal types in logging with int (#…
DevilXD commentedon Aug 24, 2022
As the author of #6610 who introduced these literals in question to typeshed, I figured I'd leave my thoughts on the matter here. It's kinda a shame I only stumbled upon this issue and discussion after 6 months since it started, but it's better late than never, especially since it's still open.
Personally, I agree with @srittau about typeshed eventually committing to the
Literalsyntax instead ofint. The reason is quite simple - these values are logging constants that are never supposed to change, so them having aninttype makes their type definition unnecessarily broad. Whether or not it's gonna be later done viaLiteral, orFinalthat gets translated to aLiteral, it doesn't really matter to me personally.Now, as far as the issue itself goes, the starting sentence of:
has immediately caught my attention, since I've personally provided a solution to this exact same problem that was described right under it, in the PR of mine. And I disagree with the:
statement, as this really isn't that complicated. In fact, it follows one of the "common issues" MyPy documentation describes. Now I know a
Literalisn't really a "supertype", but it can be treated as one in this exact context: https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=invariance#declaring-a-supertype-as-variable-type Note that this paragraph is one of the shortest on the page, so there's shouldn't be much problem with understanding it.Now, I know that MyPy isn't the only type checker typeshed provides the typing information for, but it'd only be a case of adding a similar explanation of this exact situation in other type checker documentations, in case it wouldn't be there already, possibly also mentioning that the same logic applies to
Literals. If the user would like to avoid "weird" errors of having a variable with a literal type overwritten by another literal, they should use either a union of literals, or justintper the documentation (the prerequisites of existence of which I described above).I think that sufficiently solves the problem already. If desirable, the inference logic could be improved to try constructing a union of literals, if it detects an unannotated variable being assigned a literal, and a different literal later.
srittau commentedon Nov 16, 2022
I haven't reread the whole thread, so I might be missing something from the discussion. But from what I understand, we can't really use literals for enum- or flag-like constants, because when assigning a literal to a variable, at least in mypy the variable now has a literal type that can't be changed later:
Would typing such constants in typeshed like this work?
As far as I understand, this would automatically get us "enum-like" type checking automatically. What would probably not work at the moment is code like this, although type checkers could probably make it work when they would special case the idiom above:
DevilXD commentedon Nov 16, 2022
@srittau You can though, and I kinda explained this twice already, not sure why it's still unclear. Here's a practical explanation using your example:

There is something to be said about how MyPy infers the type without an annotation, forcing only the first literal it gets assigned to, and not a
Literal[1, 2]- this is mostly what this thread is about. The final type beingintfrom my solution isn't a union of said literals, but it surely does it's job at telling MyPy that this variable is okay to change later on. If you'd want to be really specific about it, you'd have to specify the union of literals yourself, but if you think about it and consider the fact that those literals are currently defined as anintalready, you'll see that making such a change has no real impact, other than maybe requiring the user to add some explicitintannotations in affected places - that's it.This discussion also seems to argue about whether or not one should use
x: Literal[42] = 42orx: Final = 42for typeshed and stub annotations, in places that have them defined asintright now, such as theloggingmodule:typeshed/stdlib/logging/__init__.pyi
Lines 298 to 305 in c626137
As far as the semantics go, either of the two translates to a literal value to me, so it's really whatever as long as it's usable as a literal in the end. The supposed problem arises from the fact that people would often assign those literals to a variable, which would infer the literal type, and then throw an error upon a reassignment attempt, like so:
My PR which added
Literals as the logging levels, was reverted because "it was giving false positives", at least according to the very first comment on this issue, despite me clearly providing a solution of "just slapping" anintannotation on the affected variables and calling it a day. Those were, as I've mentioned, already defined asint's before and them beingint's now gains and losses exactly nothing, while more diligent people could actually use those literal types being defined properly from them on - that's the main reason I made that PR in the first place. But it's been completely disregarded and I sort of can't understand why it's been reverted and made into such a big deal. Is there a place where adding anintannotation wouldn't solve the false positives, or am I missing something?If this literal inference issue is really so bad to require the user to add an explicit annotation to, MyPy could change the inference logic to using
intor a union of literals automatically when that happens -intwould work just fine for beginner typed code IMO.Avasam commentedon Dec 31, 2022
Happy new year! With the end of 2022, what is the status on this? If I can specify the value of constants in the stubs I've contributed, I'd like to do it, and do it right :)
JukkaL commentedon Jan 3, 2023
My preference is that literal constants are only/mainly used in stubs when the value of the literal is documented or the value is otherwise semantically significant somehow. #9367 has some context. Otherwise we risk unnecessarily leaking implementation details in the stub, which is intended as a description of the (public) interface of a module.
If we want to expose the value of a particular constant, I propose that we standardize on a
Finaldeclaration with an initializer but no explicit type, like this:It should be easy to add support for this, even If some type checkers don't support this currently in stubs. Mypy already supports this, and
PY3above would be treated as "effectively literal" with the value True. I think other type checkers would also have no issues with type inference if they follow this section in PEP 586: https://peps.python.org/pep-0586/#interactions-with-finalMy reasoning is that it's better for the stub definition to be similar to the implementation. Writing the definition like this outside a stub would look out of place and redundant to me:
I think that this is analogous to function definitions. We don't declare a function like this in a stub:
Instead the stub looks similar to the actual definition, even if this is sometimes more verbose than the prior option:
I believe that adding type aliases that map to literal unions and using them as "fake enums" is a clever idea but ultimately too confusing for end users to be adopted as a best practice. I'm thinking of something like this, as suggested by @srittau above:
I discussed this in #9367, but here are my main arguments:
if TYPE_CHECKING, since it doesn't exist at runtime, and this looks kind of ugly and is confusing for new users.Using a real enum avoids all the issues and I think that we should recommend that libraries switch to enums instead (e.g.
IntEnum). The non-enum literal constant use case is mostly important for legacy code, and over time the need should diminish.Avasam commentedon May 2, 2023
Is there anything left to do or discuss here? We seem to have been specifying literal constants for a while now.
srittau commentedon Jul 13, 2023
Indeed, the moratorium is over.