Skip to content
Open
42 changes: 30 additions & 12 deletions Doc/reference/expressions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -266,17 +266,19 @@ called "displays", each of them in two flavors:
Common syntax elements for comprehensions are:

.. productionlist:: python-grammar
comprehension: `assignment_expression` `comp_for`
comprehension: `flexible_expression` `comp_for`
comp_for: ["async"] "for" `target_list` "in" `or_test` [`comp_iter`]
comp_iter: `comp_for` | `comp_if`
comp_if: "if" `or_test` [`comp_iter`]

The comprehension consists of a single expression followed by at least one
:keyword:`!for` clause and zero or more :keyword:`!for` or :keyword:`!if` clauses.
In this case, the elements of the new container are those that would be produced
by considering each of the :keyword:`!for` or :keyword:`!if` clauses a block,
nesting from left to right, and evaluating the expression to produce an element
each time the innermost block is reached.
:keyword:`!for` clause and zero or more :keyword:`!for` or :keyword:`!if`
clauses. In this case, the elements of the new container are those that would
be produced by considering each of the :keyword:`!for` or :keyword:`!if`
clauses a block, nesting from left to right, and evaluating the expression to
produce an element each time the innermost block is reached. If the expression
is starred, the result will instead be unpacked to produce zero or more
elements.

However, aside from the iterable expression in the leftmost :keyword:`!for` clause,
the comprehension is executed in a separate implicitly nested scope. This ensures
Expand Down Expand Up @@ -321,6 +323,9 @@ See also :pep:`530`.
asynchronous functions. Outer comprehensions implicitly become
asynchronous.

.. versionchanged:: next
Unpacking with the ``*`` operator is now allowed in the expression.


.. _lists:

Expand Down Expand Up @@ -396,8 +401,8 @@ enclosed in curly braces:
.. productionlist:: python-grammar
dict_display: "{" [`dict_item_list` | `dict_comprehension`] "}"
dict_item_list: `dict_item` ("," `dict_item`)* [","]
dict_comprehension: `dict_item` `comp_for`
dict_item: `expression` ":" `expression` | "**" `or_expr`
dict_comprehension: `expression` ":" `expression` `comp_for`

A dictionary display yields a new dictionary object.

Expand All @@ -419,10 +424,21 @@ earlier dict items and earlier dictionary unpackings.
.. versionadded:: 3.5
Unpacking into dictionary displays, originally proposed by :pep:`448`.

A dict comprehension, in contrast to list and set comprehensions, needs two
expressions separated with a colon followed by the usual "for" and "if" clauses.
When the comprehension is run, the resulting key and value elements are inserted
in the new dictionary in the order they are produced.
A dict comprehension may take one of two forms:

- The first form uses two expressions separated with a colon followed by the
usual "for" and "if" clauses. When the comprehension is run, the resulting
key and value elements are inserted in the new dictionary in the order they
are produced.

- The second form uses a single expression prefixed by the ``**`` dictionary
unpacking operator followed by the usual "for" and "if" clauses. When the
comprehension is evaluated, the expression is evaluated and then unpacked,
inserting zero or more key/value pairs into the new dictionary.

Both forms of dictionary comprehension retain the property that if the same key
is specified multiple times, the associated value in the resulting dictionary
will be the last one specified.

.. index:: pair: immutable; object
hashable
Expand All @@ -439,6 +455,8 @@ prevails.
the key. Starting with 3.8, the key is evaluated before the value, as
proposed by :pep:`572`.

.. versionchanged:: next
Unpacking with the ``**`` operator is now allowed in dictionary comprehensions.

.. _genexpr:

Expand All @@ -453,7 +471,7 @@ Generator expressions
A generator expression is a compact generator notation in parentheses:

.. productionlist:: python-grammar
generator_expression: "(" `expression` `comp_for` ")"
generator_expression: "(" `flexible_expression` `comp_for` ")"

A generator expression yields a new generator object. Its syntax is the same as
for comprehensions, except that it is enclosed in parentheses instead of
Expand Down
19 changes: 19 additions & 0 deletions Doc/tutorial/classes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,25 @@ Examples::
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

>>> x = [[1,2,3], [], [4, 5]]
>>> g = (*i for i in x)
>>> list(g)
[1, 2, 3, 4, 5]

In most cases, generator expressions must be wrapped in parentheses. As a
special case, however, when provided as the sole argument to a function (as in
the examples involving ``sum``, ``set``, ``max``, and ``list`` above), the
generator expression does not need to be wrapped in an additional set of
parentheses. That is to say, the following two pieces of code are semantically
equivalent::

>>> f(x for x in y)
>>> f((x for x in y))

as are the following::

>>> f(*x for x in y)
>>> f((*x for x in y))


