Describe the bug
This is a regression introduced in 1.1.20 whereby a forward reference in a bounded type variable (a string) is not resolved generically, instead being interpreted as a literal:
{
"resource": "test.py",
"owner": "_generated_diagnostic_collection_name_#0",
"severity": 8,
"message": "Argument of type 'Foo' cannot be assigned to parameter 'value' of type 'TypeVar['T']'\n Type 'Foo' is not compatible with bound type 'Literal['Base']' for TypeVar 'T'\n 'Foo' cannot be assigned to 'Literal['Base']'",
"source": "pyright",
"startLineNumber": 20,
"startColumn": 17,
"endLineNumber": 20,
"endColumn": 22
}
The instance method also did not emit an error, unlike the function, which made debugging more difficult.
To Reproduce
Type check the snippet below.
Expected behavior
That the return type of both Foo().bar() and baz(Foo()) would be Foo.
Screenshots or Code
from typing import TypeVar
T = TypeVar('T', bound='Base')
class Base:
def bar(self: T) -> T:
pass
class Foo(Base):
pass
reveal_type(Foo().bar()) # Type of 'Foo().bar()' is 'TypeVar['T']'
def baz(value: T) -> T:
pass
reveal_type(baz(Foo())) # Type of 'baz(Foo())' is 'TypeVar['T']'
VS Code extension or command-line
extension, 1.1.20
Additional context
Minimal reproducible example of #485.
I don't consider this a bug. A string literal within an argument expression should not be interpreted as a forward-declared type.
Expressions within type annotations can be use string literal syntax to denote a forward declaration. In this case, 'Base' is not used as a type annotation but rather as an argument expression. In that context, a string literal should be interpreted as a string, not a type. The type checker shouldn't need to special-case this particular parameter and treat it differently from every other argument expression.
I also don't see how this was a regression. Pyright has always followed the rule that string literals should be interpreted as forward types only within type annotations. This didn't change in 1.1.20.
Ah, I think I see what happened. Pyright has always interpreted 'Bound' as the type Literal['Bound'] (which is obviously not what was intended). Earlier versions of Pyright did not properly detect that baz(Foo()) violated the bounded TypeVar. With the bug fix introduced in 1.1.20, Pyright now properly reports this error.
It looks like the author of the django-stubs type stub assumes that the type checker will special-case the "bound" parameter and treat the argument expression as a forward-declared type if it's specified as a string literal. I wonder if mypy special-cases this parameter. If so, that's really really gross. I'm not sure I want to follow that precedent in Pyright.
This is the first example given in PEP 563 of constructs continuing to require quotes. I can't seem to find where this behaviour is documented and it might very well be that PEP 563 is following a precedent set by mypy.
(I assume this can be trivially resolved in the stub file by quoting the type annotation and defining the type variable _after_ the class body, though when exactly quoted annotations should be evaluated by type checkers is, again, not defined.)
I've found a way to support this without adding too many hacks.
This is now implemented in 1.1.21, which I just published.
Most helpful comment
I've found a way to support this without adding too many hacks.