I wrote the following function in Python 3.12:
# pyright: strict
from collections.abc import Iterable, Mapping
from typing import Any
type Nested = Any | Mapping[str, Nested] | Iterable[Nested]
def _get_max_depth(obj: Nested) -> int:
if isinstance(obj, Mapping):
return max([0] + [_get_max_depth(val) for val in obj.values()]) + 1
elif isinstance(obj, Iterable) and not isinstance(obj, str):
return max([0] + [_get_max_depth(elt) for elt in obj]) + 1
else:
return 0
However, pyright 1.1.398 complains that:
demo-pyright.py
demo-pyright.py:11:42 - error: Argument type is partially unknown
Argument corresponds to parameter "obj" in function "_get_max_depth"
Argument type is "Any | Mapping[str, Any | ... | Iterable[Nested]] | Iterable[Any | Mapping[str, Nested] | ...] | Unknown" (reportUnknownArgumentType)
demo-pyright.py:11:51 - error: Type of "val" is partially unknown
Type of "val" is "Any | Mapping[str, Nested] | Iterable[Nested] | Unknown" (reportUnknownVariableType)
demo-pyright.py:13:42 - error: Argument type is partially unknown
Argument corresponds to parameter "obj" in function "_get_max_depth"
Argument type is "Unknown | Any | Mapping[str, Any | ... | Iterable[Nested]] | Iterable[Any | Mapping[str, Nested] | ...]" (reportUnknownArgumentType)
demo-pyright.py:13:51 - error: Type of "elt" is partially unknown
Type of "elt" is "Unknown | Any | Mapping[str, Nested] | Iterable[Nested]" (reportUnknownVariableType)
4 errors, 0 warnings, 0 informations
To silence pyright, I could use the following workarounds:
- Change
Any
to concrete types, such asint | str
. - Replace
Mapping
withdict
. - Replace
Iterable
withlist
.
However, I am not satisfied with these workarounds since the function should be generic.
Is there a way to write type hints that satisfy pyright in strict mode in this case?
I wrote the following function in Python 3.12:
# pyright: strict
from collections.abc import Iterable, Mapping
from typing import Any
type Nested = Any | Mapping[str, Nested] | Iterable[Nested]
def _get_max_depth(obj: Nested) -> int:
if isinstance(obj, Mapping):
return max([0] + [_get_max_depth(val) for val in obj.values()]) + 1
elif isinstance(obj, Iterable) and not isinstance(obj, str):
return max([0] + [_get_max_depth(elt) for elt in obj]) + 1
else:
return 0
However, pyright 1.1.398 complains that:
demo-pyright.py
demo-pyright.py:11:42 - error: Argument type is partially unknown
Argument corresponds to parameter "obj" in function "_get_max_depth"
Argument type is "Any | Mapping[str, Any | ... | Iterable[Nested]] | Iterable[Any | Mapping[str, Nested] | ...] | Unknown" (reportUnknownArgumentType)
demo-pyright.py:11:51 - error: Type of "val" is partially unknown
Type of "val" is "Any | Mapping[str, Nested] | Iterable[Nested] | Unknown" (reportUnknownVariableType)
demo-pyright.py:13:42 - error: Argument type is partially unknown
Argument corresponds to parameter "obj" in function "_get_max_depth"
Argument type is "Unknown | Any | Mapping[str, Any | ... | Iterable[Nested]] | Iterable[Any | Mapping[str, Nested] | ...]" (reportUnknownArgumentType)
demo-pyright.py:13:51 - error: Type of "elt" is partially unknown
Type of "elt" is "Unknown | Any | Mapping[str, Nested] | Iterable[Nested]" (reportUnknownVariableType)
4 errors, 0 warnings, 0 informations
To silence pyright, I could use the following workarounds:
- Change
Any
to concrete types, such asint | str
. - Replace
Mapping
withdict
. - Replace
Iterable
withlist
.
However, I am not satisfied with these workarounds since the function should be generic.
Is there a way to write type hints that satisfy pyright in strict mode in this case?
Share Improve this question edited Mar 28 at 14:59 InSync 11.1k4 gold badges18 silver badges56 bronze badges asked Mar 28 at 11:22 Marcin BarczyńskiMarcin Barczyński 4052 silver badges13 bronze badges 1 |2 Answers
Reset to default 1Keep your code and add explicit casts
.
I'm not sure why exactly pyright
is so angry at that code - Unknown
means it is impossible to infer complete type of some variable, but your Nested
alias is packed with Any
- so no matter what that value is, it's still definitely assignable to Nested
. Why can't it infer? Because if Nested
was more restricted, some Mapping
that isn't a Mapping[str, Nested]
would still be assignable to Any
part of the union, but pyright fails to make the last connection and realize that it's still fine - any value type is still assignable to Nested
.
Note that reportUnknown*
rules don't always mean a type error - they only prevent errors from passing silently. It's fine to # type: ignore
them in your case instead of casting.
To help pyright, you can add casts as shown in @chepner answer. Here's a minimal modifications version (playground):
from collections.abc import Iterable, Mapping
from typing import Any, cast
type Nested = Any | Mapping[str, Nested] | Iterable[Nested]
def _get_max_depth(obj: Nested) -> int:
if isinstance(obj, Mapping):
return max([0] + [_get_max_depth(val) for val in cast(Mapping[str, Any], obj).values()]) + 1
elif isinstance(obj, Iterable) and not isinstance(obj, str):
return max([0] + [_get_max_depth(elt) for elt in cast(Iterable[Any], obj)]) + 1
else:
return 0
Going a bit further, you don't actually need Any
. Any
is a "do-whatever-you-want-with-me" type. You only need such a type that everything is assignable to it - such type is object
. This will add some safety, but it isn't critical when used as part of some union. playground
from collections.abc import Iterable, Mapping
from typing import cast
type Nested = object | Mapping[str, Nested] | Iterable[Nested]
def _get_max_depth(obj: Nested) -> int:
if isinstance(obj, Mapping):
return max([0] + [_get_max_depth(val) for val in cast(Mapping[str, object], obj).values()]) + 1
elif isinstance(obj, Iterable) and not isinstance(obj, str):
return max([0] + [_get_max_depth(elt) for elt in cast(Iterable[object], obj)]) + 1
else:
return 0
This is good as-is. I'd probably just annotate _get_max_depth(obj: object) -> int
which is just as safe as the previous version, but that's less helpful to future readers - perhaps keeping a union makes sense.
Going a bit into refactoring, you don't need [0] +
as max(..., default=0)
exists:
def _get_max_depth(obj: Nested) -> int:
items: Iterable[object] = []
if isinstance(obj, Mapping):
items = cast(Mapping[str, object], obj).values()
elif isinstance(obj, Iterable) and not isinstance(obj, str):
items = cast(Iterable[object], obj)
return max((_get_max_depth(val) for val in items), default=0)
# or more functional
# return max(map(_get_max_depth, items), default=0)
First, Any
is the wrong thing to use to define a generic type; you need a type variable:
type Nested[T] = T | Mapping[str, Nested[T]] | Iterable[Nested[T]]
Since isinstance
requires a class or tuple of classes, you can switch from asking permission (isinstance
checks) to asking fiveness (for example, pretend obj
is a mapping until it fails). You'll need to use cast
to make the type checker happy, but an obj
that isn't really a mapping will raise an AttributeError
on obj.values()
, and an obj
that isn't iterable will raise a TypeError
on list(obj)
.
def _get_max_depth[T](obj: Nested[T]) -> int:
try:
values = list(cast(Mapping[str, Nested[T]], obj).values())
except AttributeError:
try:
if isinstance(obj, str):
values = [obj]
else:
values = list(cast(Iterable[T], obj))
except TypeError:
values = []
return max([0] + [_get_max_depth(val) for val in values])
Any
is the wrong thing to use for a generic type; you need a type variable. – chepner Commented Mar 28 at 11:36