I have a lot of generic code that operates over many different “element” types. Some generic functions need to dispatch to different non-generic functions for each possible element type. The way that this is kept track of at runtime is by having a “handler” type that is associated to each element type. Each function takes the handler type as an argument and can either use the generic methods of the handler or can consult the handler to get information that narrows the element type.
I thought that I could handle this by using isinstance
with the handler object so that a typechecker could narrow the element type via an upper bound declared in the handler type. Specifically this is like:
from __future__ import annotations
from typing import Self, Protocol
class ElementA(Protocol):
def a_method(self) -> Self:
return self
class ElementB(ElementA, Protocol):
def b_method(self) -> Self:
return self
class HandlerA[T: ElementA]:
pass
class HandlerB[T: ElementB](HandlerA[T]):
pass
def generic_func[T: ElementA](x: T, handler: HandlerA[T]) -> T:
if isinstance(handler, HandlerB):
reveal_type(handler) # HandlerB[Any/Unknown]
reveal_type(x) # T
return x.b_method() # type checkers complain here
else:
return x.a_method()
I realise that in general isinstance(obj, list[T])
etc doesn’t work but here I am using a concrete isinstance
check for HandlerB
which is fine. The checkers narrow the type of handler
from HandlerA[T]
to HandlerB[Any]
but this is not what I wanted.
I use the notation Foo[T: B]
to mean the generic type Foo[T]
but with T
bounded by B
and what would be better is:
- At least this could be
HandlerB[T: ElementA]
because the bound onT
inHandlerB
ensures that every realisation ofHandlerB
can match the bound onT
ingeneric_func
. - Better would be if the checker could narrow this to
HandlerB[T: ElementB]
which should also be valid sinceElementB
is a subtype ofElementA
. - Ideally having narrowed
handler
fromHandlerA[T: ElementA]
toHandlerB[T: ElementB]
the checker would also realise thatT
is narrowed fromT: ElementA
toT: ElementB
and then narrow the type ofx
toElementB
.
This last point is the thing that I want. The way that this highly generic code is made type safe at runtime is by passing around the handler object so that quite literally records what the element type is. Functions may have more complex signatures like:
def f(x: list[T], y: list[T], h: Handler[T]) -> list[T]: ...
We don’t want to check isinstance
on the elements in the lists x
and y
and that wouldn’t even work if the lists were empty and the data structures can be much more complicated than this. Also it is does not make sense to dispatch by calling isinstance
with the element objects because the relationship between element types and handlers can be one to many: there can be different handler types that use the same element types.
The handler makes the types well defined at runtime so ideally it would also be what makes them well defined in a static type checker. It doesn’t look like either mypy or pyright allows narrowing of the bounds on the type parameter like this though.
Is there some better way of handling this?
Is this something that checkers could reasonably be expected to handle?