From 1463e8d170bdd912242cae72c359765adde40159 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Mon, 19 Oct 2026 13:23:42 -0700 Subject: [PATCH 1/7] PEP 823: Expand 'How to Teach This' to a mini-tutorial --- peps/pep-0823.rst | 136 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 122 insertions(+), 14 deletions(-) diff --git a/peps/pep-0823.rst b/peps/pep-0823.rst index ccb74052c6c..b36aacd72b1 100644 --- a/peps/pep-0823.rst +++ b/peps/pep-0823.rst @@ -538,19 +538,128 @@ There are no new security implications from this proposal. How to Teach This ================= -After students know how the attribute access ``.`` and subscript ``[ ]`` -operators work, they may learn about the "``None``-aware" variants for -both. +Prerequisites +------------- + +Students should already know how the following operators work: + +* Plain attribute access: ``.`` +* Plain subscripting: ``[ ]`` + +Basics +------ + +The operator ``?.`` is a variant of ``.`` (attribute access) that returns +``None`` instead of raising ``AttributeError`` when the left argument is +``None``. For example, ``a?.b`` returns ``None`` when ``a`` is ``None``. + +Similarly, ``a?[b]`` is a variant of ``a[b]`` that returns ``None`` instead +of raising ``TypeError`` when ``a`` is ``None``. Here ``b`` can be anything +that can be written inside square brackets, including multiple keys or +indices, or slices. + +The left argument can be any expression. Examples: + +.. code-block:: py823 + + a.b.c?.d + f()?.g + a[b]?[c] + (a or b)?.c + +Chaining +-------- + +.. warning:: + + ``a?.b.c`` is not the same as ``(a?.b).c``! When ``a`` is ``None``, + the former returns ``None``, but the latter raises ``AttributeError``. + Parentheses end the chain, so the final ``.c`` is evaluated normally. + +You can write ``a?.b.c.d``; when ``a`` is ``None``, the entire ``.b.c.d`` +portion is skipped, and ``None`` is returned. This also applies to +``a?[b].c.d``. + +The rationale is that when ``a`` is ``None``, ``a?.b`` is ``None``, so a +subsequent ``.c`` would definitely raise ``AttributeError``. Since the +``?.`` operator allows ``None`` instead of an error in this case, it +makes sense to extend this behavior to subsequent ``.`` operations. + +This also works for subsequent subscript and call operators. For example, +``a?.b[c](d)`` returns ``None`` when ``a`` is ``None``, without evaluating +``c`` or ``d``. + +Chaining does not suppress other operators, such as ``+`` or ``==``. +Chaining also stops at the end of the expression containing it, for +example at the closing parenthesis in ``(a?.b).c``, or at the comma in +``f(a?.b, c)``. A subscript or call that is part of the chain does not +end it: ``a?.b[c](d).e`` still returns ``None`` when ``a`` is ``None``. + +Caveats +------- + +An access is skipped only when the **left** argument is ``None``. +Errors from evaluating the left argument or performing an access on a +value other than ``None`` are not suppressed: + +* When the left argument does not exist at all, you still get a + ``NameError`` or similar. +* When the left argument is not ``None`` and the attribute name on the + right does not exist, you still get an ``AttributeError``. +* Similarly, if the key or index in ``a?[b]`` does not exist, you get a + ``KeyError`` or ``IndexError``. To turn a missing key into ``None``, + assuming ``a`` is a dictionary, you can use ``a?.get(b)``. There is no + equivalent of ``dict.get`` for lists. + +The expression may still be ``None`` when ``a`` is not ``None``. For +example, ``a?.b`` returns ``None`` if ``a`` is not ``None`` but ``a.b`` is +``None``. Static type checkers can flag ``a?.b.c`` as an error when +``.b`` can be ``None``. At runtime you get an ``AttributeError`` when +``.b`` is ``None``. You can use ``a?.b?.c`` to handle this situation, +but that gets ugly quickly. Instead, consider changing the type of +``.b`` so it will never be ``None``, or using a helper function. + +You cannot use ``?.`` or ``?[ ]`` in an assignment target; +``a?.b = 42`` and ``a?[b] = 42`` are syntax errors. You can use them in +an independently evaluated expression within a target, though: +``a[b?.c] = 42`` is fine, but it may assign to ``a[None]``. + +When to use +----------- + +If you find yourself writing code like this: + +.. code-block:: python + + def get_account(user: User | None) -> Account | None: + if user is None: + return None + return user.account + +you can write it more concisely and more idiomatically as: + +.. code-block:: py823 + + def get_account(user: User | None) -> Account | None: + return user?.account + +The same applies to conditional expressions like this: + +.. code-block:: python + + return None if user is None else user.account + +When not to use +--------------- -Students may find it helpful to think of ``?.`` and ``?[ ]`` as a -combination of two different actions. First the ``?`` postfix represents -an ``is not None`` check on the subexpression with short-circuiting -if the check fails. If it succeeds, the attribute and subscript access -are performed like normal. +You should rarely, if ever, need more than one ``?.`` or ``?[ ]`` operator +in a single chain. If you find yourself writing ``a?.b?.c`` a lot, +consider a less noisy alternative, such as a helper function or a schema +change. -Experienced developers may find that, after learning about the PEP, they -start to notice the patterns described in the `Motivation`_ section in -their own code bases. +Do not overuse ``?.`` and ``?[ ]``. If you use them a lot, ask yourself +whether your schema could be improved, or whether your code would benefit +from some helper functions. Reference Implementation @@ -998,9 +1107,8 @@ one. The author believes that this is only temporary and will resolve itself once a PEP has been accepted. Any spelling mistake will also raise a ``SyntaxError``. -As described in the `How to Teach This`_ section, it can be helpful to -think of ``?.`` and ``?[ ]`` as a combination of two different actions -which need to be performed in a certain order. The ``?`` postfix +It can be helpful to think of ``?.`` and ``?[ ]`` as a combination of +two different actions which need to be performed in a certain order. The ``?`` postfix represents an ``is not None`` check on the subexpression and as such should always come first. From f0f266cdebd1a29ca0419d613fe05b96144e7c16 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 20 Oct 2026 08:20:13 -0700 Subject: [PATCH 2/7] Moved warning to end of Chaining section --- peps/pep-0823.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/peps/pep-0823.rst b/peps/pep-0823.rst index b36aacd72b1..5986116396f 100644 --- a/peps/pep-0823.rst +++ b/peps/pep-0823.rst @@ -570,12 +570,6 @@ The left argument can be any expression. Examples: Chaining -------- -.. warning:: - - ``a?.b.c`` is not the same as ``(a?.b).c``! When ``a`` is ``None``, - the former returns ``None``, but the latter raises ``AttributeError``. - Parentheses end the chain, so the final ``.c`` is evaluated normally. - You can write ``a?.b.c.d``; when ``a`` is ``None``, the entire ``.b.c.d`` portion is skipped, and ``None`` is returned. This also applies to ``a?[b].c.d``. @@ -595,6 +589,12 @@ example at the closing parenthesis in ``(a?.b).c``, or at the comma in ``f(a?.b, c)``. A subscript or call that is part of the chain does not end it: ``a?.b[c](d).e`` still returns ``None`` when ``a`` is ``None``. +.. warning:: + + ``a?.b.c`` is not the same as ``(a?.b).c``! When ``a`` is ``None``, + the former returns ``None``, but the latter raises ``AttributeError``. + Parentheses end the chain, so the final ``.c`` is evaluated normally. + Caveats ------- From da9432e485e35d2b77f7a47904eddf73f65b4f95 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 20 Oct 2026 08:22:16 -0700 Subject: [PATCH 3/7] Change user.account example per Marc --- peps/pep-0823.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0823.rst b/peps/pep-0823.rst index 5986116396f..b2590335a83 100644 --- a/peps/pep-0823.rst +++ b/peps/pep-0823.rst @@ -647,7 +647,7 @@ The same applies to conditional expressions like this: .. code-block:: python - return None if user is None else user.account + return user.account if user is not None else None When not to use --------------- From e78cb16e887f06c9ff02bacced148dd018b1a75c Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 20 Oct 2026 08:24:24 -0700 Subject: [PATCH 4/7] Add Change History entry --- peps/pep-0823.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/peps/pep-0823.rst b/peps/pep-0823.rst index b2590335a83..81dbed49a1f 100644 --- a/peps/pep-0823.rst +++ b/peps/pep-0823.rst @@ -1352,6 +1352,12 @@ Footnotes (https://docs.python.org/3/reference/grammar.html) +Change History +============== + +* 20-Oct-2026: Turned `How to Teach This`_ into a mini-tutorial. + + Copyright ========= From 2c503033dea8df0a4cb2613a37a0d6d974c815d0 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 20 Oct 2026 08:36:11 -0700 Subject: [PATCH 5/7] Explain what a.b(c?.d).e does --- peps/pep-0823.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/peps/pep-0823.rst b/peps/pep-0823.rst index 81dbed49a1f..6716e7b0f8b 100644 --- a/peps/pep-0823.rst +++ b/peps/pep-0823.rst @@ -589,6 +589,10 @@ example at the closing parenthesis in ``(a?.b).c``, or at the comma in ``f(a?.b, c)``. A subscript or call that is part of the chain does not end it: ``a?.b[c](d).e`` still returns ``None`` when ``a`` is ``None``. +A ``?.`` or ``?[ ]`` inside a call argument or subscript only affects +that subexpression; for example, ``a.b(c?.d).e`` still calls ``a.b`` and +accesses ``.e`` even if ``c`` is ``None``. + .. warning:: ``a?.b.c`` is not the same as ``(a?.b).c``! When ``a`` is ``None``, From 4002be422a6f1ba171e5506c7e3a9311ed456565 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 20 Oct 2026 08:38:45 -0700 Subject: [PATCH 6/7] Reflow a para --- peps/pep-0823.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/peps/pep-0823.rst b/peps/pep-0823.rst index 6716e7b0f8b..ce9b546ba6a 100644 --- a/peps/pep-0823.rst +++ b/peps/pep-0823.rst @@ -1112,9 +1112,9 @@ itself once a PEP has been accepted. Any spelling mistake will also raise a ``SyntaxError``. It can be helpful to think of ``?.`` and ``?[ ]`` as a combination of -two different actions which need to be performed in a certain order. The ``?`` postfix -represents an ``is not None`` check on the subexpression and as such -should always come first. +two different actions which need to be performed in a certain order. +The ``?`` postfix represents an ``is not None`` check on the subexpression +and as such should always come first. Not obvious what ``?.`` and ``?[ ]`` do --------------------------------------- From 1dddb50bf161468ae6843fab6178262cd1c49d81 Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 20 Oct 2026 08:44:29 -0700 Subject: [PATCH 7/7] Mention truthiness in 'When to use' --- peps/pep-0823.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/peps/pep-0823.rst b/peps/pep-0823.rst index ce9b546ba6a..1ba141a952d 100644 --- a/peps/pep-0823.rst +++ b/peps/pep-0823.rst @@ -653,6 +653,10 @@ The same applies to conditional expressions like this: return user.account if user is not None else None +If you see a truthiness check such as ``user.account if user else None``, +check whether it should test specifically for ``None``. If so, consider +writing ``user?.account`` instead. + When not to use ---------------