What would it take to implement type mapping for generics?

I recently came about a situation in my code where, as mentioned here, I have a clear mapping between input and output types that occurs in many different places throughout my code, and updating that mapping is just infeasible.

I’m a huge supporter of type-hinting for Python, and would be willing to look into implementing this functionality (see link below - as a new user to this forum I’m only able to include 2 links). However, I’m not sure what the process is like.

What steps would need to be taken in order for this to be implemented in a future version of Python?

2 Likes

It is better to explain what you are talking about here in discourse. Not having clicked either of your links I have no idea what you are talking about.

Got it, I’ll copy/paste from the original source. I’d like to implement functionality like this:

TypeLookupTable = TypeMapping('TypeLookupTable', {
    FromType1: ToType1,
    FromType2: ToType2,
    FromType3: ToType3,
    ...
})

T = TypeVar('T', bound=any_type_present_as_a_key_in_that_lookup_table)

@dataclass
class SomeGeneric(Generic[T]):
     data: T

     def func(self) -> TypeLookupTable[T]:
         # This function should produce an instance of the corresponding type of `T` in
         # that lookup table.

It looks like there was some agreement in the thread I linked that this would be useful functionality, and I was looking for documentation on the process for actually implementing this and getting it integrated into python’s typing module, and the main page linked me here.

So here I am, asking what the process is for updating the typing functionality to allow for this sort of construction. Does there need to be a PEP for it? How does the proposal I linked to on GitHub actually turn into a real contribution / change to typing in Python?

Does there need to be a PEP for it?

It’s a big change so yes

2 Likes

What you’re looking for is support for type families. There are not many languages that do this in convenient syntax outside of haskell/scala. Swift, Rust, and Kotlin have some support as well as I understand it.

Not sure what python’s stance on more advanced type features is, but it would require a PEP to support.

2 Likes

I posted a similar idea a while back: Make `@overload` less verbose - #10 by jorenham, and in false positive with `divmod` and overloaded `__divmod__` · Issue #9835 · microsoft/pyright · GitHub I explained it in a bit more detail.

Maybe we’re talking about the same thing here?

The “type mappings” I had in mind can do at least everything that overloads can do, i.e. they generalize overloads. Overloads in python are notoriously difficult, and very underspecified.

So if we’re indeed talking about the same thing here, then the PEP for these type-mappings is going to be a beefy one, and considering the current state of overloads, likely to be contentious.

I don’t mean to discourage you, but if you’re seriously considering this, then you should know what you’re getting yourself into.

2 Likes

This is wonderful feedback, thank you! I’ll look into these.

Interesting reading your comments, I was thinking something along similar lines a while ago, I actually got a simple type mapping working in the current type system by abusing the constraint solving. The below works (on pyright, but not mypy) to give a mapping from literal keys to differently typed values. It’s a bit clunky, but doesn’t require every overload to be specified:

from __future__ import annotations
from typing import Any, Literal, LiteralString

class Mapping[Kt: tuple[Any, Any]]:
    def __getitem__[GKt, Vt](
        self: Mapping[tuple[GKt, Vt]],
        key: GKt
    ) -> Vt:
        ...

class ApplyGet[Tk: LiteralString]:
    val: Tk
    def __init__(self, val: Tk):
        self.val = val

    def get[Vt, Vo: tuple[Any, Any]](self, mapping: Mapping[tuple[Tk, Vt] | Vo]) -> Vt:
        return mapping[self.val]

type IntKey = Literal["int_key"]
type IntKV = tuple[IntKey, int]
type StrKey = Literal["str_key"]
type ErrorStr = Literal["error_key"]
type StrKV = tuple[StrKey, str]
type MyMapping = Mapping[IntKV | StrKV]

def get_val(mapping: MyMapping):
    int_key: IntKey = "int_key"
    int_val = ApplyGet(int_key).get(mapping)
    raw_int_val = ApplyGet("int_key").get(mapping)
    reveal_type(int_val) # inferred type: int - correct
    reveal_type(raw_int_val) # inferred type: int - correct

    str_key: StrKey = "str_key"
    str_val = ApplyGet(str_key).get(mapping)
    raw_str_val = ApplyGet("str_key").get(mapping)
    reveal_type(str_val) # inferred type: str - correct
    reveal_type(raw_str_val) # inferred type: str - correct

    error_key: ErrorStr = "error_key"
    error_val = ApplyGet(error_key).get(mapping) # return type unknown error - correct
    raw_error_val = ApplyGet("error_key").get(mapping) # return type unknown error - correct
    reveal_type(error_val) # inferred type: unknown - correct
    reveal_type(raw_error_val) # inferred type: unknown - correct

As Eric pointed out, mypy fully rejects it and it relies on the behaviour of the constraint system, which is currently completely unspecified in the type system. I think pyright merges a union of tuples into a tuple of unions at 64 types to avoid combinatorial explosion. It’s a neat way to push the type system though.

What I’d like is something like a “type pair” which I think is pretty close to your type mapping, that would allow the below:

from __future__ import annotations
from typing import Any, LiteralString

type TypePair = tuple[Any, Any]

class Mapping[KvT: TypePair]:
    def __getitem__[Kt, Vt, OtherT: TypePair](
        self: Mapping[tuple[Kt, Vt] | OtherT],
        key: Kt
    ) -> Vt:
        ...

type Mappings = tuple[int, str] | tuple[str, list[str]]
type MyMapping = Mapping[Mappings]
def use_get_fn(mapping: MyMapping):
    val_for_int_key = mapping[2]
    val_for_str_key= mapping["str_key"]

But without the indirection shown above pyright doesn’t resolve the constraints as might be expected (not an easy thing to do without slowing down).

1 Like