.. rubric:: Footnotes
Expand Down
64 changes: 62 additions & 2 deletions Doc/tutorial/datastructures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,47 @@ The :func:`zip` function would do a great job for this use case::

See :ref:`tut-unpacking-arguments` for details on the asterisk in this line.

Unpacking in Lists and List Comprehensions
------------------------------------------

The section on :ref:`tut-unpacking-arguments` describes the use of ``*`` to
"unpack" the elements of an iterable object, providing each one seperately as
an argument to a function. Unpacking can also be used in other contexts, for
example, when creating lists. When specifying elements of a list, prefixing an
expression by a ``*`` will unpack the result of that expression, adding each of
its elements to the list we're creating::

>>> x = [1, 2, 3]
>>> [0, *x, 4, 5, 6]
[0, 1, 2, 3, 4, 5, 6]

This only works if the expression following the ``*`` evaluates to an iterable
object; trying to unpack a non-iterable object will raise an exception::

>>> x = 1
>>> [0, *x, 2, 3, 4]
Traceback (most recent call last):
File "<python-input-1>", line 1, in <module>
[0, *x, 2, 3, 4]
TypeError: Value after * must be an iterable, not int

Unpacking can also be used in list comprehensions, as a way to build a new list
representing the concatenation of an arbitrary number of iterables::

>>> x = [[1, 2, 3], [4, 5, 6], [], [7], [8, 9]]
>>> [*element for element in x]
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Note that the effect is that each element from ``x`` is unpacked. This works
for arbitrary iterable objects, not just lists::

>>> x = [[1, 2, 3], 'cat', {'spam': 'eggs'}]
>>> [*element for element in x]
[1, 2, 3, 'c', 'a', 't', 'spam']

But if the objects in ``x`` are not iterable, this expression would again raise
an exception.

.. _tut-del:

The :keyword:`!del` statement
Expand Down Expand Up @@ -394,7 +435,10 @@ A tuple consists of a number of values separated by commas, for instance::
>>> v = ([1, 2, 3], [3, 2, 1])
>>> v
([1, 2, 3], [3, 2, 1])

>>> # they support unpacking just like lists:
>>> x = [1, 2, 3]
>>> 0, *x, 4
(0, 1, 2, 3, 4)

As you see, on output tuples are always enclosed in parentheses, so that nested
tuples are interpreted correctly; they may be input with or without surrounding
Expand Down Expand Up @@ -480,12 +524,16 @@ Here is a brief demonstration::
{'r', 'd', 'b', 'm', 'z', 'l'}

Similarly to :ref:`list comprehensions <tut-listcomps>`, set comprehensions
are also supported::
are also supported, including comprehensions with unpacking::

>>> a = {x for x in 'abracadabra' if x not in 'abc'}
>>> a
{'r', 'd'}

>>> fruits = [{'apple', 'avocado', 'apricot'}, {'banana', 'blueberry'}]
>>> {*fruit for fruit in fruits}
{'blueberry', 'banana', 'avocado', 'apple', 'apricot'}


.. _tut-dictionaries:

Expand Down Expand Up @@ -563,6 +611,18 @@ arbitrary key and value expressions::
>>> {x: x**2 for x in (2, 4, 6)}
{2: 4, 4: 16, 6: 36}

And dictionary unpacking (via ``**``) can be used to merge multiple
dictionaries::

>>> odds = {i: i**2 for i in (1, 3, 5)}
>>> evens = {i: i**2 for i in (2, 4, 6)}
>>> {**odds, **evens}
{1: 1, 3: 9, 5: 25, 2: 4, 4: 16, 6: 36}

>>> all_values = [odds, evens, {0: 0}]
>>> {**i for i in all_values}
{1: 1, 3: 9, 5: 25, 2: 4, 4: 16, 6: 36, 0: 0}

When the keys are simple strings, it is sometimes easier to specify pairs using
keyword arguments::

Expand Down
67 changes: 48 additions & 19 deletions Grammar/python.gram
Original file line number Diff line number Diff line change
Expand Up @@ -709,12 +709,17 @@ expressions[expr_ty]:
| expression

expression[expr_ty] (memo):
| invalid_if_expression
| invalid_expression
| invalid_legacy_expression
| a=disjunction 'if' b=disjunction 'else' c=expression { _PyAST_IfExp(b, a, c, EXTRA) }
| if_expression
| disjunction
| lambdef

if_expression[expr_ty]:
| a=disjunction 'if' b=disjunction 'else' c=expression { _PyAST_IfExp(b, a, c, EXTRA) }
| invalid_if_expression

yield_expr[expr_ty]:
| 'yield' 'from' a=expression { _PyAST_YieldFrom(a, EXTRA) }
| 'yield' a=[star_expressions] { _PyAST_Yield(a, EXTRA) }
Expand All @@ -731,10 +736,16 @@ star_expression[expr_ty] (memo):

