Suppose I have a class parametrized by a type in Python 3.13:
class Foo[T]: ...
I want to define and type-annotate a method that should work with any Foo[T]
instance, regardless of the concrete type T
. I don't need the type T
at all in this method.
Some options I can come up with are:
Don't use the brackets:
def process_foo(x: Foo) -> None: ...
Use an Any
:
from typing import Any
def process_foo(x: Foo[Any]) -> None: ...
Parametrize the function too:
def process_foo[T](x: Foo[T]) -> None: ...
Option 1 gets a basedpyright error, so I don't like it at all. Option 2 requires an otherwise unnecessary import and gets a basedpyright warning, which can either be silenced with an inline comment (more verbosity and clutter)
def process_foo(x: Foo[Any]) -> None: ... # pyright: ignore[reportExplicitAny]
or project-wide, which I don't think is a good idea since the warning is there for a reason. Option 3 is a bit verbose and gets increasingly so if T is bound to some other type; and I also must repeat that bound so it increases coupling unnecessarily (I mean, the whole point is that I don't care about the type):
class Bar: ...
class Foo[T: Bar]: ...
def process_foo[T: Bar](x: Foo[T]) -> None: ...
Is there any truly pythonic way to do this? None of the above feel like so. Option 2 feels like the closest to me, but still, there must be a better way?
The all-encompassing/most general (fully static) type for such a generic class depends on the variance of the type parameters. Such a type is called the "top materialization" or "upper bound materialization".
T
is covariant, the top materialization is Foo[object]
(or Foo[UpperBound]
if T
has one).T
is contravariant, the top materialization is Foo[Never]
.T
is invariant, the top materialization is currently undenotable.In other words, process_foo()
can be defined as one of the following:
# Covariant & bivariant
def process_foo(x: Foo[object]) -> None: ...
# Contravariant
def process_foo(x: Foo[Never]) -> None: ...
# Invariant
def process_foo(x: Foo[Any]) -> None: ... # No better way
As an aside, ty implements this algorithm as part of its resolver.
class Bivariant[T]: ...
class Covariant[T]:
def _(self) -> T: ...
class Contravariant[T]:
def _(self, _: T) -> None: ...
class Invariant[T]:
def _(self, _: T) -> T: ...
from ty_extensions import Top
def _(
bivariant: Top[Bivariant[Any]],
covariant: Top[Covariant[Any]],
contravariant: Top[Contravariant[Any]],
invariant: Top[Invariant[Any]]
):
reveal_type(bivariant) # Bivariant[object]
reveal_type(covariant) # Covariant[object]
reveal_type(contravariant) # Contravariant[Never]
reveal_type(invariant) # Top[Invariant[Any]]