最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

How to write type hints for recursive function computing depth in Python? - Stack Overflow

programmeradmin1浏览0评论

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 as int | str.
  • Replace Mapping with dict.
  • Replace Iterable with list.

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 as int | str.
  • Replace Mapping with dict.
  • Replace Iterable with list.

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
  • Any is the wrong thing to use for a generic type; you need a type variable. – chepner Commented Mar 28 at 11:36
Add a comment  | 

2 Answers 2

Reset to default 1

Keep 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])
发布评论

评论列表(0)

  1. 暂无评论