Skip to content

Commit 7c81675

Browse files
PEP 679: Modify proposal and improve sections (#4575)
1 parent 59918e8 commit 7c81675

File tree

1 file changed

+239
-88
lines changed

1 file changed

+239
-88
lines changed

peps/pep-0679.rst

Lines changed: 239 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,215 @@
11
PEP: 679
2-
Title: Allow parentheses in assert statements
3-
Author: Pablo Galindo Salgado <pablogsal@python.org>
4-
Discussions-To: https://discuss.python.org/t/pep-679-allow-parentheses-in-assert-statements/13003
2+
Title: New assert statement syntax with parentheses
3+
Author: Pablo Galindo Salgado <pablogsal@python.org>,
4+
Stan Ulbrych <stanulbrych@gmail.com>,
5+
Discussions-To: Pending
56
Status: Draft
67
Type: Standards Track
78
Created: 07-Jan-2022
8-
Python-Version: 3.12
9+
Python-Version: 3.15
10+
Post-History: `10-Jan-2022 <https://discuss.python.org/t/pep-679-allow-parentheses-in-assert-statements/13003>`__
911

1012

1113
Abstract
1214
========
1315

14-
This PEP proposes to allow parentheses surrounding the two-argument form of
15-
assert statements. This will cause the interpreter to reinterpret what before
16-
would have been an assert with a two-element tuple that will always be True
17-
(``assert (expression, message)``) to an assert statement with a subject and a
18-
failure message, equivalent to the statement with the parentheses removed
19-
(``assert expression, message``).
16+
This PEP proposes allowing parentheses in the two-argument form of :keyword:`assert`.
17+
The interpreter will reinterpret ``assert (expr, msg)`` as ``assert expr, msg``,
18+
eliminating the common pitfall where such code was previously treated as
19+
asserting a two-element :class:`tuple`, which is always truthy.
2020

2121

2222
Motivation
2323
==========
2424

25-
It is a common user mistake when using the form of the assert statement that includes
26-
the error message to surround it with parentheses. Unfortunately, this mistake
27-
passes undetected as the assert will always pass, because it is
28-
interpreted as an assert statement where the expression is a two-tuple, which
29-
always has truth-y value.
25+
It is a common user mistake when using the form of the :keyword:`assert`
26+
statement that includes the error message to surround it with parentheses [#SO1]_ [#RD]_.
27+
This is because many beginners assume :keyword:`!assert` is a function.
28+
The prominent :mod:`unittest` methods, particularly :meth:`~unittest.TestCase.assertTrue`,
29+
also require parentheses around the assertion and message.
3030

31-
The mistake most often happens when extending the test or description beyond a
32-
single line, as parentheses are the natural way to do that.
31+
Unfortunately, this mistake passes undetected as the ``assert`` will always pass
32+
[#exception]_, because it is interpreted as an ``assert`` statement where the
33+
expression is a two-tuple, which always has truth-y value.
34+
The mistake also often occurs when extending the test or description beyond a
35+
single line, as parentheses are a natural way to do that.
3336

34-
This is so common that a ``SyntaxWarning`` is `now emitted by the compiler
35-
<https://bugs.python.org/issue35029>`_.
37+
This is so common that a :exc:`SyntaxWarning` is `emitted by the compiler
38+
<https://github.com/python/cpython/issues/79210>`_ since 3.10 and several
39+
code linters [#fl8]_ [#pylint]_.
3640

3741
Additionally, some other statements in the language allow parenthesized forms
38-
in one way or another like ``import`` statements (``from x import (a,b,c)``) and
39-
``del`` statements (``del (a,b,c)``).
42+
in one way or another, for example, ``import`` statements
43+
(``from x import (a,b,c)``) or ``del`` statements (``del (a,b,c)``).
4044

41-
Allowing parentheses not only will remove the common mistake but also will allow
45+
Allowing parentheses not only will remove the pitfall but also will allow
4246
users and auto-formatters to format long assert statements over multiple lines
4347
in what the authors of this document believe will be a more natural way.
44-
Although is possible to currently format long ``assert`` statements over
45-
multiple lines as::
48+
Although it is possible to currently format long :keyword:`assert` statements
49+
over multiple lines with backslashes (as is recommended by
50+
:pep:`8#maximum-line-length`) or parentheses and a comma::
4651

47-
assert (
52+
assert (
4853
very very long
49-
expression
50-
), (
54+
test
55+
), (
5156
"very very long "
52-
"message"
53-
)
57+
"error message"
58+
)
5459

55-
the authors of this document believe the parenthesized form is more clear and more consistent with
56-
the formatting of other grammar constructs::
60+
the authors of this document believe the proposed parenthesized form is more
61+
clear and intuitive, along with being more consistent with the formatting of
62+
other grammar constructs::
5763

58-
assert (
64+
assert (
5965
very very long
60-
expression,
66+
test,
6167

6268
"very very long "
63-
"message",
64-
)
69+
"message"
70+
)
6571

66-
This change has been originally discussed and proposed in [bpo-46167]_.
6772

6873
Rationale
6974
=========
7075

71-
This change can be implemented in the parser or in the compiler. We have
72-
selected implementing this change in the parser because doing it in the compiler
73-
will require re-interpreting the AST of an assert statement with a two-tuple::
74-
75-
Module(
76-
body=[
77-
Assert(
78-
test=Tuple(
79-
elts=[
80-
Name(id='x', ctx=Load()),
81-
Name(id='y', ctx=Load())],
82-
ctx=Load()))],
83-
type_ignores=[])
84-
85-
as the AST of an assert statement with an expression and a message::
86-
87-
Module(
88-
body=[
89-
Assert(
90-
test=Name(id='x', ctx=Load()),
91-
msg=Name(id='y', ctx=Load()))],
92-
type_ignores=[])
93-
94-
The problem with this approach is that the AST of the first form will
95-
technically be "incorrect" as we already have a specialized form for the AST of
96-
an assert statement with a test and a message (the second one). This
97-
means that many tools that deal with ASTs will need to be aware of this change
98-
in semantics, which will be confusing as there is already a correct form that
99-
better expresses the new meaning.
76+
Due to backwards compatibility concerns (see section below), to inform users
77+
of the new change of how what was previously a two element tuple is parsed,
78+
a :exc:`SyntaxWarning` with a message like
79+
``"new assertion syntax, will assert first element of tuple"``
80+
will be raised till Python 3.17. For example, when using the new syntax:
81+
82+
.. code-block:: pycon
83+
84+
>>> assert ('Petr' == 'Pablo', "That doesn't look right!")
85+
<python-input-0>:0: SyntaxWarning: new assertion syntax, will assert first element of tuple
86+
Traceback (most recent call last):
87+
File "<python-input-0>", line 1, in <module>
88+
assert ('Petr' == 'Pablo', "That doesn't look right!")
89+
^^^^^^^^^^^^^^^^^
90+
AssertionError: That doesn't look right!
91+
92+
Note that improving syntax warnings in general
93+
is out of the scope of this PEP.
94+
10095

10196
Specification
10297
=============
10398

104-
This PEP proposes changing the grammar of the ``assert`` statement to: ::
99+
The formal grammar of the :keyword:`assert` statement will change to [#edgecase]_:
100+
101+
.. code-block::
105102
106103
| 'assert' '(' expression ',' expression [','] ')' &(NEWLINE | ';')
107104
| 'assert' a=expression [',' expression ]
108105
109106
Where the first line is the new form of the assert statement that allows
110-
parentheses. The lookahead is needed so statements like ``assert (a, b) <= c,
111-
"something"`` are still parsed correctly and to prevent the parser to eagerly
112-
capture the tuple as the full statement.
107+
parentheses and will raise a :exc:`SyntaxWarning` till 3.17.
108+
The lookahead is needed to prevent the parser from eagerly capturing the
109+
tuple as the full statement, so statements like ``assert (a, b) <= c, "something"``
110+
are still parsed correctly.
111+
112+
113+
Implementation Notes
114+
====================
115+
116+
This change can be implemented in the parser or in the compiler.
117+
The specification that a :exc:`SyntaxWarning` be raised informing users
118+
of the new syntax complicates the implementation, as warnings
119+
should be raised during compilation.
120+
121+
The authors believe that an ideal implementation would be in the parser [#edgecase]_,
122+
resulting in ``assert (x,y)`` having the same AST as ``assert x,y``.
123+
This necessitates a two-step implementation plan, with a necessary temporary
124+
compromise.
125+
126+
127+
Implementing in the parser
128+
--------------------------
113129

114-
Optionally, new "invalid" rule can be added to produce custom syntax errors to
115-
cover tuples with 0, 1, 3 or more elements.
130+
It is not possible to have a pure parser implementation with the warning
131+
specification.
132+
(Note that, without the warning specification the pure parser implementation is
133+
a small grammar change [#previmp]_).
134+
To raise the warning, the compiler must
135+
be aware of the new syntax, which means that a flag would be necessary as
136+
otherwise the information is lost during parsing.
137+
As such, the AST of an :keyword:`assert` would look like so,
138+
with a ``paren_syntax`` flag::
139+
140+
>>> print(ast.dump(ast.parse('assert(True, "Error message")'), indent=4))
141+
Module(
142+
body=[
143+
Assert(
144+
test=Constant(value=True),
145+
msg=Constant(value='Error message'),
146+
paren_syntax=1)])
147+
148+
The flag would be removed in 3.18 along with the :exc:`SyntaxWarning`.
149+
150+
151+
Implementing in the compiler
152+
----------------------------
153+
154+
The new syntax can be implemented in the compiler by special casing tuples
155+
of length two. This however, will have the side-effect of not modifying the
156+
AST whatsoever during the transition period while the :exc:`SyntaxWarning`
157+
is being emitted.
158+
159+
Once the :exc:`SyntaxWarning` is removed, the implementation
160+
can be moved to the parser level, where the parenthesized form would be
161+
parsed directly into the same AST structure as ``assert expression, message``.
162+
This approach is more backwards-compatible, as the many tools that deal with
163+
ASTs will have more time to adapt.
116164

117165

118166
Backwards Compatibility
119167
=======================
120168

121-
The change is not technically backwards compatible, as parsing ``assert (x,y)``
122-
is currently interpreted as an assert statement with a 2-tuple as the subject,
123-
while after this change it will be interpreted as ``assert x,y``.
169+
The change is not technically backwards compatible. Whether implemented initially
170+
in the parser or the compiler, ``assert (x,y)``,
171+
which is currently interpreted as an assert statement with a 2-tuple as the
172+
subject and is always truth-y, will be interpreted as ``assert x,y``.
124173

125174
On the other hand, assert statements of this kind always pass, so they are
126175
effectively not doing anything in user code. The authors of this document think
127176
that this backwards incompatibility nature is beneficial, as it will highlight
128-
these cases in user code while before they will have passed unnoticed (assuming that
129-
these cases still exist because users are ignoring syntax warnings).
130-
131-
Security Implications
132-
=====================
133-
134-
There are no security implications for this change.
177+
these cases in user code while before they will have passed unnoticed. This case
178+
has already raised a :exc:`SyntaxWarning` since Python 3.10, so there has been
179+
a deprecation period of over 5 years.
180+
The continued raising of a :exc:`!SyntaxWarning` should mitigate surprises.
181+
182+
The change will also result in changes to the AST of ``assert (x,y)``,
183+
which currently is:
184+
185+
.. code-block:: text
186+
187+
Module(
188+
body=[
189+
Assert(
190+
test=Tuple(
191+
elts=[
192+
Name(id='x', ctx=Load()),
193+
Name(id='y', ctx=Load())],
194+
ctx=Load()))],
195+
type_ignores=[])
196+
197+
the final implementation, in Python 3.18, will result in the following AST:
198+
199+
.. code-block:: text
200+
201+
Module(
202+
body=[
203+
Assert(
204+
test=Name(id='x', ctx=Load()),
205+
msg=Name(id='y', ctx=Load()))],
206+
type_ignores=[])
207+
208+
The problem with this is that the AST of the first form will
209+
technically be "incorrect" as we already have a specialized form for the AST of
210+
an assert statement with a test and a message (the second one).
211+
Implementing initially in the compiler will delay this change, alleviating
212+
backwards compatibility concerns, as tools will have more time to adjust.
135213

136214

137215
How to Teach This
@@ -141,21 +219,94 @@ The new form of the ``assert`` statement will be documented as part of the langu
141219
standard.
142220

143221
When teaching the form with error message of the ``assert`` statement to users,
144-
now it can be noted that adding parentheses also work as expected, which allows to break
145-
the statement over multiple lines.
222+
now it can be noted that adding parentheses also work as expected, which allows
223+
to break the statement over multiple lines.
146224

147225

148226
Reference Implementation
149227
========================
150228

151-
A proposed draft PR with the change exist in [GH-30247]_.
229+
A reference implementation in the parser can be found in this
230+
`branch <https://github.com/python/cpython/compare/main...StanFromIreland:assert-prototype?expand=1>`__
231+
and reference implementation in the compiler can be found in this
232+
`branch <https://github.com/python/cpython/compare/main...StanFromIreland:assert-codegen?expand=1>`__.
152233

153234

154-
References
155-
==========
235+
Rejected Ideas
236+
==============
237+
238+
Adding a syntax with a keyword
239+
------------------------------
240+
241+
Everywhere else in Python syntax, the comma separates variable-length “lists”
242+
of homogeneous elements, like the the items of a :class:`tuple` or :class:`list`,
243+
parameters/arguments of functions, or import targets.
244+
After Python 3.0 introduced :keyword:`except...as <except>`,
245+
the :keyword:`assert` statement remains as the only exception to this convention.
246+
247+
It's possible that user confusion stems, at least partly, from an expectation
248+
that comma-separated items are equivalent.
249+
Enclosing an :keyword:`!assert` statement's expression and message in
250+
parentheses would visually bind them together even further.
251+
Making ``assert`` look more similar to a function call encourages a wrong
252+
mentality.
253+
254+
As a possible solution, it was proposed [#assertwith]_ to replace the comma with
255+
a keyword, and the form would allow parentheses, for example::
256+
257+
assert condition else "message"
258+
assert (condition else "message")
259+
260+
The comma could then be slowly and carefully deprecated, starting with
261+
the case where they appear in parentheses, which already raises a
262+
:exc:`SyntaxWarning`.
263+
264+
The authors of this PEP believe that adding a completely new syntax will,
265+
first and foremost, not solve the common beginner pitfall that this PEP aims to
266+
patch, and will not improve the formatting of assert statements across multiple
267+
lines, which the authors believe the proposed syntax improves.
268+
269+
270+
Security Implications
271+
=====================
272+
273+
There are no security implications for this change.
274+
275+
276+
Acknowledgements
277+
================
278+
279+
This change was originally discussed and proposed in :cpython-issue:`90325`.
280+
281+
Many thanks to Petr Viktorin for his help during the drafting process of this PEP.
282+
283+
284+
Footnotes
285+
=========
286+
287+
.. [#SO1] `StackOverflow: "'assert' statement with or without parentheses" <https://stackoverflow.com/questions/3112171/assert-statement-with-or-without-parentheses>`_
288+
289+
.. [#RD] `/r/python: "Rant: use that second expression in assert! " <https://www.reddit.com/r/Python/comments/1n87g91/rant_use_that_second_expression_in_assert/>`_
290+
291+
.. [#fl8] `flake8: Rule F631 <https://flake8.pycqa.org/en/latest/user/error-codes.html>`_
292+
293+
.. [#pylint] `pylint: assert-on-tuple (W0199) <https://pylint.pycqa.org/en/latest/user_guide/checkers/features.html>`_
294+
295+
.. [#previmp] For the previous parser implementation, see :cpython-pr:`30247`
296+
297+
.. [#exception] During the updating of this PEP, an exception
298+
(``assert (*(t := ()),)``) was found, contradicting the warning.
299+
300+
.. [#assertwith] `[DPO] Pre-PEP: Assert-with: Dedicated syntax for assertion messages <https://discuss.python.org/t/pre-pep-assert-with-dedicated-syntax-for-assertion-messages/13247>`_
301+
302+
.. [#edgecase] An edge case arises with constructs like:
303+
304+
>>> x = (0,)
305+
>>> assert (*x, "edge cases aren't fun:-(")
156306
157-
.. [bpo-46167] https://bugs.python.org/issue46167
158-
.. [GH-30247] https://github.com/python/cpython/pull/30247
307+
This form is currently parsed as a single tuple expression, not
308+
as a condition/message pair, and will need explicit handling in
309+
the compiler.
159310
160311
161312
Copyright

0 commit comments

Comments
 (0)