Repository navigation
Map with bind: match Tuple of parameterized types with parameters from TypeVarTuple #1383
Description
Prior work in PEP-646 on Map was dropped to simplify it, with a promise it would appear in a future PEP. Here is a reference to the dropped Map feature: #193 (comment)
Here is an discussion of a potential usage of this Map feature for matching argument types for a length-n Tuple of unary Callables with a length-n Tuple parameterized by a TypeVarTuple: https://groups.google.com/g/dev-python/c/SbPOxIEvI60?pli=1
Here is a draft spec of PEP-646 that included discussion of Map: https://github.com/python/peps/blob/bf897f8c839d1b4d4534ab1fa223a210e2cacf06/pep-0646.rst
I would like to propose an extension of Map with a "bind" argument to allow disambiguation in case the parameterized type has multiple generic parameters. To give an example:
###########
## Converting from multiple domains to a common codomain
############
Domain = TypeVar('Domain')
Codomain = TypeVar('Codomain')
class Detector(Generic(Domain)):
def __init__(self, detector_fn: Callable[[Any], Optional[Domain]):
self.detector_fn = detector_fn
def detect(arg: Any) -> Optional[Domain]
# returns `arg` if `arg` belongs to Domain, otherwise returns None
return self.detector_fn(arg)
class Converter(Generic[Domain, Codomain]):
def __init__(self, converter_fn: Callable[[Domain], Codomain]):
self.converter_fn = converter_fn
def convert(arg: Domain) -> Codomain:
return self.converter_fn(arg)
Domains = TypeVarTuple('Domains')
class ConverterCollection(Generic[*Domains, Codomain]):
def __init__(self, detectors: Tuple[*Map[Detector, Domains]], converters: Tuple[*Map[Converter, Domains, bind=Domain]]):
self.detectors = detectors
self.converters = converters
def convert(self, obj_to_convert: Union[*Domains]) -> Codomain:
for detector, converter in zip(self.detectors, self.converters):
detected_object = detector.detect(obj_to_convert)
if not (detected_object is None):
return converter.convert(object_to_convert)
raise ValueError('No converter for object.')
### Usage
int_detector = Detector[int](lambda x: x if isinstance(x, int) else None)
int_converter = Converter[int, str](lambda x: json.dumps(x))
float_detector = Detector[float](lambda x: x if isinstance(x, float) else None)
float_converter = Converter[float, str](lambda x: json.dumps(x))
cc = ConverterCollection[int, float, str](
(int_detector, float_detector),
(int_converter, float_converter)
)
cc.convert(2) # works
cc.convert(2.0) # also works
cc.convert('hi') # type errorThe bind=Domain argument here would be because Converter class is generic in both Domain and Codomain, so there is ambiguity about which generic parameter to Map. This is not necessary for Detector because, because it is generic only over Domain, so there is no ambiguity what to map over.
Activity
erictraut commentedon Apr 3, 2023
There are a couple of problems with your proposed solution.
First, keyword arguments are not allowed syntactically within an index expression. Support for this was proposed but unfortunately rejected in PEP 637.
Second, you've proposed associating the "bound" parameter with a reference to a
TypeVarTuplethat has no meaning in the context in which it's used.TypeVarTuple(likeTypeVarandParamSpec) must be used only within a context in which they are bound to a valid scope (a generic class, function or type alias). You are using theTypeVarTuplecalledDomainin a context where it has no valid meaning. Does it refer to theDomainscoped to theDetectorclass? Some other scope?Scoping of TypeVars is admittedly confusing and poorly understood by many Python users. This is partly why we are pursuing PEP 695.
jmsmdy commentedon Apr 3, 2023
I am not committed to that syntax. Mostly I would just like some kind of operation like Map (or similar) to allow expressing element-wise relationships between types in two related variadic tuples. The "bind=" here is because in this case
Converterclass has two parameters, and it is ambiguous which should be mapped over.It would refer unambiguously to the
Domainscoped in theConverterclass (the first argument to Map). The previously proposedMap[MyGeneric, MyTypeVarTuple]from the draft of PEP-646 would already implicitly pick out the unique free parameter inMyGeneric, assuming there is exactly one free parameter. I just want to extend this by allowingMyGenericto be doubly-generic (two parameters) or triple-generic (three parameters) etc. and specify which free parameter inMyGenericis to be used to map over the types inMyTypeVarTuple.For example, the draft of PEP-646 would allow
Map[List, MyTypeVarTuple], which whenMyTypeVarTupleis instantiated withTuple[str, int, int, float]would giveTuple[list[str], list[int], list[int], list[float]].But the draft of PEP-646 would not allow
Map[Mapping, MyTypeVarTuple], because the generic Mapping type has two free parameters. It is ambiguous whetherMap[Mapping[T,S], MyTypeVarTuple]would resolve toTuple[Mapping[str, S], Mapping[int, S], Mapping[int, S], Mapping[float, S]]orTuple[Mapping[T, str], Mapping[T, int], Mapping[T, int], Mapping[T, float](hereMappingis just an example of a generic type with more than one parameter).This looks interesting. I also understand the desire to avoid creating a Turing-complete / unsolvable type system, which limits what programmatic typing features can be safely added. Map would obviously add to the complexity somewhat, but I don't think it poses too much of a risk.
To give more philosophical motivation: it is already possible to type the example I gave above by something like:
But now you have not constrained there to be the same number of detectors and converters, and you have not constrained the types for each detector to match with the type of its corresponding converter. I am really not dogmatic about the exact solution, but the general idea is that you would want to be able to impose constraints on variadic generic types in some kind of simple language (maybe a relational language). The role of "Map" is just to impose the above constraints on the types of detectors and converters. It could be filled by any other way of expressing that these two variadic generic types are to have the same length and such-and-such parameters should be related in such-and-such way. Another way of doing this might be to have an explicit language for type constraints, for example a syntax like this:
A generic type like
SelfMapping = Mapping[T, T]already implicitly has a constraint from coreference, and could be expressed in such a language like this:So the idea is just to allow more sophisticated constraints than is allowed by coreference to the same TypeVar. I'm not proposing this (it is very ugly and has problems), just trying to illustrate that the typing feature I am interested in follows a kind of "multiply, then constrain" paradigm, where the two main operations for constructing new generic types are to make multiple new generic types (which are by default totally independent and unrelated), and then constrain those generic types by imposing relations on them. I believe this kind of paradigm is unlikely to create an unsolvable type system, as long as the constraints can be computed. I view
Mapas a more functional way of expressing the relation that "this parameter of this generic type should be equal to that parameter of this other generic type".To focus the discussion (since I do not want to try to argue for a syntax change that has already been rejected):
Mapstill planned for a future PEP (as it was circa 2021), or has it been officially reviewed and rejected?Converters[i] = Converter[Ts[i], S]where some but not all free parameters are subject to constraints (relational or otherwise)?For the record, I don't have any great knowledge of type systems, nor do I have strong opinions on the right solution here. I opened this issue mostly to follow up with the seemingly forgotten promise of a future PEP for Map.
erictraut commentedon Apr 4, 2023
Mapwas removed from PEP 646 because the spec was already very complex, and there were concerns that it would take type checkers a long time to implement it. Indeed, mypy still has not implemented full support for PEP 646. Until this happens, we will likely see little or no adoption of PEP 646 in any major library. Once mypy releases full support for PEP 646, we should start to see more adoption of variadic type vars, and the typing community will then be in a better position to assess the need (and prioritization) for further extensions likeMap.If individuals like you feel like there's a compelling need for
Map, it might make sense to start discussing and formalizing the spec for such a facility in the form of a proto-PEP.mikeshardmind commentedon Aug 28, 2023
A motivating use case, without full detail here, if it helps to have simple motivating examples supporting this down the line.
Be3y4uu-K0T commentedon Sep 10, 2023
I would suggest using an unpacked TypeVarTuple with subsequent unpacking GenericAlias (or special class for this PackedGenericAlias):
insilications commentedon Oct 4, 2023
It was sad to see Map operation removed from PEP 646 (but understandable).
Python is not my "main language", but when I do I prefer typing it and I've seen the need for such an operation.
HAOCHENYE commentedon Dec 18, 2025
Ray's type system urgently needs support for Map-like syntax. For example, for any function foo
Ray hopes that after being decorated, the function can derive the following two function signatures:
To achieve this effect, Ray implemented a flawed solution using this approach: https://github.com/ray-project/ray/blob/631684a13067b15d95117ec097b6f3c6c1a44554/python/ray/actor.py#L93
Would be very happy to see the Map feature implemented!
MajorDallas commentedon Feb 20, 2026
Another motivating example:
more_itertools.unzipcurrently has this type signature:If you feed it an iterable of tuples with heterogeneous types, however, the type information is lost. Mypy erases the types entirely; Pyright collapses them to a union:
Without something like
Map, Pyright is doing the only reasonable thing to avoid completely erasing the type information. Unfortunately, that doesn't save me from needing acastlater before passing these lists to other functions.With
Map, if I'm understanding the examples in #1383 (comment), the type signature could be something like:This (assuming I've done it correctly) should enable the type checker to correctly assign
list[str]topopulationsandlist[float]toweights, or to handle cases where the tuple has 3, 4, ..., or n elements.All that said, I feel like "Map" is too easy to confuse with
Mapping, both lexically and semantically. "Distribute" seems semantically appropriate (in its mathematical sense), but I'll be the first to say I do not want to type that. "Unpack" is taken... Maybe "Spread," as it both sort of means the same as "distribute" and is a term used for equivalent operations to "unpacking" in other languages (eg. JS' "spread operator"). To be fair, "Map" vs "Mapping" isn't as confusing as "TypeGuard" vs "TypeIs," so it's probably fine.[-]Create PEP for Map with bind: match Tuple of parameterized types with parameters from TypeVarTuple[/-][+]Map with bind: match Tuple of parameterized types with parameters from TypeVarTuple[/+]