diff --git a/peps/pep-0823.rst b/peps/pep-0823.rst index ccb74052c6c..1ba141a952d 100644 --- a/peps/pep-0823.rst +++ b/peps/pep-0823.rst @@ -538,19 +538,136 @@ 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 +-------- + +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``. + +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``, + the former returns ``None``, but the latter raises ``AttributeError``. + Parentheses end the chain, so the final ``.c`` is evaluated normally. + +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: -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. +.. code-block:: python + + def get_account(user: User | None) -> Account | None: + if user is None: + return None + return user.account -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. +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 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 +--------------- + +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. + +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,11 +1115,10 @@ 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 -represents an ``is not None`` check on the subexpression and as such -should always come first. +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. Not obvious what ``?.`` and ``?[ ]`` do --------------------------------------- @@ -1244,6 +1360,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 =========