star_named_expressions[asdl_expr_seq*]: a[asdl_expr_seq*]=','.star_named_expression+ [','] { a }

star_named_expressions_sequence[asdl_expr_seq*]: a[asdl_expr_seq*]=','.star_named_expression_sequence+ [','] { a }

star_named_expression[expr_ty]:
| '*' a=bitwise_or { _PyAST_Starred(a, Load, EXTRA) }
| named_expression

star_named_expression_sequence[expr_ty]:
| invalid_starred_expression_unpacking_sequence
| star_named_expression

assignment_expression[expr_ty]:
| a=NAME ':=' ~ b=expression {
CHECK_VERSION(expr_ty, 8, "Assignment expressions are",
Expand Down Expand Up @@ -882,9 +893,9 @@ atom[expr_ty]:
| 'None' { _PyAST_Constant(Py_None, NULL, EXTRA) }
| &(STRING|FSTRING_START|TSTRING_START) strings
| NUMBER
| &'(' (tuple | group | genexp)
| &'[' (list | listcomp)
| &'{' (dict | set | dictcomp | setcomp)
| &'(' (genexp | tuple | group)
| &'[' (listcomp | list)
| &'{' (dictcomp | setcomp | dict | set)
| '...' { _PyAST_Constant(Py_Ellipsis, NULL, EXTRA) }

group[expr_ty]:
Expand Down Expand Up @@ -998,13 +1009,13 @@ strings[expr_ty] (memo):
| a[asdl_expr_seq*]=tstring+ { _PyPegen_concatenate_tstrings(p, a, EXTRA) }

list[expr_ty]:
| '[' a=[star_named_expressions] ']' { _PyAST_List(a, Load, EXTRA) }
| '[' a=[star_named_expressions_sequence] ']' { _PyAST_List(a, Load, EXTRA) }

tuple[expr_ty]:
| '(' a=[y=star_named_expression ',' z=[star_named_expressions] { _PyPegen_seq_insert_in_front(p, y, z) } ] ')' {
| '(' a=[y=star_named_expression_sequence ',' z=[star_named_expressions_sequence] { _PyPegen_seq_insert_in_front(p, y, z) } ] ')' {
_PyAST_Tuple(a, Load, EXTRA) }

set[expr_ty]: '{' a=star_named_expressions '}' { _PyAST_Set(a, EXTRA) }
set[expr_ty]: '{' a=star_named_expressions_sequence '}' { _PyAST_Set(a, EXTRA) }

# Dicts
# -----
Expand Down Expand Up @@ -1040,20 +1051,20 @@ for_if_clause[comprehension_ty]:
| invalid_for_target

listcomp[expr_ty]:
| '[' a=named_expression b=for_if_clauses ']' { _PyAST_ListComp(a, b, EXTRA) }
| '[' a=star_named_expression b=for_if_clauses ']' { _PyAST_ListComp(a, b, EXTRA) }
| invalid_comprehension

setcomp[expr_ty]:
| '{' a=named_expression b=for_if_clauses '}' { _PyAST_SetComp(a, b, EXTRA) }
| '{' a=star_named_expression b=for_if_clauses '}' { _PyAST_SetComp(a, b, EXTRA) }
| invalid_comprehension

genexp[expr_ty]:
| '(' a=( assignment_expression | expression !':=') b=for_if_clauses ')' { _PyAST_GeneratorExp(a, b, EXTRA) }
| '(' a=( assignment_expression | expression !':=' | starred_expression ) b=for_if_clauses ')' { _PyAST_GeneratorExp(a, b, EXTRA) }
| invalid_comprehension

dictcomp[expr_ty]:
| '{' a=kvpair b=for_if_clauses '}' { _PyAST_DictComp(a->key, a->value, b, EXTRA) }
| invalid_dict_comprehension
| '{' '**' a=expression b=for_if_clauses '}' { _PyAST_DictComp(a, NULL, b, EXTRA) }

# FUNCTION CALL ARGUMENTS
# =======================
Expand Down Expand Up @@ -1262,6 +1273,12 @@ invalid_expression:
| a='lambda' [lambda_params] b=':' &TSTRING_MIDDLE {
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "t-string: lambda expressions are not allowed without parentheses") }

invalid_if_expression:
| disjunction 'if' b=disjunction 'else' a='*' {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "cannot unpack only part of a conditional expression") }
| disjunction 'if' b=disjunction 'else' a='**' {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "cannot use dict unpacking on only part of a conditional expression") }

