Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 55 additions & 49 deletions peps/pep-0798.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,33 +97,38 @@ equivalent to ``(x async for ait in aits() for x in ait)``.
Rationale
=========

Combining iterable objects together into a single larger object is a common
task. One `StackOverflow post
Combining multiple iterable objects together into a single object is a common
task. For example, one `StackOverflow post
<https://stackoverflow.com/questions/952914/how-do-i-make-a-flat-list-out-of-a-list-of-lists>`_
asking about flattening a list of lists, for example, has been viewed 4.6
million times. Despite this being a common operation, the options currently
available for performing it concisely require levels of indirection that can
make the resulting code difficult to read and understand.

The proposed notation is concise (avoiding the use and repetition of auxiliary
variables) and, we expect, intuitive and familiar to programmers familiar with
both comprehensions and unpacking notation (see :ref:`pep798-examples` for
examples of code from the standard library that could be rewritten more clearly
and concisely using the proposed syntax).

This proposal was motivated in part by a written exam in a Python programming
class, where several students used the notation (specifically the ``set``
version) in their solutions, assuming that it already existed in Python. This
suggests that the notation is intuitive, even to beginners. By contrast, the
existing syntax ``[x for it in its for x in it]`` is one that students often
get wrong, the natural impulse for many students being to reverse the order of
the ``for`` clauses.

Additionally, the comment section of a `Reddit post
asking about flattening a list of lists has been viewed 4.6 million times, and
there are several examples of code from the standard library that perform this
operation (see :ref:`pep798-examples`). While Python provides a means of
combining a small, known number of iterables using extended unpacking from
:pep:`448`, no comparable syntax currently exists for combining an arbitrary
number of iterables.

This proposal represents a natural extension of the language, paralleling
existing syntactic structures: where ``[x, y, z]`` creates a list from a fixed
number of vaues, ``[item for item in items]`` creates a list from an arbitrary
number of values; this proposal extends that notion to the construction of
lists that involve unpacking, making ``[*item for item in items]`` analogous to
``[*x, *y, *z]``.

We expect this syntax to be intuitive and familiar to programmers already
comfortable with both comprehensions and unpacking notation. This proposal was
motivated in part by a written exam in a Python programming class, where
several students used the proposed notation (specifically the ``set`` version)
in their solutions, assuming that it already existed in Python. This suggests
that the notation represents a logical, consistent extension to Python's
existing syntax. By contrast, the existing double-loop version ``[x for it in
its for x in it]`` is one that students often get wrong, the natural impulse
for many students being to reverse the order of the ``for`` clauses. The
intuitiveness of the proposed syntax is further supported by the comment
section of a `Reddit post
<https://old.reddit.com/r/Python/comments/1m607oi/pep_798_unpacking_in_comprehensions/>`__
following the publication of this PEP shows substantial support for the
proposal and further suggests that the syntax proposed here is legible,
intuitive, and useful.
made following the initial publication of this PEP, which demonstrates support
from a broader community.


Specification
=============
Expand Down Expand Up @@ -412,9 +417,9 @@ Code Examples
=============

This section shows some illustrative examples of how small pieces of code from
the standard library could be rewritten to make use of this new syntax to
improve concision and readability. The :ref:`pep798-reference` continues to
pass all tests with these replacements made.
the standard library could be rewritten to make use of this new syntax. The
:ref:`pep798-reference` continues to pass all tests with these replacements
made.

Replacing Explicit Loops
------------------------
Expand All @@ -430,7 +435,7 @@ need for defining and referencing an auxiliary variable.
comments.extend(token.comments)
return comments

# improved:
# proposed:
return [*token.comments for token in self]

* From ``shutil.py``::
Expand All @@ -441,7 +446,7 @@ need for defining and referencing an auxiliary variable.
ignored_names.extend(fnmatch.filter(names, pattern))
return set(ignored_names)

# improved:
# proposed:
return {*fnmatch.filter(names, pattern) for pattern in patterns}

* From ``http/cookiejar.py``::
Expand All @@ -452,7 +457,7 @@ need for defining and referencing an auxiliary variable.
cookies.extend(self._cookies_for_domain(domain, request))
return cookies

