pythongenericspython-typing

Type-annotating function with generics that I don't care about


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:

  1. Don't use the brackets:

    def process_foo(x: Foo) -> None: ...
    
  2. Use an Any:

    from typing import Any
    def process_foo(x: Foo[Any]) -> None: ...
    
  3. 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?


Solution

  • 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".

    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.

    (playground)

    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]]