Narrowing with isinstance and possibly subscripted generic

Use case:

I have a class which is a list of instances of classes which extend Base,
and those extending classes could themselves have a Generic mixin.

I want an of_type function which will let me filter to the concrete class.


First naive attempt:

class Base:
    ...

T = TypeVar('T', bound=Base)

class BaseList(list[Base]):
    def of_type(self, klass: type[T]) -> list[T]:
        return [m for m in self if isinstance(m, klass)]

Trivial case is fine:

class TrivialSub(Base):
    ...

list = BaseList()
list.append(TrivialSub())
print(list.of_type(TrivialSub)) # ok

But with Generic mixin:

S = TypeVar('S')

class GenericSub(Base, Generic[S]):
    val: S

list.append(GenericSub[int]())
print(list.of_type(GenericSub[int])) # fails:
# TypeError: Subscripted generics cannot be used with class and instance checks

Annoying, but makes sense.


Best I can come up with to satisfy type checker and at run time:

    def of_type(self, klass: type[T]) -> list[T]:
        maybe_origin = get_origin(klass)
        klass_ = maybe_origin if maybe_origin else klass

        return [cast(klass, m) for m in self if isinstance(m, klass_)]

Does anyone have any better ideas how to implement this?

I know my solution loses type safety on S and I feel like I could also handle that with get_args, but I wanted to see if I’m going down a rabbit hole first :grin:


Full MRE here: Narrowing with isinstance and possibly subscripted generic · GitHub

The key difficulty here is that:

# TypeError: Subscripted generics cannot be used with class and instance checks

If the only subscripted generics you want to support are garden-variety collection types like List[MyClass] or Set[MyClass] then you might consider using isassignable from trycast or similar enhanced isinstance functions in other libraries.

In the case of the trycast library in particular, I know that “User-defined generic types” (which would also support custom subscripted generics beyond the usual collection types) is on the roadmap.

1 Like