Skip to content

Commit 031d1a9

Browse files
committed
Improve short-circuiting spec + add additional rejected ideas
1 parent 072c686 commit 031d1a9

File tree

1 file changed

+105
-9
lines changed

1 file changed

+105
-9
lines changed

peps/pep-0999.rst

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -429,20 +429,43 @@ just because a ``?.`` or ``?[ ]`` is used prior.
429429
::
430430

431431
>>> a = None
432-
>>> a?.b.c[0].some_function()
432+
>>> print(a?.b.c[0].some_function())
433433
None
434434

435+
The ``None``-aware access operators will only short-circuit expressions
436+
containing name, attribute access, subscript, their ``None``-aware
437+
counterparts and call expressions. As a rule of thumb, short-circuiting
438+
is broken once a (soft-) keyword is reached.
439+
440+
::
441+
442+
>>> a = None
443+
>>> print(a?.b.c)
444+
None
445+
>>> print(a?.b.c or "Hello")
446+
'Hello'
447+
>>> 2 in a?.b.c
448+
Traceback (most recent call last):
449+
File "<python-input>", line 1, in <module>
450+
2 in a?.b.c
451+
TypeError: argument of type 'NoneType' is not a container or iterable
452+
>>> 2 in (a?.b.c or ())
453+
False
454+
435455
Grouping
436456
********
437457

438-
Using ``?.`` and ``?[ ]`` inside groups is possible. Any non-trivial group
439-
will be evaluate on its own, short-circuiting will only skip to the end of
440-
the the expression inside the group itself.
458+
Grouping is an implicit property of the `Short-circuiting`_ behavior.
459+
If a group contains a non short-circuiting expression, i.e. one that
460+
is not either a name, attribute access, subscript, their ``None``-aware
461+
counterparts or a call expression, the short-circuiting chain will be
462+
broken. The rule of thumb still applies: short-circuiting is broken once
463+
a (soft-) keyword is reached.
441464

442-
::
443-
444-
# Trivial groups
445-
(a?.b).c?.d == a?.b.c?.d
465+
In the example below the group contains a ``BoolOp`` (``or``) expression
466+
which breaks the short-circuiting chain into two: ``a.b?.c`` inside
467+
the group which is evaluated first and ``(...).e?.func()`` on the
468+
outside.
446469

447470
::
448471

@@ -457,6 +480,15 @@ the the expression inside the group itself.
457480
# (...).e?.func()
458481
_t4.func() if ((_t4 := _t3.e) is not None) else None
459482

483+
In contrast, the example below only consists of a name, one attribute
484+
access and two ``None``-aware attribute access expressions. As such the
485+
grouping does not break the short-circuiting chain. The brackets can
486+
safely be removed.
487+
488+
::
489+
490+
# Trivial groups
491+
(a?.b).c?.d == a?.b.c?.d
460492

461493
Assignments
462494
***********
@@ -693,6 +725,33 @@ a try-except block can be used instead.
693725
As the ``?.`` and ``?[ ]`` would allow developers to be more explicit in
694726
their intend, this suggestion is rejected.
695727

728+
Remove short-circuiting
729+
-----------------------
730+
731+
It was suggested to remove the `Short-circuiting`_ behavior completely
732+
because it might be too difficult to understand. Developers should
733+
instead change any subsequent attribute access or subscript to their
734+
``None``-aware variants.
735+
736+
::
737+
738+
# before
739+
a.b.optional?.c.d.e
740+
741+
# after
742+
a.b.optional?.c?.d?.e
743+
744+
The idea has some of the same challenges as `Add a maybe keyword`_.
745+
By forcing the use of ``?.`` or ``?[ ]`` for attributes which are
746+
``not-optional``, it will be difficult to know if the ``not-optional``
747+
attributes ``.c`` or ``.d`` suddenly started to return ``None`` as well.
748+
The ``AttributeError`` would have been silenced.
749+
750+
Another issue especially for longer expressions is that **all**
751+
subsequent attribute access and subscript operators need to be changed
752+
as soon as just one attribute in a long chain is ``optional``. Missing
753+
just one can instantly cause a new ``AttributeError`` or ``TypeError``.
754+
696755
``?`` Unary Postfix operator
697756
----------------------------
698757

@@ -865,6 +924,41 @@ for lists and tuples and as such would be a valuable addition to the
865924
language itself, it doesn't remove the need for arbitrary objects which
866925
implement a custom ``__getitem__`` method.
867926

927+
Limit scope of short-circuiting with grouping
928+
---------------------------------------------
929+
930+
Some languages like JS [#js_short_circuiting]_ and C# [#csharp]_ limit the
931+
scope of the `Short-circuiting`_ via explicit grouping::
932+
933+
a = None
934+
x = (a?.b).c
935+
# ^^^^^^
936+
937+
In the example above short-circuiting would be limited to just ``a?.b``,
938+
thus with ``a = None`` the expression would raise an ``AttributeError``
939+
instead of setting ``x`` to ``None``.
940+
941+
Even though other languages have implemented it that way, this kind of
942+
explicit grouping for short-circuiting does have its disadvantages.
943+
The ``None``-aware access operators are explicitly designed to return
944+
``None`` at some point. Directly limiting the scope of the
945+
short-circuiting behavior almost guarantees that the code will raise
946+
an ``AttributeError`` or ``TypeError`` at some point. Type checkers
947+
would also have to raise an error for trying to access an attribute
948+
or subscript on an ``optional`` variable again.
949+
950+
As such breaking the short-circuiting chain does only make sense if a
951+
fallback value is provided at the same time. For example::
952+
953+
(a?.b.c or fallback).e.func()
954+
955+
In case it is known that ``a`` will always be a not ``None`` value,
956+
and it is just still typed as optional, better options include adding
957+
an ``assert a is not None`` or if it is ever proposed a ``Not-None``
958+
assertion operator ``a!`` (out of scope for this PEP). Developers also
959+
always have the option of splitting the expression up again like they do
960+
today.
961+
868962

869963
Common objections
870964
=================
@@ -1092,8 +1186,10 @@ Footnotes
10921186
(https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#optional-chaining)
10931187
.. [#js] JavaScript: Optional chaining (?.)
10941188
(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)
1189+
.. [#js_short_circuiting] JavaScript: Optional chaining (?.) - Short-circuiting
1190+
(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining#short-circuiting)
10951191
.. [#csharp] C# Reference: Member access operators
1096-
(https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators)
1192+
(https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-)
10971193
.. [#dart] Dart: Other operators
10981194
(https://dart.dev/language/operators#other-operators)
10991195
.. [#swift] Swift: Optional Chaining

0 commit comments

Comments
 (0)