Closed zenbot closed 2 years ago
It works with mypy because it naively resolves the type variable to a literal, however when you are defining a function with a type variable no such literal type constraints are taken into consideration, in fact without a more expressive type system resolving type variables to literals seems sort of useless:
from typing import *
T = TypeVar('T')
def mk_instance(cls: type[T]) -> T:
return cls()
def foo(a: T) -> T:
return mk_instance(type(a))
num: Literal[5] = 5
reveal_type(foo(num)) # Revealed type is "Literal[5]" // 0 at runtime
Pyright (and I presume mypy) uses various heuristics to determine when to retain a literal and when to widen the type to its non-literal counterpart. These heuristics have been tuned over time to produce expected behaviors most of the time while avoiding false positive errors. Tuning these heuristics is challenging because fixing one case can break others.
For the code sample at the top of this issue, it seems logical (at least to me) that the literal type would be preserved in this case. I'll investigate this particular case further to see if I can adjust the heuristics accordingly without affecting other use cases.
@MathiasSven, there are definitely cases where it's useful (and even necessary) to retain literals in the TypeVar constraint solver. The example you provided is highly contrived, so I wouldn't optimize for that use case.
I apologize, I meant to say that resolving type variables across a type signature (arg, return) seems pointless as currently neither type checker does any checking to make sure the implementing function actually retain the same literal. The same example can be reproduced with simple literal manipulation inside functions, such as def foo(a: SupportsAddT) -> SupportsAddT: return a + 5
, I only brought this up because this has been a long bug I noticed in mypy, but maybe even then optimistically inferring literal across the arrow might have overweighting advantages as you mentioned.
Yeah, that's a good point. Thanks for clarifying.
As I anticipated, my first attempt to adjust this heuristic broke some other important type evaluation cases. The solution I implemented is to have the TypeVar constraint solver retain literals for the return type of a Callable if the source function has a declared return type with a Literal
. If the source function has an inferred return type that includes a Literal
, the literals are not retained by the constraint solver in that case.
This change will be included in the next release.
@MathiasSven, I think you are correct to point out a potential type safety hole, but this is a case where practicality is more important than purity. It's reasonable to expect that decorators like functools.cache
will preserve the return type of the function they are decoratoring, even when that type includes a Literal
.
This is addressed in pyright 1.1.245, which I just published. It will also be included in the next release of pylance.
pyright 1.1.244 appears to interpret functions decorated with
functools.cache
(&functools.lru_cache
) that returnLiteral
s as returning the less specific non-literal type instead. The following code reproduces this:This code should pass type checking (& mypy is happy with it), but instead produces the following error in pyright: