A canonical "isinstance" implementation for typing types?

You can’t use typing types like Dict[str, int] in an isinstance check:

Python 3.7.6 (default, Dec 30 2019, 19:38:28)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.12.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: from typing import Dict

In [2]: myvar = {"a": 1}

In [3]: isinstance(myvar, Dict[str, int])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-a8fee57141ae> in <module>
----> 1 isinstance(myvar, Dict[str, int])

However any library that does type-checking needs to be able to do something like isinstance(myvar, Dict[str, int])

I feel like the equivalent function that works for typing types must exist somewhere, maybe in mypy project? (there’s a lot of complicated code in there and I couldn’t find it)

There are plenty of projects besides mypy which need this, libraries like pydantic for example, and AFAICT they all have complicated hand-rolled implementations and it seems like there are lots of edge cases (or just… ‘cases’) which have to be enumerated and covered. This leads to bugs/limited type recognition e.g. Mapping type should be passed to _handle_dict · Issue #27 · bloomberg/attrs-strict · GitHub

It seems like there is a need for a canonical implementation of this functionality. Does one exist already somewhere that I haven’t found?

1 Like

No bites here…

I got some good responses on Stack Overflow: python 3.x - Is there a canonical “isinstance” implementation for typing types? - Stack Overflow

Basically the answer is “no”.

I gathered from comments there that this feature is not just missing from the core typing library but there seems to be some resistance to the idea of adding it, that core devs prefer typing to be used in static-analysis only.

I can understand not wanting to encourage overuse of runtime type-checking. At the same time I think this makes it unnecessarily difficult for libraries which make use of type introspection to provide a nice API. I was thinking mainly of serialization/validation libs like Pydantic.

Then the other day I came across this example from the stdlib:

For functions annotated with types, the decorator will infer the type of the first argument automatically:

>>> @fun.register
... def _(arg: int, verbose=False):
...     if verbose:
...         print("Strength in numbers, eh?", end=" ")
...     print(arg)
...
>>> @fun.register
... def _(arg: list, verbose=False):
...     if verbose:
...         print("Enumerate this:")
...     for i, elem in enumerate(arg):
...         print(i, elem)

Well that’s pretty cool.

But it’s cheating, because these days generally we wouldn’t annotate the second function using list builtin, but rather something like List[str]. And that doesn’t work, because singledispatch is just doing a naive isinstance check, and that can’t handle typing generics. So singledispatch doesn’t really support dispatch by type-annotation like it claims to.

Want to bump this up.

With the new type x = ... syntax in 3.12 being unusable with isinstance, it just feels weird that a native language feature to create types doesn’t work with runtime type checking.

2 Likes

The trycast library provides an isassignable function that acts like isinstance but can recognize more kinds of types. For example you can check whether a value is assignable to a Union type, a TypedDict type, or to a Dict[K, V] type:

>>> from trycast import isassignable
>>> from typing import Dict
>>> myvar = {"a": 1}
>>> isassignable(myvar, Dict[str, int])
True
>>> isassignable({"a": 1.1}, Dict[str, int])
False

This library is designed to be potentially integrated into the Python standard library in the future, although that hasn’t been formally proposed yet.

2 Likes

This is awesome! I think there’s good reasons to integrate this into isinstance. Would the library also work with 3.12 type x = ... types?

I expect that making isinstance behave like isassignable would break a lot of existing code. And the performance characteristics of the two functions are very different:

  • isinstance checks are usually relatively cheap
  • isassignable can be more expensive, especially if you check against a type like list[str] which involves examining the entire nested structure of the value being inspected.

So I wouldn’t advocate merging the two of them myself.

isassignable recognizes regular type aliases. So I expect you could use it with type aliases created using the type statement, although I haven’t tried yet:

type CardDeck = list[tuple[str, int]]

isassignable doesn’t currently support TypeVars, so type statements creating a generic type alias probably won’t work:

type Ordered[T] = list[T] | tuple[T, ...]

isassignable sounds like it would belong nicely in the inspect module.

2 Likes

In case people are not aware, there is check_type in the Typeguard library. I don’t know if it supports the new Python 3.12 syntax yet.

3 Likes

I’d also point folks at beartype, which implements beartype.door.is_bearable. Beartype and typeguard are probably the two most popular general runtime type-checking libraries.

FWIW this has been a sore spot for a long time, isinstance is buggy with the old Union type for example:

from typing import Union

class MetaAlwaysTrue(type):
    def __instancecheck__(self, instance):
        return True


class AlwaysTrue(metaclass=MetaAlwaysTrue):
    pass

print(isinstance(1, AlwaysTrue))              # True
print(isinstance(1, AlwaysTrue | str))        # True
print(isinstance(1, Union[AlwaysTrue, str]))  # False!

Good call out. For completeness, here are all the other libraries I was able to track down in early 2022 with some kind of “enhanced isinstance” when I did a survey of typing-related libraries:

  • pytypes
  • runtype
  • trycast - mentioned earlier
  • typeguard - mentioned earlier
  • beartype - mentioned earlier
2 Likes