PEP 577: Revise & withdraw based on review feedback (#665)

This commit is contained in:
Nick Coghlan 2018-06-19 20:36:50 +10:00 committed by GitHub
parent 1a43bed5ca
commit 8857369e2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 347 additions and 173 deletions

View File

@ -1,7 +1,7 @@
PEP: 577 PEP: 577
Title: Augmented Assignment Expressions Title: Augmented Assignment Expressions
Author: Nick Coghlan <ncoghlan@gmail.com> Author: Nick Coghlan <ncoghlan@gmail.com>
Status: Draft Status: Withdrawn
Type: Standards Track Type: Standards Track
Content-Type: text/x-rst Content-Type: text/x-rst
Created: 14-May-2018 Created: 14-May-2018
@ -9,71 +9,62 @@ Python-Version: 3.8
Post-History: 22-May-2018 Post-History: 22-May-2018
PEP Withdrawal
==============
While working on this PEP, I realised that it didn't really address what was
actually bothering me about PEP 572's proposed scoping rules for previously
unreferenced assignment targets, and also had some significant undesirable
consequences (most notably, allowing ``>>=` and ``<<=`` as inline augmented
assignment operators that meant something entirely different from the ``>=``
and ``<=`` comparison operators).
I also realised that even without dedicated syntax of their own, PEP 572 allows
inline augmented assignments to be written using the ``operator`` module::
from operator import iadd
if (target := iadd(target, value)) < limit:
...
(The restriction to simple names as inline assignment targets means that the
target expession can always be repeated without side effects)
Accordingly, I'm withdrawing this PEP without submitting it for pronouncement,
and will instead be writing a replacement PEP that focuses specifically on the
handling of assignment targets which haven't already been declared as local
variables in the current scope (for both regular block scopes, and for scoped
expressions).
Abstract Abstract
======== ========
This is a proposal to allow augmented assignment statements such as This is a proposal to allow augmented assignments such as ``x += 1`` to be
``x += 1`` to be used as expressions when the assignment target is a used as expressions, not just statements.
simple name.
For example, this will allow operation retry loops to be written as:: As part of this, ``NAME := EXPR`` is proposed as an inline assignment expression
that uses the new augmented assignment scoping rules, rather than implicitly
defining a new local variable name the way that existing name binding
statements do. The question of allowing expression level local variable
declarations at function scope is deliberately separated from the question of
allowing expression level name bindings, and deferred to a later PEP.
remaining_attempts = 10 This PEP is a direct competitor to PEP 572 (although it borrows heavily from that
while remaining_attempts -= 1: PEP's motivation, and even shares the proposed syntax for inline assignments).
try: See `Relationship with PEP 572`_ for more details on the connections between
result = attempt_operation() the two PEPs.
except Exception as exc:
continue # Failed, so try again
break # Success!
else:
# Ran out of attempts before succeeding
raise OperationFailed("No more attempts remaining") from exc
It is a direct competitor to PEP 572 (although it borrows heavily from that To improve the usability of the new expressions, a semantic split is proposed
PEP's motivation, and even borrows its proposed syntax for a slightly between the handling of augmented assignments in regular block scopes (modules,
different purpose). classes, and functions), and the handling of augmented assignments in scoped
expressions (lambda expressions, generator expressions, and comprehensions),
As part of this, a semantic split is proposed between the handling of augmented such that all inline assignments default to targeting the nearest containing
assignments in regular block scopes (modules, classes, and functions), and the block scope.
handling of augmented assignments in scoped expressions (lambda expressions,
generator expressions, and comprehensions), such that augmented assignments
default to targeting the nearest containing block scope.
A new compile time ``TargetNameError`` is added as a subclass of ``SyntaxError`` A new compile time ``TargetNameError`` is added as a subclass of ``SyntaxError``
to handle cases where it either isn't clear to the compiler which target is to handle cases where it is deemed to be currently unclear which target is
expected to be rebound by an augmented assignment, or else the augmented expected to be rebound by an inline assignment, or else the target scope
assignment target scope is invalid for another reason. for the inline assignment is considered invalid for another reason.
Finally, ``NAME := EXPR`` is proposed as a name rebinding expression that
uses the new augmented assignment scoping rules, rather than implicitly
defining a new local variable name the way that existing name binding
statements do.
Relationship with PEP 572
=========================
The case for allowing inline assignments at all is made in PEP 572. This
competing PEP was initially going to propose an alternate surface syntax
(``EXPR given NAME = EXPR``), while retaining the expression semantics from
PEP 572, but that changed when discussing one of the initial motivating use
cases for allowing embedded assignments at all: making it possible to easily
calculate cumulative sums in comprehensions and generator expressions.
As a result of that, and unlike PEP 572, this PEP focuses primarily on use
cases for inline augmented assignment. It also has the effect of converting
cases that currently inevitably raise ``UnboundLocalError`` at function call
time to report a new compile time ``TargetNameError``.
New syntax for a name rebinding expression (``NAME := TARGET``) is then added
primarily as a lower level primitive to help illustrate, implement and explain
the new augmented assignment semantics, rather than being the sole change being
proposed.
The author of this PEP believes that this approach makes the value of the new
flexibility in name rebinding clearer, while also mitigating many of the
potential concerns raised with PEP 572 around explaining when to use
``NAME = EXPR`` over ``NAME := EXPR`` (and vice-versa).
Syntax and semantics Syntax and semantics
@ -82,10 +73,10 @@ Syntax and semantics
Augmented assignment expressions Augmented assignment expressions
-------------------------------- --------------------------------
The language grammar would be adjusted to allow augmented assignments that The language grammar would be adjusted to allow augmented assignments to
target simple names to appear as expressions, where the result of the appear as expressions, where the result of the augmented assignment
augmented assignment expression is the same post-calculation reference as is expression is the same post-calculation reference as is being bound to the
being bound to the given target. given target.
For example:: For example::
@ -99,25 +90,96 @@ For example::
>>> n >>> n
9 9
Augmented assignments to attributes and container subscripts will continue to For mutable targets, this means the result is always just the original object::
be restricted to the standalone statement form, and will be defined as
returning ``None`` for purposes of interactive use. While a future PEP could
potentially make the case for allowing those more complex targets as expressions,
this PEP doesn't attempt to do so due to the ambiguity around whether or not
they should call ``__getitem__`` and/or ``__getattribute`` on the target
expression after performing the assignment, or else just return a reference to
the binary operation result being assigned.
(Note: as an implementation detail, the language grammar itself may allow all >>> seq = []
existing permitted targets for all augmented assignments, regardless of whether >>> seq_id = id(seq)
they're appearing as an expression or a statement, with the restriction to >>> seq += range(3)
statement level usage for more complex targets being implemented at the AST [0, 1, 2]
generation step). >>> seq_id == id(seq)
True
Augmented assignments to attributes and container subscripts will be permitted,
with the result being the post-calculation reference being bound to the target,
just as it is for simple name targets::
def increment(self, step=1):
return self._value += step
In these cases, ``__getitem__`` and ``__getattribute__`` will *not* be called
after the assignment has already taken place (they will only be called as
needed to evaluate the in-place operation).
Augmented assignment in block scopes Adding an inline assignment operator
------------------------------------ ------------------------------------
Given only the addition of augmented assignment expressions, it would be
possible to abuse a symbol like ``|=`` as a general purpose assignment
operator by defining a ``Target`` wrapper type that worked as follows::
>>> class Target:
... def __init__(self, value):
... self.value = value
... def __or__(self, other):
... return Target(other)
...
>>> x = Target(10)
>>> x.value
10
>>> x |= 42
<__main__.Target object at 0x7f608caa8048>
>>> x.value
42
This is similar to the way that storing a single reference in a list was long
used as a workaround for the lack of a ``nonlocal`` keyword, and can still be
used today (in combination with ``operator.itemsetter``) to work around the
lack of expression level assignments.
Rather than requiring such workarounds, this PEP instead proposes that
PEP 572's "NAME := EXPR" syntax be adopted as a new inline assignment
expression that uses the augmented assignment scoping rules described below.
This cleanly handles cases where only the new value is of interest, and the
previously bound value (if any) can just be discarded completely.
Note that for both simple names and complex assignment targets, the inline
assignment operator does *not* read the previous reference before assigning
the new one. However, when used at function scope (either directly or inside
a scoped expression), it does *not* implicitly define a new local variable,
and will instead raise ``TargetNameError`` (as described for augmented
assignments below).
Assignment operator precedence
------------------------------
To preserve the existing semantics of augmented assignment statements,
inline assignment operators will be defined as being of lower precedence
than all other operators, include the comma pseudo-operator. This ensures
that when used as a top level expression the entire right hand side of the
expression is still interpreted as the value to be processed (even when that
value is a tuple without parentheses).
The difference this introduces relative to PEP 572 is that where
``(n := first, second)`` sets ``n = first`` in PEP 572, in this PEP it would set
``n = (first, second)`, and getting the first meaning would require an extra
set of parentheses (``((n := first), second)``).
PEP 572 quite reasonably notes that this results in ambiguity when assignment
expressions are used as function call arguments. This PEP resolves that concern
a different way by requiring that assignment expressions be parenthesised
when used as arguments to a function call (unless they're the sole argument).
This is a more relaxed version of the restriction placed on generator
expressions (which always require parentheses, except when they're the sole
argument to a function call).
Augmented assignment to names in block scopes
---------------------------------------------
No target name binding changes are proposed for augmented assignments at module No target name binding changes are proposed for augmented assignments at module
or class scope (this also includes code executed using "exec" or "eval"). These or class scope (this also includes code executed using "exec" or "eval"). These
will continue to implicitly declare a new local variable as the binding target will continue to implicitly declare a new local variable as the binding target
@ -180,7 +242,7 @@ as they do today::
n += 1 # Raises UnboundLocalError at runtime n += 1 # Raises UnboundLocalError at runtime
def local_declaration_without_initial_assignment(): def local_declaration_without_initial_assignment():
n : typing.Any n: typing.Any
n += 1 # Raises UnboundLocalError at runtime n += 1 # Raises UnboundLocalError at runtime
Whereas the following would raise a compile time ``DeprecationWarning`` Whereas the following would raise a compile time ``DeprecationWarning``
@ -202,11 +264,16 @@ as a backwards incompatible change that requires a deprecation period.
When augmented assignments are used as expressions in function scope (rather When augmented assignments are used as expressions in function scope (rather
than as standalone statements), there aren't any backwards compatibility than as standalone statements), there aren't any backwards compatibility
concerns, so the compile time name binding checks would be enforced immediately concerns, so the compile time name binding checks would be enforced immediately
in Python 3.9. in Python 3.8.
Similarly, the new inline assignment expressions would always require explicit
predeclaration of their target scope when used as part of a function, at least
for Python 3.8. (See the design discussion section for notes on potentially
revisiting that restriction in the future).
Augmented assignment in scoped expressions Augmented assignment to names in scoped expressions
------------------------------------------ ---------------------------------------------------
Scoped expressions is a new collective term being proposed for expressions that Scoped expressions is a new collective term being proposed for expressions that
introduce a new nested scope of execution, either as an intrinsic part of their introduce a new nested scope of execution, either as an intrinsic part of their
@ -227,6 +294,12 @@ be defined as follows:
binding or declaration can be found in that scope, then ``TargetNameError`` binding or declaration can be found in that scope, then ``TargetNameError``
will be raised at compile time (rather than creating a new binding within will be raised at compile time (rather than creating a new binding within
the scoped expression). the scoped expression).
* if the containing block scope is a function scope, and the target name is
explicitly declared as ``global`` or ``nonlocal``, then it will be use the
same scope declaration in the body of the scoped expression
* if the containing block scope is a function scope, and the target name is
a local variable in that function, then it will be implicitly declared as
``nonlocal`` in the body of the scoped expression
* if the containing block scope is a class scope, than ``TargetNameError`` will * if the containing block scope is a class scope, than ``TargetNameError`` will
always be raised, with a dedicated message indicating that combining class always be raised, with a dedicated message indicating that combining class
scopes with augmented assignments in scoped expressions is not currently scopes with augmented assignments in scoped expressions is not currently
@ -282,66 +355,53 @@ initial implementation rather than needing to be phased in as a potentially
backwards incompatible change. backwards incompatible change.
Promoting nonlocal references to global references Design discussion
-------------------------------------------------- =================
As part of the above changes, all ``nonlocal NAME`` declarations (including Allowing complex assignment targets
the implicit ones added for augmented assignment targets in scoped expressions -----------------------------------
at function scope) will be changed to take explicit ``global NAME`` declarations
into account, such that the affected name is considered ``global`` in the inner
scope as well. For example, the following code would work by binding ``x`` in
the global scope instead of raising ``SyntaxError`` as it does today::
>>> def f(): The initial drafts of this PEP kept PEP 572's restriction to single name targets
... global x when augmented assignments were used as expressions, allowing attribute and
... def g(): subscript targets solely for the statement form.
... nonlocal x
... x = 1 However, enforcing that required varying the permitted targets based on whether
... g() or not the augmented assignment was a top level expression or not, as well as
>>> f() explaining why ``n += 1``, ``(n += 1)``, and ``self.n += 1`` were all legal,
>>> x but ``(self.n += 1)`` was prohibited, so the proposal was simplified to allow
1 all existing augmented assignment targets for the expression form as well.
Since this PEP defines ``TARGET := EXPR`` as a variant on augmented assignment,
that also gained support for assignment and subscript targets.
Adding an inline assignment expression Augmented assignment or name binding only?
-------------------------------------- ------------------------------------------
Given just the above changes, it would be possible to abuse a symbol like PEP 572 makes a reasonable case that the potential use cases for inline
``|=`` as a general purpose assignment operator by defining a ``Target`` wrapper augmented assignment are notably weaker than those for inline assignment in
type that worked as follows:: general, so it's acceptable to require that they be spelled as ``x := x + 1``,
bypassing any in-place augmented assignment methods.
>>> class Target: While this is at least arguably true for the builtin types (where potential
... def __init__(self, value): counterexamples would probably need to focus on set manipulation use cases
... self.value = value that the PEP author doesn't personally have), it would also rule out more
... def __or__(self, other): memory intensive use cases like manipulation of NumPy arrays, where the data
... return Target(other) copying involved in out-of-place operations can make them impractical as
... alternatives to their in-place counterparts.
>>> x = Target(10)
>>> x.value
10
>>> x |= 42
<__main__.Target object at 0x7f608caa8048>
>>> x.value
42
Rather than requiring such workarounds, this PEP instead proposes that That said, this PEP mainly exists because the PEP author found the inline
PEP 572's "NAME := EXPR" syntax be adopted as a new inline assignment assignment proposal much easier to grasp as "It's like ``+=``, only skipping
expression that uses the augmented assignment scoping rules described above. the addition step", and also liked the way that that framing provides an
actual semantic difference between ``NAME = EXPR`` and ``NAME := EXPR`` at
function scope.
This cleanly handles cases where only the new value is of interest, and the That difference in target scoping behaviour means that the ``NAME := EXPR``
previously bound value (if any) can just be discarded completely.
As with other augmented assignment operators, function level usage would always
require a preceding name binding or scope declaration to avoid getting
``TargetNameError`` (as a new operator, there's no need for a
``DeprecationWarning`` period).
This difference in target scoping behaviour means that the ``NAME := EXPR``
syntax would be expected to have two primary use cases: syntax would be expected to have two primary use cases:
- as a way of allowing assignments to be embedded as an expression in an ``if`` * as a way of allowing assignments to be embedded as an expression in an ``if``
or ``while`` statement, or as part of a scoped expression or ``while`` statement, or as part of a scoped expression
- as a way of requesting a compile time check that the target name be previously * as a way of requesting a compile time check that the target name be previously
declared or bound in the current function scope declared or bound in the current function scope
At module or class scope, ``NAME = EXPR`` and ``NAME := EXPR`` would be At module or class scope, ``NAME = EXPR`` and ``NAME := EXPR`` would be
@ -351,32 +411,72 @@ type checkers would be encouraged to enforce the same "declaration or assignment
required before use" behaviour for ``NAME := EXPR`` as the compiler would required before use" behaviour for ``NAME := EXPR`` as the compiler would
enforce at function scope. enforce at function scope.
Unlike existing augmented assignment statements, inline assignment expressions
would be restricted entirely to single name targets (even when used as a
standalone statement).
Postponing a decision on expression level target declarations
-------------------------------------------------------------
Design discussion At least for Python 3.8, usage of inline assignments (whether augmented or not)
================= at function scope would always require a preceding name binding or scope
declaration to avoid getting ``TargetNameError``, even when used outside a
scoped expression.
Restriction to single name targets The intent behind this requirement is to clearly separate the following two
---------------------------------- language design questions:
This PEP keeps PEP 572's restriction to single name targets when augmented 1. Can an expression rebind a name in the current scope?
assignments are used as expressions, restricting attribute and subscript 2. Can an expression declare a new name in the current scope?
targets to the statement form.
While the case could be made that it would be more consistent to allow For module global scopes, the answer to both of those questions is unequivocally
those in the expression form as well, the rationale for excluding them is "Yes", because it's a language level guarantee that mutating the ``globals()``
that it's inherently ambiguous as to whether or not the expression form would dict will immediately impact the runtime module scope, and ``global NAME``
return the expression being bound, or the result of evaluating the LHS as declarations inside a function can have the same effect (as can importing the
an expression (rather than as an assignment target). currently executing module and modifying its attributes).
If this restriction was deemed unduly confusing, then the simplest resolution For class scopes, the answer to both questions is also "Yes" in practice,
would be to retain the current semantics of augmented assignment statements although less unequivocally so, since the semantics of ``locals()`` are
and have the expression result be the reference bound to the target (i.e. currently formally unspecified. However, if the current behaviour of ``locals()``
``__getitem__`` and ``__getattribute__`` would *not* be called after the at class scope is taken as normative (as PEP 558 proposes), then this is
assignment had already taken place) essentially the same scenario as manipulating the module globals, just using
``locals()`` instead.
For function scopes, however, the current answers to these two questions are
respectively "Yes" and "No". Expression level rebinding of function locals is
already possible thanks to lexically nested scopes and explicit ``nonlocal NAME``
expressions. While this PEP will likely make expression level rebinding more
common than it is today, it isn't a fundamentally new concept for the language.
By contrast, declaring a *new* function local variable is currently a statement
level action, involving one of:
* an assignment statement (``NAME = EXPR``, ``OTHER_TARGET = NAME = EXPR``, etc)
* a variable declaration (``NAME : EXPR``)
* a nested function definition
* a nested class definition
* a ``for`` loop
* a ``with`` statement
* an ``except`` clause (with limited scope of access)
The historical trend for the language has actually been to *remove* support for
expression level declarations of function local names, first with the
introduction of "fast locals" semantics (which made the introduction of names
via ``locals()`` unsupported for function scopes), and again with the hiding
of comprehension iteration variables in Python 3.0.
Now, it may be that in Python 3.9, we decide to revisit this question based on
our experience with expression level name binding in Python 3.8, and decide that
we really do want expression level function local variable declarations as well,
and that we want ``NAME := EXPR`` to be the way we spell that (rather than,
for example, spelling inline declarations more explicitly as
``NAME := EXPR given NAME``, which would permit them to carry type annotations,
and also permit them to declare new local variables in scoped expressions,
rather than having to pollute the namespace in their containing scope).
But the proposal in this PEP is that we explicitly give ourselves a full
release to decide how much we want that feature, and exactly where we find
its absence irritating. Python has survived happily without expression level
name bindings *or* declarations for decades, so we can afford to give ourselves
a couple of years to decide if we really want *both* of those, or if expression
level bindings are sufficient.
Ignoring scoped expressions when determining augmented assignment targets Ignoring scoped expressions when determining augmented assignment targets
@ -409,7 +509,7 @@ areas:
the name already be defined to the new name binding operator (raising the name already be defined to the new name binding operator (raising
``TargetNameError`` rather than implicitly declaring new local variables at ``TargetNameError`` rather than implicitly declaring new local variables at
function scope) function scope)
- it includes lambda expressions in the set of scopes that gets ignored for - it includes lambda expressions in the set of scopes that get ignored for
target name binding purposes, making this transparency to assignments common target name binding purposes, making this transparency to assignments common
to all of the scoped expressions rather than being specific to comprehensions to all of the scoped expressions rather than being specific to comprehensions
and generator expressions and generator expressions
@ -428,7 +528,9 @@ One of the challenges with PEP 572 is the fact that ``NAME = EXPR`` and
``NAME := EXPR`` are entirely semantically equivalent at every scope. This ``NAME := EXPR`` are entirely semantically equivalent at every scope. This
makes the two forms hard to teach, since there's no inherent nudge towards makes the two forms hard to teach, since there's no inherent nudge towards
choosing one over the other at the statement level, so you end up having to choosing one over the other at the statement level, so you end up having to
resort to "``NAME = EXPR`` is preferred because it's been around longer". resort to "``NAME = EXPR`` is preferred because it's been around longer"
(and PEP 572 proposes to enfore that historical idiosyncrasy at the compiler
level).
That semantic equivalence is difficult to avoid at module and class scope while That semantic equivalence is difficult to avoid at module and class scope while
still having ``if NAME := EXPR:`` and ``while NAME := EXPR:`` work sensibly, but still having ``if NAME := EXPR:`` and ``while NAME := EXPR:`` work sensibly, but
@ -440,6 +542,14 @@ providing a reasonable incentive to continue to default to using the
indicating that the targeted name has already been bound or declared and hence indicating that the targeted name has already been bound or declared and hence
should already be known to the compiler). should already be known to the compiler).
If Guido were to declare that support for inline declarations was a hard
design requirement, then this PEP would be updated to propose that
``EXPR given NAME`` also be introduced as a way to support inline name declarations
after arbitrary expressions (this would allow the inline name declarations to be
deferred until the end of a complex expression rather than needing to be
embedded in the middle of it, and PEP 8 would gain a recommendation encouraging
that style).
Disallowing augmented assignments in class level scoped expressions Disallowing augmented assignments in class level scoped expressions
------------------------------------------------------------------- -------------------------------------------------------------------
@ -459,6 +569,43 @@ other than directly inline in the class body (e.g. in a separate helper
function). function).
Comparison operators vs assignment operators
--------------------------------------------
The ``OP=`` construct as an expression currently indicates a comparison
operation::
x == y # Equals
x >= y # Greater-than-or-equal-to
x <= y # Less-than-or-equal-to
Both this PEP and PEP 572 propose adding at least one operator that's somewhat
similar in appearance, but defines an assignment instead::
x := y # Becomes
This PEP then goes much further and allows all *13* augmented assignment symbols
to be uses as binary operators::
x += y # In-place add
x -= y # In-place minus
x *= y # In-place multiply
x @= y # In-place matrix multiply
x /= y # In-place division
x //= y # In-place int division
x %= y # In-place mod
x &= y # In-place bitwise and
x |= y # In-place bitwise or
x ^= y # In-place bitwise xor
x <<= y # In-place left shift
x >>= y # In-place right shift
x **= y # In-place power
Of those additional binary operators, the most questionable would be the
bitshift assignment operators, since they're each only one doubled character
away from one of the inclusive ordered comparison operators.
Examples Examples
======== ========
@ -468,29 +615,29 @@ Simplifying retry loops
There are currently a few different options for writing retry loops, including:: There are currently a few different options for writing retry loops, including::
# Post-decrementing a counter # Post-decrementing a counter
remaining_attempts = 9 remaining_attempts = MAX_ATTEMPTS
while remaining_attempts: while remaining_attempts:
remaining_attempts -= 1
try: try:
result = attempt_operation() result = attempt_operation()
except Exception as exc: except Exception as exc:
remaining_attempts -= 1
continue # Failed, so try again continue # Failed, so try again
log.debug(f"Succeeded after {attempts} attempts")
break # Success! break # Success!
else: else:
# Ran out of attempts before succeeding raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
raise OperationFailed("No more attempts remaining") from exc
# Loop-and-a-half with a pre-decremented counter # Loop-and-a-half with a pre-incremented counter
remaining_attempts = 10 attempt = 0
while True: while True:
remaining_attempts -= 1 attempts += 1
if not remaining_attempts: if attempts > MAX_ATTEMPTS:
# Ran out of attempts before succeeding raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
raise OperationFailed("No more attempts remaining") from exc
try: try:
result = attempt_operation() result = attempt_operation()
except Exception as exc: except Exception as exc:
continue # Failed, so try again continue # Failed, so try again
log.debug(f"Succeeded after {attempts} attempts")
break # Success! break # Success!
Each of the available options hides some aspect of the intended loop structure Each of the available options hides some aspect of the intended loop structure
@ -500,16 +647,16 @@ or both.
The proposal in this PEP allows both the state modification and the exit The proposal in this PEP allows both the state modification and the exit
condition to be included directly in the loop header:: condition to be included directly in the loop header::
remaining_attempts = 10 attempt = 0
while remaining_attempts -= 1: while (attempt += 1) <= MAX_ATTEMPTS:
try: try:
result = attempt_operation() result = attempt_operation()
except Exception as exc: except Exception as exc:
continue # Failed, so try again continue # Failed, so try again
log.debug(f"Succeeded after {attempts} attempts")
break # Success! break # Success!
else: else:
# Ran out of attempts before succeeding raise OperationFailed(f"Failed after {MAX_ATTEMPTS} attempts") from exc
raise OperationFailed("No more attempts remaining") from exc
Simplifying if-elif chains Simplifying if-elif chains
@ -552,7 +699,7 @@ indicated as local before the first use as a ``:=`` target, either by
binding it to a value (as shown above), or else by including an appropriate binding it to a value (as shown above), or else by including an appropriate
explicit type declaration:: explicit type declaration::
m : typing.re.Match m: typing.re.Match
if m := pattern.match(data): if m := pattern.match(data):
... ...
elif m := other_pattern.match(data): elif m := other_pattern.match(data):
@ -570,20 +717,18 @@ The proposal in this PEP makes it straightforward to capture and reuse
intermediate values in comprehensions and generator expressions by intermediate values in comprehensions and generator expressions by
exporting them to the containing block scope:: exporting them to the containing block scope::
factor = 1 factor: int
while any(n % (factor := p) == 0 for p in small_primes): while any(n % (factor := p) == 0 for p in small_primes):
n //= factor n //= factor
def cumulative_sums(data, start=0) total = 0
total = start partial_sums = [total += value for value in data]
yield from (total += value for value in data)
return total
Allowing lambda expressions to act more like re-usable code thunks Allowing lambda expressions to act more like re-usable code thunks
------------------------------------------------------------------ ------------------------------------------------------------------
This PEP allows the closure-based counter example:: This PEP allows the classic closure usage example::
def make_counter(start=0): def make_counter(start=0):
x = start x = start
@ -597,7 +742,7 @@ To be abbreviated as::
def make_counter(start=0): def make_counter(start=0):
x = start x = start
return (lambda step=1: x += step) return lambda step=1: x += step
While the latter form is still a conceptually dense piece of code, it can be While the latter form is still a conceptually dense piece of code, it can be
reasonably argued that the lack of boilerplate (where the "def", "nonlocal", reasonably argued that the lack of boilerplate (where the "def", "nonlocal",
@ -606,6 +751,35 @@ have been replaced with the "lambda" keyword) may make it easier to read in
practice. practice.
Relationship with PEP 572
=========================
The case for allowing inline assignments at all is made in PEP 572. This
competing PEP was initially going to propose an alternate surface syntax
(``EXPR given NAME = EXPR``), while retaining the expression semantics from
PEP 572, but that changed when discussing one of the initial motivating use
cases for allowing embedded assignments at all: making it possible to easily
calculate cumulative sums in comprehensions and generator expressions.
As a result of that, and unlike PEP 572, this PEP focuses primarily on use
cases for inline augmented assignment. It also has the effect of converting
cases that currently inevitably raise ``UnboundLocalError`` at function call
time to report a new compile time ``TargetNameError``.
New syntax for a name rebinding expression (``NAME := TARGET``) is then added
not only to handle the same use cases as are identified in PEP 572, but also
as a lower level primitive to help illustrate, implement and explain
the new augmented assignment semantics, rather than being the sole change being
proposed.
The author of this PEP believes that this approach makes the value of the new
flexibility in name rebinding clearer, while also mitigating many of the
potential concerns raised with PEP 572 around explaining when to use
``NAME = EXPR`` over ``NAME := EXPR`` (and vice-versa), without resorting to
prohibiting the bare statement form of ``NAME := EXPR`` outright (such
that ``NAME := EXPR`` is a compile error, but ``(NAME := EXPR)`` is permitted).
Acknowledgements Acknowledgements
================ ================