Here's the code
# args: argparse.Namespace
source: str = args.source
...
# subparsers: argparse._SubParsersAction
parser = subparsers.add_parser()
In the first line of code, : str
is enough to tell Pylance that source
is a string, although args.source
is Any
.
However, in the second line, when I add : argparse.ArgumentParser
, the type of parser
is still Any | argparse.ArgumentParser
. I have to manually use cast(argparse.ArgumentParser)
to get the desired effect, since add_parser()
is typed as returing Any
.
Why is that the case? Both are assignment to me, one from a field, one from the return of a function.
In the schematic LEFT = RIGHT
, if RIGHT
is not assignable to LEFT
, then it "needs a cast
"*.
This also applies to a function's return type as follows:
def f(...) -> <type of LEFT>:
return RIGHT
Some general examples:
RIGHT
is a subtype of LEFT
- doesn't need a cast
:
a: int | str = ""
b: int | str | bytes = a
class C: pass
class D(C): pass
c: C = D()
RIGHT
is a supertype of LEFT
- needs a cast
:
from typing import cast
a: object = ...
b: str = cast(str, a)
LEFT
- also needs a cast
:
from typing import cast
a: int = 1
b: str = cast(str, a)
As for your examples in the question:
Q: In the first line of code,
: str
is enough to tell Pylance thatsource
is a string, althoughargs.source
isAny
.
Any
is specially treated in the type system. From the typing specification:
Every type is assignable to
Any
, andAny
is assignable to every type.
This allows the following without a cast
:
from typing import Any
a: Any = 1
b: str = a
Q: ... in the second line, when I add
: argparse.ArgumentParser
, the type of parser is stillAny | argparse.ArgumentParser
.
This is actually just a pyright/pylance quirk. When pyright thinks you're doing something wrong in RIGHT
, it propagates an Any
(or in pyright's own terminology, Unknown
) type to LEFT
even though LEFT
already has an explicit annotation (other type-checkers may not do this). Here's a simple reproducer:
a: int = THIS_NAME_IS_NOT_DEFINED
reveal_type(a) # pyright: Type of "a" is "Unknown | int"
As to why pyright thinks you're doing something wrong, there's 3 issues with your snippet:
subparsers: argparse._SubParsersAction
is not assigned a value; it's just a declaration. Pyright hates this, and may even refuse to look at any annotations.
a: int
reveal_type(a) # pyright: Type of "a" is "Unknown"
Your declaration of subparsers: argparse._SubParsersAction
is missing a type argument. From the standard library stubs:
# argparse.pyi
...
_ArgumentParserT = TypeVar("_ArgumentParserT", bound=ArgumentParser)
...
class _SubParsersAction(Action, Generic[_ArgumentParserT]):
...
You shouldn't use this annotation (it's prefixed with an underscore); a well-written, fully-typed piece of code using argparse
should never need to actually use private annotations from argparse
, as all types can be inferred. However, if you really want to use this annotation, you'll need to provide a concrete type which is bounded to argparse.ArgumentParser
, such as subparsers: argparse._SubParsersAction[argparse.ArgumentParser] = parser.add_subparsers(...)
. This type argument is actually used to solve the return type of the add_parser
method, so if you omit it, the return type is unsolvable.
Your call of add_parser
is missing the mandatory first argument name
. Again, pyright hates this and propagates an Unknown
to LEFT
if your call expression is wrong:
def get_int(a: int) -> int:
return a
a: int = get_int()
reveal_type(a) # pyright: Type of "a" is "Unknown | int"
* Of course, in some situations, you don't actually "need a cast" - an explicit type annotation along with a # type: ignore
or # pyright: ignore[<some error code>]
will work just fine, along with turning on reportUnnecessaryTypeIgnoreComment
to warn unnecessary ignore comments. This prevents the need of an extra runtime variable lookup and call to typing.cast
.