Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Abstracting over type constructors in Python via type annotations

I want to statically enforce that a method of a class returns a value wrapped in some abstract type, that I know nothing about:

E.g. given the abstract class

F = ???    

class ThingF(Generic[F]):
    @abstractmethod
    def action(self) -> F[Foo]:
        ...

I want to to be able to statically check that this is invalid:

class ThingI(ThingF[List]):
    def action(self) -> Foo:
        ...

because action does not return List[Foo].

However the above declaration for ThingF does not even run, because Generic expects its arguments to be type variables and I cannot find a way to make F a type variable "with a hole".

Both

F = TypeVar('F')

and

T = TypeVar('T')
F = Generic[T]

do not work, because either TypeVar is not subscriptable or Generic[~T] cannot be used as a type variable.

Basically what I want is a "higher kinded type variable", an abstraction of a type constructor, if you will. I.e. something that says "F can be any type that takes another type to produce a concrete type".

Is there any way to express this with Python's type annotations and have it statically checked with mypy?

like image 683
karlson Avatar asked Oct 27 '25 21:10

karlson


2 Answers

Unfortunately, the type system (as described in PEP 484) does not support higher-kinded types -- there's some relevant discussion here: https://github.com/python/typing/issues/548.

It's possible that mypy and other type checking tools will gain support for them at some point in the future, but I wouldn't hold my breath. It would require some pretty complicated implementation work to pull off.

like image 124
Michael0x2a Avatar answered Oct 29 '25 10:10

Michael0x2a


You can use Higher Kinded Types with dry-python/returns. We ship both primitives and a custom mypy plugin to make it work.

Here's an example with Mappable aka Functor:

from typing import Callable, TypeVar

from returns.interfaces.mappable import MappableN
from returns.primitives.hkt import Kinded, KindN, kinded

_FirstType = TypeVar('_FirstType')
_SecondType = TypeVar('_SecondType')
_ThirdType = TypeVar('_ThirdType')
_UpdatedType = TypeVar('_UpdatedType')

_MappableKind = TypeVar('_MappableKind', bound=MappableN)


@kinded
def map_(
    container: KindN[_MappableKind, _FirstType, _SecondType, _ThirdType],
    function: Callable[[_FirstType], _UpdatedType],
) -> KindN[_MappableKind, _UpdatedType, _SecondType, _ThirdType]:
    return container.map(function)

It will work for any Mappable, examples:

from returns.maybe import Maybe

def test(arg: float) -> int:
        ...

reveal_type(map_(Maybe.from_value(1.5), test))  # N: Revealed type is 'returns.maybe.Maybe[builtins.int]'

And:

from returns.result import Result

def test(arg: float) -> int:
    ...

x: Result[float, str]
reveal_type(map_(x, test))  # N: Revealed type is 'returns.result.Result[builtins.int, builtins.str]'

It surely has some limitations, like: it only works with a direct Kind subtypes and we need a separate alias of Kind1, Kind2, Kind3, etc. Because at the time mypy does not support variadic generics.

Source: https://github.com/dry-python/returns/blob/master/returns/primitives/hkt.py Plugin: https://github.com/dry-python/returns/blob/master/returns/contrib/mypy/_features/kind.py

Docs: https://returns.readthedocs.io/en/latest/pages/hkt.html

Announcement post: https://sobolevn.me/2020/10/higher-kinded-types-in-python

like image 29
sobolevn Avatar answered Oct 29 '25 12:10

sobolevn



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!