Repository navigation
Add __class_getitem__ to map builtin, PEP 585 #108761
Description
Feature or enhancement
Right now it is impossible to subscribe map:
» ./python.exe
Python 3.13.0a0 (heads/main-dirty:79823c103b6, Aug 31 2023, 15:46:08) [Clang 14.0.3 (clang-1403.0.22.14.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> map[str]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: type 'map' is not subscriptable
>>> I propose adding __class_getitem__ support to map, because it makes sense in some cases.
However, https://peps.python.org/pep-0585/ does not say anything about map.
First of all, map is defined as Generic in typeshed:
class map(Iterator[_S], Generic[_S]):
def __next__(self) -> _S: ...So, any person that wants to, say, return map[str] right now has to use 'map[str]' (via '' or via from __future__ import annotations).
Second, while we can treat map[T] as just Iterator[T] in parameter types, we always want to as specific as possible in return types.
Third, it is a builtin, so we assume that it is widely used. And it actually is, even in CPython's source itself we can find usages that can be annotated as map[whatever]:
cpython/Lib/zipfile/_path/__init__.py
Line 368 in 3047f09
return map(self._next, filter(matches, self.root.namelist())) cpython/Doc/library/itertools.rst
Line 808 in 3047f09
return map(function, count(start)) cpython/Doc/library/itertools.rst
Line 964 in 3047f09
return map(operator.getitem, repeat(seq), slices) cpython/Doc/library/itertools.rst
Line 1022 in 3047f09
return map(next, map(operator.itemgetter(1), groupby(iterable, key))) return map(self._follow, (locs for name, locs in groups)) Line 112 in 3047f09
return map(str.strip, alist) cpython/Lib/importlib/metadata/__init__.py
Line 540 in 3047f09
return map('"{}"'.format, paths) cpython/Lib/importlib/metadata/__init__.py
Line 787 in 3047f09
return map(PathDistribution, found)
Lastly, it is a one-line change (+ tests) and many other builtins and stdlib objects support that already.
I cannot really think of any major cons.
Activity
pythongh-108761: Add PEP585 support for `map`
AlexWaygood commentedon Sep 1, 2023
What about other builtin iterator classes, such as
reversedandfilter. Should support be added for those as well?sobolevn commentedon Sep 1, 2023
I don't know :)
This issue is just about
map, I will try to make my research on them in separate issues.I don't want to bundle them in case someone has objections about just one specific type.
Fidget-Spinner commentedon Sep 1, 2023
We had a similar discussion some time back for
callable, and there was some push back #86268sobolevn commentedon Sep 1, 2023
callableis a function,mapis a type.Plus,
Callableis a special form intyping.py, whilemap[T]is just a regular generic alias.So, I don't think that this is the same problem.
serhiy-storchaka commentedon Sep 1, 2023
What does it mean and in what cases the type of the map iterator cannot be derived from types of
map()arguments?sobolevn commentedon Sep 1, 2023
@serhiy-storchaka I am sorry, I don't fully understand your question.
I don't think that this is related to CPython, because runtime does not derive anything at all. Typecheckers have different inference logic, so it is up to them to derive
map's type arguments.serhiy-storchaka commentedon Sep 1, 2023
What does it mean for typecheckers and why they need it?
sobolevn commentedon Sep 1, 2023
mapcan already be used from type checkers' POV: https://mypy-play.net/?mypy=latest&python=3.11&gist=04a3192eecc77928ea6aa60f40604ef0But, we have to use string
'map[...]'orfrom __future__ import annotationsjust to make sure it won't fail in runtime withTypeError.serhiy-storchaka commentedon Sep 1, 2023
You didn't answer the question why you need it.
AlexWaygood commentedon Sep 1, 2023
An argument in favour is that this increases consistency with various other iterator classes that have
__class_getitem__defined, such asbuiltins.enumerateanditertools.chain:Additionally, adding the method would only cost two lines of C code, so the maintenance burden is not particularly high.
However, I don't think the motivation here is very strong. You say:
However, I can't think of an instance where using
map[T]as a return type would give the type checker any more information thanIterator[T]. Instances ofbuiltins.mapdon't have any attributes that distinguish them from instances of any other iterator class. While it's true that, in general, we should use the most specific annotation possible for a return type, I don't think this really holds true when it comes to iterators. We should take note of this comment at the top oftypes.pythat explains whylist_iterator(the type ofiter([])) and various other "builtin" iterators are not exposed in thetypesmodule:cpython/Lib/types.py
Lines 6 to 9 in 578ebc5
Unless instances of an iterator class have some special attributes that set them apart from other iterator objects, I tend to think that people should avoid using any specific iterator class as a return annotation, and just use
Iterator[]. The specific type of iterator being returned is generally an implementation detail, and rarely gives the type checker any extra information to work with. In other words, I think it was probably a mistake to add__class_getitem__to classes such asbuiltins.enumerate(but it's also not worth it to go through a deprecation cycle and remove them again; they don't do any particular harm).If we do want to add
__class_getitem__tomap, we should also add it to other builtin iterator classes such aszip,filterandreversed: any arguments for or against adding__class_getitem__tomapwould also apply to them, I think. It makes no sense to me to only add the method tomap, but not the others.sobolevn commentedon Sep 1, 2023
Since we have nominal type system,
mapandIteratorare different types. They are, especially, treated differently by runtime tools that use annotations, like: Hypothesis, beartype, typeguard, deal, etc.I general, I think that all type that are generic in typeshed should be generic in runtime. There's no real cost in doing so, but there's a big profit in making the DX pleasant for people who annotate their code.
But, if others don't agree for some reason: feel free to close, I don't care enough about this.
As I said, I think that this is a good thing, but I want to decouple these tasks.
I only have a very limited amount of energy :)
hauntsaninja commentedon Sep 1, 2023
I'm a little wary of doing this for
map, just because it's "type-ness" is not documented.I'd be less wary if I thought there was an especially compelling use case, but I haven't any instances of people using map as a type, instead of Iterator (also struggled to find any on grep.app just now). In fact, I think I'm still slightly opposed to the decision to type them as classes in typeshed, see python/typeshed#5145.
Also see faster-cpython/ideas#131. I think if we added
__class_getitem__we might not be able to make that change.Of course, this stops mattering in a PEP 718 world.
AlexWaygood commentedon Sep 1, 2023
You've given an example where a function that is annotated as returning
mapat runtime would currently cause problems for beartype, but you've yet to give an example where annotating a function as returningmap[T]would be demonstrably more correct, and would provide more information to the type checker, than simply annotating the function returning amapinstance as returningIterator[T]. As I mentioned above, in my experience, when a function returns an iterator object, the specific class of the iterator is usually an implementation detail, so the better course of action is usually to annotate the function as returningIterator[T]rather thanmap[T].Would beartype/Hypothesis/typeguard struggle with a
returns_mapfunction annotated like this? If so, that seems like a limitation of those tools to me, rather than something we need to fix in CPython:JelleZijlstra commentedon Sep 2, 2023
I think if PEP-718 is implemented the case for making
map()subscriptable becomes stronger.JelleZijlstra commentedon Sep 7, 2023
I'd like to punt the decision here until PEP 718 is resolved. If it is accepted, we should also add subscript support to
map(). If it is rejected, we probably shouldn't.mapis technically a class, but in terms of how it's used it's much more like a function, so I expect use cases for subscripting it mirror use cases for subscripting generic functions.If we make
mapgeneric at runtime, we should also do it forfilterandreversed, and probably some iterators in itertools. Currently among the four major builtin iterators, onlyenumerateis generic, butfilter,map, andreversedare not. I don't understand howenumerateis different from the others; seems like it was made generic in #19421 without much focused discussion.picnixz commentedon Sep 9, 2023
My 2 cents:
I didn't even know that
enumeratewas subscriptable. I don't know why the latter should be since it's actually somewhat equivalent tozip(count(start), it)andzipis not subscriptable neither.Having a
map[T]may expose some implementation detail that may be needed. Like "hey, here I used amapand not some other construction". But I think that rather having it subscriptable, we should actually not have allowedenumerateto be subscriptable in the first time. AFAICT,enumeratedoesn't expose more than whatIterator[tuple[int, T]]exposes, so it's only syntactic sugar for it to be subscriptable (like, it somehow reduces the number of characters to write and make it more readable, so maybe that was why it was made subscriptable in the first place). The same holds formap,filter,reversedand technically speaking,zipas well, although the gain in characters is somewhat arguable. None of them are subscriptable in the first place, so I would say that we shouldn't makemapsubscriptable if the others aren't.If PEP 718 is accepted, then all built-ins that are classes but are used like functions should be made subscriptable (namely all that were mentioned +
zip).Otherwise, if we want to stress that something was created using those builtins, I think the user should create something like that:
and use it in stubs or
if TYPE_CHECKINGguarded blocks. While objects annotated with this type are not guaranteed to passisinstance(it, map)checks, I don't think it's reasonable to actually perform such checks in the first place (at least I don't have any usecase where you need to do this).