invalid_named_expression(memo):
| a=expression ':=' expression {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(
Expand Down Expand Up @@ -1326,16 +1343,15 @@ invalid_assert_stmt:
invalid_block:
| NEWLINE !INDENT { RAISE_INDENTATION_ERROR("expected an indented block") }
invalid_comprehension:
| ('[' | '(' | '{') a=starred_expression for_if_clauses {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "iterable unpacking cannot be used in comprehension") }
| '[' a='**' b=expression for_if_clauses {
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "cannot use dict unpacking in list comprehension") }
| '(' a='**' b=expression for_if_clauses {
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "cannot use dict unpacking in generator expression") }
| ('[' | '{') a=star_named_expression ',' b=star_named_expressions for_if_clauses {
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, PyPegen_last_item(b, expr_ty),
"did you forget parentheses around the comprehension target?") }
| ('[' | '{') a=star_named_expression b=',' for_if_clauses {
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "did you forget parentheses around the comprehension target?") }
invalid_dict_comprehension:
| '{' a='**' bitwise_or for_if_clauses '}' {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "dict unpacking cannot be used in dict comprehension") }
invalid_parameters:
| a="/" ',' {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "at least one parameter must precede /") }
Expand Down Expand Up @@ -1530,19 +1546,32 @@ invalid_class_def_raw:
RAISE_INDENTATION_ERROR("expected an indented block after class definition on line %d", a->lineno) }

invalid_double_starred_kvpairs:
| ','.double_starred_kvpair+ ',' invalid_kvpair
| expression ':' a='*' bitwise_or { RAISE_SYNTAX_ERROR_STARTING_FROM(a, "cannot use a starred expression in a dictionary value") }
| invalid_kvpair_unpacking [',']
| ','.double_starred_kvpair+ ',' (invalid_kvpair | invalid_kvpair_unpacking)
| expression a=':' &('}'|',') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "expression expected after dictionary key and ':'") }
invalid_kvpair_unpacking:
| a='**' b=if_expression {
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "invalid double starred expression. Did you forget to wrap the conditional expression in parentheses?") }
| a='*' b=bitwise_or ':' expression { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "cannot use a starred expression in a dictionary key") }
| a='**' b=bitwise_or ':' expression { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "cannot use dict unpacking in a dictionary key") }
| expression ':' a='*' b=bitwise_or { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "cannot use a starred expression in a dictionary value") }
| expression ':' a='**' b=bitwise_or { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "cannot use dict unpacking in a dictionary value") }
invalid_kvpair:
| a=expression !(':') {
RAISE_ERROR_KNOWN_LOCATION(p, PyExc_SyntaxError, a->lineno, a->end_col_offset - 1, a->end_lineno, -1, "':' expected after dictionary key") }
| expression ':' a='*' bitwise_or { RAISE_SYNTAX_ERROR_STARTING_FROM(a, "cannot use a starred expression in a dictionary value") }
| expression ':' a='**' bitwise_or { RAISE_SYNTAX_ERROR_STARTING_FROM(a, "cannot use dict unpacking in a dictionary value") }
| expression a=':' &('}'|',') {RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "expression expected after dictionary key and ':'") }
invalid_starred_expression_unpacking:
| a='*' b=if_expression {
RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "invalid starred expression. Did you forget to wrap the conditional expression in parentheses?") }
| a='*' expression '=' b=expression { RAISE_SYNTAX_ERROR_KNOWN_RANGE(a, b, "cannot assign to iterable argument unpacking") }
invalid_starred_expression_unpacking_sequence:
| a='**' bitwise_or {
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "cannot use dict unpacking here") }
| invalid_starred_expression_unpacking
invalid_starred_expression:
| '*' { RAISE_SYNTAX_ERROR("Invalid star expression") }

invalid_fstring_replacement_field:
| '{' a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before '='") }
| '{' a='!' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "f-string: valid expression required before '!'") }
Expand Down
4 changes: 2 additions & 2 deletions Lib/test/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,6 @@ def baz():
check('x=1\nfrom __future__ import division', 2, 1)
check('foo(1=2)', 1, 5)
check('def f():\n x, y: int', 2, 3)
check('[*x for x in xs]', 1, 2)
check('foo(x for x in range(10), 100)', 1, 5)
check('for 1 in []: pass', 1, 5)
check('(yield i) = 2', 1, 2)
Expand Down Expand Up @@ -2433,7 +2432,8 @@ def test_encodings(self):
)
err = run_script(source.encode('cp437'))
self.assertEqual(err[-3], ' "¢¢¢¢¢¢" + f(4, x for x in range(1))')
self.assertEqual(err[-2], ' ^^^')
self.assertEqual(err[-2], ' ^^^^^^^^^^^^^^^^^^^')
self.assertEqual(err[-1], 'SyntaxError: Generator expression must be parenthesized')

# Check backwards tokenizer errors
source = '# -*- coding: ascii -*-\n\n(\n'
Expand Down
Loading
Loading