import dataclasses import typing # need to set _root so typing._Final thinks we're allowed to be a subclass class MyGenericAlias(typing._GenericAlias, _root=True): def __subclasscheck__(self, cls): """Checks cls is a subclass of self, called by issubclass(cls, self)""" # iterate through all parents transitively # eg. if self=A[B1, B2] and cls_parent=C[D1, D2] for cls_parent in cls.mro(): for base in getattr(cls_parent, "__orig_bases__", []): if( # check C is a subclass of A issubclass(self.__origin__, base.__origin__) # check they have the same number of args in brackets # (they may differ in case of subclasses, I think?) and len(self.__args__) == len(base.__args__) # check all Dn are subclasses of their respective Bn and all( isinstance(self_arg, type) and isinstance(cls_arg, type) and issubclass(self_arg, cls_arg) for (self_arg, cls_arg) in zip(self.__args__, base.__args__)) ): return True return False # In Python >=3.8, we're only allowed to subclass Generic in classes named # "Protocol", so we're using this empty intermediate class. # (In Python 3.7, you might be able to make it work by calling it "_Protocol" # instead.) class Protocol(typing.Generic): pass class MyGeneric(Protocol): def __class_getitem__(cls, params): return MyGenericAlias(cls, params) T = typing.TypeVar("T", bool, int) @typing.sealed class Expr(MyGeneric[T]): pass @dataclasses.dataclass class EBool(Expr[bool]): b: bool @dataclasses.dataclass class EInt(Expr[int]): n: int @dataclasses.dataclass class EEqual(Expr[bool]): l: Expr[int] r: Expr[int] def eeval(e: Expr[T]) -> T: match e: case EBool(b): return b case EInt(n): return n case EEqual(l, r): return eeval(l) == eeval(r) a = EInt(42) b = EInt(21) c = EEqual(a, b) print("a", eeval(a)) print("b", eeval(b)) print("c", eeval(c)) print("c is an Expr[int]: ", isinstance(c, Expr[int])) print("c is an Expr[bool]:", isinstance(c, Expr[bool]))