# improved:
# proposed:
return [
*self._cookies_for_domain(domain, request)
for domain in self._cookies.keys()
Expand All @@ -463,8 +468,8 @@ Replacing from_iterable and Friends

While not always the right choice, replacing ``itertools.chain.from_iterable``
and ``map`` can avoid an extra level of redirection, resulting in code that
follows conventional wisdom that comprehensions are more readable than
map/filter.
follows conventional wisdom that comprehensions are generally more readable
than map/filter.

* From ``dataclasses.py``::

Expand All @@ -473,7 +478,7 @@ map/filter.
itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
)

# improved:
# proposed:
inherited_slots = {*_get_slots(c) for c in cls.__mro__[1:-1]}

* From ``importlib/metadata/__init__.py``::
Expand All @@ -483,23 +488,23 @@ map/filter.
path.search(prepared) for path in map(FastPath, paths)
)

# improved:
# proposed:
return (*FastPath(path).search(prepared) for path in paths)

* From ``collections/__init__.py`` (``Counter`` class)::

# current:
return _chain.from_iterable(_starmap(_repeat, self.items()))

# improved:
# proposed:
return (*_repeat(elt, num) for elt, num in self.items())

* From ``zipfile/_path/__init__.py``::

# current:
parents = itertools.chain.from_iterable(map(_parents, names))

# improved:
# proposed:
parents = (*_parents(name) for name in names)

* From ``_pyrepl/_module_completer.py``::
Expand All @@ -510,7 +515,7 @@ map/filter.
for spec in specs if spec
))

# improved:
# proposed:
search_locations = {
*getattr(spec, 'submodule_search_locations', [])
for spec in specs if spec
Expand All @@ -520,30 +525,30 @@ Replacing Double Loops in Comprehensions
----------------------------------------

Replacing double loops in comprehensions avoids the need for defining and
referencing an auxiliary variable, reducing clutter.
referencing an auxiliary variable.

* From ``importlib/resources/readers.py``::

# current:
children = (child for path in self._paths for child in path.iterdir())

# improved:
# proposed:
children = (*path.iterdir() for path in self._paths)

* From ``asyncio/base_events.py``::

# current:
exceptions = [exc for sub in exceptions for exc in sub]

# improved:
# proposed:
exceptions = [*sub for sub in exceptions]

* From ``_weakrefset.py``::

# current:
return self.__class__(e for s in (self, other) for e in s)

# improved:
# proposed:
return self.__class__(*s for s in (self, other))


Expand Down Expand Up @@ -773,9 +778,9 @@ Beyond the proposal outlined above, the following were also considered:
This strategy would also make unpacking in synchronous and asynchronous
generators behave symmetrically, but it would also be more complex, enough
so that the cost may not be worth the benefit. As such, this PEP proposes
that generator expressions using the unpacking operator should not use
semantics similar to ``yield from`` until ``yield from`` is supported in
asynchronous generators more generally.
that asynchronous generator expressions using the unpacking operator should
not adopt semantics similar to ``yield from`` until ``yield from`` is
supported in asynchronous generators more generally.

3. Using ``yield from`` for unpacking in synchronous generator expressions, and
disallowing unpacking in asynchronous generator expressions until they
Expand Down Expand Up @@ -817,8 +822,9 @@ this syntax was clear and intuitive, several concerns and potential downsides
were raised as well. This section aims to summarize those concerns.

* **Overlap with existing alternatives:**
While the proposed syntax is arguably clearer and more concise, there are
already several ways to accomplish this same thing in Python.
While the proposed syntax represents a consistent extension to the language
and is likely to result in more-concise code, there are already several ways
to accomplish this same thing in Python.

* **Function call ambiguity:**
Expressions like ``f(*x for x in y)`` may initially appear ambiguous, as it's
Expand All @@ -828,13 +834,13 @@ were raised as well. This section aims to summarize those concerns.
may not be immediately obvious.

* **Potential for overuse or abuse:**
Complex uses of unpacking in comprehensions could obscure logic that would be
Complex uses of unpacking in comprehensions could obscure logic that may be
clearer in an explicit loop. While this is already a concern with
comprehensions more generally, the addition of ``*`` and ``**`` may make
particularly complex uses even more difficult to read and understand at a
glance. For example, while these situations are likely rare, comprehensions
that use unpacking in multiple ways can make it difficult to know what's
being unpacked and when: ``f(*(*x for *x, _ in list_of_lists))``.
being unpacked and when, e.g., ``f(*(*x for *x, _ in list_of_lists))``.

* **Unclear limitation of scope:**
This proposal restricts unpacking to the top level of the comprehension
Expand Down