0
\$\begingroup\$

Background

The typing module supports subscriptable types. Here is an example use case:

from typing import List

def foo(bar: List[int]):
    pass

However, when you try to use this with isinstance, it fails:

>>> from typing import List
>>> my_list = [1, 2, 3]
>>> isinstance(my_list, List[int])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 719, in __instancecheck__
    return self.__subclasscheck__(type(obj))
  File "/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py", line 722, in __subclasscheck__
    raise TypeError("Subscripted generics cannot be used with"
TypeError: Subscripted generics cannot be used with class and instance checks

Code

So, I wrote a program which can test for subscripted types. Here is my code:

class Union:
    def __init__(self, type1, type2):
        if not isinstance(type1, (type, Type)) or not isinstance(type2, (type, Type)):
            raise ValueError(f'{type1!r}, {type2!r} are not types')
        self.types = type1, type2
    def __str__(self):
        return self.types[0].__name__ + ' | ' + self.types[1].__name__
    def __repr__(self):
        return str(self)
    @property
    def __name__(self):
        return str(self)

class Type:
    def __init__(self, dtype, of=None):
        if isinstance(dtype, Type):
            self.dtype = dtype.dtype
        elif isinstance(dtype, (type, Union)):
            self.dtype = dtype
        else:
            raise ValueError(f'{dtype!r} is not a type')
        if of is not None and not isinstance(of, (Type, Union, type)):
            raise ValueError(f'{of!r} is not a type')
        self.of = of
    def __str__(self):
        if self.of is not None:
            return f'{self.dtype.__name__}[{str(self.of.__name__)}]'
        else:
            return str(self.dtype.__name__)
    def __repr__(self):
        return str(self)
    def __getitem__(self, cls):
        return Type(self, cls)
    def __or__(self, other):
        return Union(self, other)
    def __ror__(self, other):
        return Union(other, self)
    @property
    def __name__(self):
        return str(self)

def IsInstance(obj, dtype):
    if isinstance(dtype, type):
        return isinstance(obj, dtype)
    elif isinstance(dtype, Type):
        if dtype.of is None:
            return isinstance(obj, dtype.dtype)
        return all(IsInstance(item, dtype.of) for item in obj)
    elif isinstance(dtype, Union):
        return any(IsInstance(obj, t) for t in dtype.types)

Bool = Type(bool)
Dict = Type(dict)
Float = Type(float)
Int = Type(int)
List = Type(list)
Set = Type(set)
Str = Type(str)
Tuple = Type(tuple)

Notes

  • Union is the equivalent of typing.Union, but it is a bit different. You can get a Union by either:
    • Using Union(int, float)
    • Using Int | Float
  • To do the instance check, use IsInstance, not isinstance. For example:
    • Use IsInstance([1, 2, 3], List[int])

The code is also on Github, and PyPI

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I get the motivation for doing this, but it crosses an important line - one that I've myself crossed and then somewhat regretted later.

There's only so much you can do to save Python from itself. The type system is weak, and will never be strong no matter how clever we attempt to be. PEP484 is explicitly about static type analysis and not runtime type enforcement.

You've implemented IsInstance to check for the hint, but crucially it never actually checks whether the associated data in runtime match the hint.

Also you've written Type, Union, Dict, List, Set and Tuple classes that, while they don't technically shadow their alternatives in the built-in typing module, are really not a good idea to name identically.

Please don't do any of this. Use of isinstance itself is very often an anti-pattern, but we can't say for sure because you haven't shown where this code is used. Frequent remedies to isinstance are class polymorphism, writing type-alternative front-end functions to a core logic function (since Python has no signature overloading), etc.

\$\endgroup\$
2
  • \$\begingroup\$ "You've implemented IsInstance to check for the hint, but crucially it never actually checks whether the associated data in runtime match the hint." - I didn't want to do that. If I want to check the type, I'll do it explicitly by calling IsInstance. \$\endgroup\$
    – The Thonnu
    Oct 15, 2022 at 16:42
  • \$\begingroup\$ You're doing something that should be done by mypy and not you, in static time and not runtime, and you're failing to actually validate runtime data. So I think what you want to do is a problem. \$\endgroup\$
    – Reinderien
    Oct 15, 2022 at 20:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.