Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is list[str] an iterable?

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.

like image 778
Fallible Avatar asked Apr 30 '26 13:04

Fallible


1 Answers

Thanks to @anthonysotille and @SUTerliakov for the hints.

What we are seeing here is an unintended consequence of the decisions taken when designing Variadic Generics (PEP 646) support for 3.11. In particular the implementation of the Unpack operator * in type annotations that involve TypeVarTuple.

Unpack is

A typing operator that conceptually marks an object as having been unpacked. ...

In 3.10:

from typing_extension import Unpack
from typing import Tuple

>>> Tuple[int, str], type(Tuple[int, str]), Unpack[Tuple[int, str]]
(typing.Tuple[int, str] <class 'typing._GenericAlias'> typing_extensions.Unpack[typing.Tuple[int, str]])

We already know that generic aliases weren't iterables,

>>> list(list[str])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'types.GenericAlias' object is not iterable

because they didn't need to.

Unpack works with any generic, in fact any type:

>>> Unpack[list[int]], Unpack[str]
(typing_extensions.Unpack[list[int]] typing_extensions.Unpack[str])

The runtime expression of Unpack is very lean, as any other typing construct, involving just the typing module. Although type hints are useful outside type context, Python try very hard not to impact runtime performance.

In 3.11, however, Python maintainers decided to use the star operator * as a syntactic sugar for Unpack, which required minor grammar changes and implementing __iter__ for generic aliases, given * calls __iter__ on the callee . For List, Tuple and old-schools generics, changing typing._GenericAlias is enough, but list and the rest of buit-int containers types need changing types.GenericAlias in CPython. This has implications outside type contexts, see details here.

>>> Tuple[int, str], type(Tuple[int, str]), [*Tuple[int, str]]
(typing.Tuple[int, str] <class 'typing._GenericAlias'> [*typing.Tuple[int, str]])

>>> list[str], type(list[str]), [*list[str]], [Unpack[list[str]]]
(list[str] <class 'types.GenericAlias'> [*list[str]] [*list[str]])

When unpacking, GenericAlias.__iter__ simply returns another instance marked as unpacked.

>>> type(list(List[str])[0]), hasattr(list(List[str])[0], '__unpacked__')
(<class 'typing._UnpackGenericAlias'>, False)  

>>> type(list(list[str])[0]), list(list[str])[0].__unpacked__
(<class 'types.GenericAlias'>, True)

In conclusion, the reason why GenericAlias instances are iterables in Python 3.11 is due to the implementation of the Unpack operator, which required minor grammar changes and implementing __iter__ for generic aliases. For further details and implications, please refer to the resources provided in the answer.

like image 116
Fallible Avatar answered May 02 '26 04:05

Fallible



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!