Python 3.10 doesn't think so:
Python 3.10.6 | packaged by conda-forge | (main, Aug 22 2022, 20:38:29) [Clang 13.0.1 ] \
on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from typing import Iterable
>>> isinstance(list[str], Iterable)
False
>>> list(list[str])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'types.GenericAlias' object is not iterable
Python 3.11 considers it is:
Python 3.11.0 | packaged by conda-forge | (main, Jan 15 2023, 05:44:48) [Clang 14.0.6 ] \
on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from typing import Iterable
>>> isinstance(list[str], Iterable)
True
>>> list(list[str])
[*list[str]]
If it is an iterable, what should be the result of iterating over it? The *list[str]
item appears to be the unpacking
of itself or of a type variable tuple.
What's going on here? I know that typing in python is in state of flux and evolving rapidly, but I really don't know how to interpret this.
Update: Fixed typo in 3.10 example as noted by Daniil Fajnberg
Update: I didn't want a long post for a seemingly edge issue, but I suppose a bit of background is necessary.
I know of at least one case this has caused problems. I frequently use the Fastcore lib. In notebooks environments, the test_eq module is handy for documenting/testing code. The test_eq
helper function checks for equality (==
).
Up until version 3.11, the following code was okay:
test_eq(_generic_order((list[str],)), (list[str],))
(_generic_order is a function that order annotations by "genericity", which is not important now)
In version 3.11:
test_eq(_generic_order((list[str],)), (list[str],))
RecursionError Traceback (most recent call last)
Cell In[81], line 1
----> 1 test_eq(_generic_order((list[str],)), (list[str],))
...
File ~/dev/repo/project/rei/.micromamba/envs/rei/lib/python3.11/site-packages/fastcore/imports.py:33, in <genexpr>(.0)
31 "Compares whether `a` and `b` are the same length and have the same contents"
32 if not is_iter(b): return a==b
---> 33 return all(equals(a_,b_) for a_,b_ in itertools.zip_longest(a,b))
...
File ~/dev/repo/project/rei/.micromamba/envs/rei/lib/python3.11/typing.py:1550, in _SpecialGenericAlias.__subclasscheck__(self, cls)
1548 return issubclass(cls.__origin__, self.__origin__)
1549 if not isinstance(cls, _GenericAlias):
-> 1550 return issubclass(cls, self.__origin__)
1551 return super().__subclasscheck__(cls)
File <frozen abc>:123, in __subclasscheck__(cls, subclass)
RecursionError: maximum recursion depth exceeded in comparison
test_eq
checks internally whether the args are iterable
and compares item-wise. As list[str]
is an iterable that iterates over itself, recursion hell.
If test_eq
wants to be truly general probably should guard against recursion. But it's understable that a case where a container contains itself at first level is rare. And test_eq
is not the focus of the question, just an example of a problem generated by an undocumented 3.11 change.
Note that the old generics (List
, Tuple
, etc) use the typing
module, but the built-in types (list
, tuple
) implement GenericAlias
in C. Thus, a Python maintainer has put substantial effort into changing the behavior of GenericAlias
to make them iterables, and not only that, but iterables of themselves. There isn't a single mention of this fact in the documentation or change logs (and no time yet to delve into CPython for git comments).
Is it an edge case? Probably. Python docs frequently warn us that using type annotations for purposes other than type hinting is discouraged. At the same time, type hint introspection becomes more powerful with every Python version.
Around the time typing was introduced, we also obtained dataclasses that are not only useful data containers by themselves but also elegant examples of leveraging type annotations dynamically at runtime. Pydantic and an ever-growing number of tools are using annotations to check/change code, and I have found myself using them more and more in my own code.
We put a lot of effort into typing because it's a powerful and useful tool, one that is likely useful aside from type checking. As a developer, I want to understand my tools, particularly one as important as typing.
So, the question remains: i'm curious about not how, which is trivial, but why did GenericAlias suddenly become iterable?