Simplifying Parametrized Type Hints for Generic Functions in Python

Hi everyone,

I’m looking for a way to simplify the type hints for a generic function. Specifically, I want to parametrize a type of a function, where this type itself is also parametrized, and I want to specify the latter differently for different variables. To be specific, I want to achieve something like this:

from typing import TypeVar
GenericType = TypeVar("GenericType", bound=GenericClass)

def normalized(obj: GenericType[int], factor: float) -> GenericType[float]:
    # actual implementation

That has the same effect as this:

@overload
def normalized(obj: GenericSubclass[int], factor: float) -> GenericSubclass[float]:
    ...

@overload
def normalized(obj: GenericClass[int], factor: float) -> GenericClass[float]:
    ...

def normalized(obj, factor):
    # actual implementation

Is there a more concise way to achieve this behavior using type hints? Any insights or suggestions would be greatly appreciated!

Thanks!

Which versions of Python do you need to support?

Generics have changed fairly dramatically in most recent versions.

What you are looking for are higher kinded types, they do not yet exist in Python, so you can’t really write generic functions that preserve the container type while changing the element type.

Your overload hack is the best you can do currently without writing a mypy plugin that uses a function hook to change the return type to be more precise.

2 Likes

I’m on 3.8, but would love to understand if something is possible on the latest if not on 3.8

Thanks, do you know if it’s on the roadmap to be able to create higher kinded types to preserve the container type while changing the element type? If not, do you know if I should request that somewhere and where?

It’s definitely something a lot of people have asked for. But their implementation is non-trivial and some type systems have explicitly decided against adding them for that very reason. Python is not at that point yet, since nobody has gone through the trouble of writing a PEP yet as far as I’m aware. There are other more pressing topics, so I wouldn’t hold my breath, until the typing specification is in a more complete and stable place.

2 Likes

Thanks a lot for your help and prompt replies!

If an idea to write a mypy plugin for that sounds scary, I may have good news for you: it already exists.

dry-python/returns supports HKT natively, here’s the relevant documentation (no affiliation, just a happy user).

If you have sufficient control over GenericClass, here’s what I managed to code with returns:

from typing import Self, TypeVar, reveal_type

from returns.primitives.hkt import Kind1, SupportsKind1, kinded

_T = TypeVar('_T')
_I = TypeVar('_I', bound='_SomeContainer')


class _SomeContainer(SupportsKind1[_I, _T], list[_T]):
    pass

class SomeContainer(_SomeContainer['SomeContainer', _T]):
    pass

class ChildContainer(_SomeContainer['ChildContainer', _T]):
    pass


@kinded
def normalized(obj: Kind1[_I, int], factor: float) -> Kind1[_I, float]:
    return obj


parent: SomeContainer[int]
child: ChildContainer[int]
reveal_type(normalized(parent, 2))  # N: Revealed type is "main.SomeContainer[builtins.float]"
reveal_type(normalized(child, 2))  # N: Revealed type is "main.ChildContainer[builtins.float]"

Note that ChildContainer does not inherit from SomeContainer - I can’t figure out any way to do correct inheritance with SupportsKind types - you cannot override first type arg, and it has to be specific. However, probably inheritance from _SomeContainer should be enough in most cases, and some workarounds should exist if not.

2 Likes