None-aware operators (PEP 823 / 824) — draft for 3.16

Authors

K19G

grok-bot

Updated

October 31, 2026

None-aware operators (PEP 823 / 824) — draft for 3.16

Important

Draft status. PEP 823 (None-aware access operators) and PEP 824 (None-coalescing operators) are Draft Standards Track PEPs (created 2026-09-20, target Python 3.16). Nothing in this chapter is shipping syntax today. CPython 3.13 and 3.14 still raise SyntaxError on ?., ?[ ], ??, and ??=. Do not invent operators beyond what those PEPs specify; treat every code sample that uses the new spelling as proposed, not runnable on current interpreters unless you deliberately use a PEP reference build.

Thesis

Nested optional attributes (always present, sometimes None) force a tax of is not None checks, temporary names, and duplicated subexpressions. PEP 823 proposes ?. and ?[ ] so a chain short-circuits to None when a base is None. PEP 824 separately proposes ?? and ??= so defaults attach on None without truthiness traps. Both revive ideas from deferred PEP 505, split into two independently adoptable PEPs, sponsored by Guido van Rossum.

Mental model

PEP 823 (access)                         PEP 824 (coalesce)
─────────────────                        ──────────────────
base?.attr     ≈  _t.attr if (_t := base) is not None else None
base?[key]     ≈  _t[key] if (_t := base) is not None else None

a ?? b         ≈  _t if (_t := a) is not None else b
a ??= b        ≈  if a is None: a = b

optional  = attribute always exists, value may be None
missing   = attribute / key not present at all  ← NOT handled by ?.
Operator PEP Checks Does not do
?. 823 is not None then attribute access Catch AttributeError for missing names
?[ ] 823 is not None then subscript Turn missing dict keys into None (use .get)
?? 824 left is not None else right Treat 0, "", [] as missing (unlike or)
??= 824 assign right only if left is None Always evaluate the right-hand side

Short-circuiting (823): if the base is None, the tail (further ., [ ], calls, and nested ?. / ?[ ]) is skipped. Parentheses break that chain: (a?.b).c raises if a is None, because the group finishes first.

Worked examples

Case 1: What ships today (verified on this box)

# pep823_today.py — CPython 3.13.5 on the writing box
import ast
import sys

print(sys.version.split()[0])
for src in ("a?.b", "a?[0]", "a ?? b", "a ??= b"):
    try:
        ast.parse(src)
        print(src, "→ parsed (unexpected on 3.13)")
    except SyntaxError as e:
        print(f"{src!r} → SyntaxError: {e.msg}")
$ python3 pep823_today.py
3.13.5
'a?.b' → SyntaxError: invalid syntax
'a?[0]' → SyntaxError: invalid syntax
'a ?? b' → SyntaxError: invalid syntax
'a ??= b' → SyntaxError: invalid syntax

Until 3.16 accepts the PEPs (if accepted), the boring equivalent is still is not None and :=.

Case 2: Nested optional attributes (PEP 823 shape)

Proposed spelling from the PEP (not valid on 3.13):

# pep823_customer_proposed.py — PROPOSED syntax (PEP 823); will not parse on 3.13/3.14
from dataclasses import dataclass

@dataclass
class User:
    name: str

@dataclass
class Customer:
    user: User | None

@dataclass
class Data:
    customer: Customer | None

def get_customer_name(data: Data) -> str | None:
    return data.customer?.user?.name.lower()

Equivalent today:

# pep823_customer_today.py — runnable equivalent of the PEP 823 example
from dataclasses import dataclass

@dataclass
class User:
    name: str

@dataclass
class Customer:
    user: User | None

@dataclass
class Data:
    customer: Customer | None

def get_customer_name(data: Data) -> str | None:
    if (customer := data.customer) is not None and (user := customer.user) is not None:
        return user.name.lower()
    return None


def main() -> None:
    print(get_customer_name(Data(None)))
    print(get_customer_name(Data(Customer(None))))
    print(get_customer_name(Data(Customer(User("Ada")))))


if __name__ == "__main__":
    main()
$ python3 pep823_customer_today.py
None
None
ada

Case 3: Structured data — ?. is not dict.get

PEP 823 is explicit: operators handle optional values, not missing keys. For TypedDict / JSON dicts, combine .get with proposed ?[ ]:

# proposed:  return data.get("customer")?["user"]?["name"].lower()
# today:
def get_customer_name(data: dict) -> str | None:
    customer = data.get("customer")
    if customer is not None:
        user = customer["user"]  # KeyError if missing — intentional
        if user is not None:
            return user["name"].lower()
    return None

Case 4: Coalesce without truthiness (PEP 824)

or collapses every falsy value. ?? (proposed) only replaces None:

# pep824_coalesce_today.py — runnable stand-in for a ?? b
def coalesce(a, b):
    return a if a is not None else b


def main() -> None:
    print(coalesce(0, 99))       # 0  — keep zero
    print(coalesce("", "x"))     # '' — keep empty string
    print(coalesce(None, "x"))   # x
    print(0 or 99)               # 99 — truthiness trap
    print("" or "x")             # x


if __name__ == "__main__":
    main()
$ python3 pep824_coalesce_today.py
0

x
99
x

Proposed forms from PEP 824: age = user.age ?? "unknown" and user.name ??= "unknown".

Case 5: Assignments and short-circuit edges (823)

Per the PEP:

  • a?.b = 1 is a SyntaxError (Load context only).
  • a?.b.c[0].f() short-circuits the whole primary tail if a is None.
  • (a?.b).c does not — the group ends the short-circuit.
  • Precedence for ?? (824): lower than or, higher than conditional expressions.

The trap

Reading PyCoder’s Weekly or a blog demo and pasting data.customer?.user?.name into a 3.14 codebase. It is still a SyntaxError. Equally bad: assuming ?. means “forgive missing attributes” the way some JS optional-chaining folklore suggests — PEP 823 rejects exception-aware operators so misspellings still raise.

The boring rule

Until 3.16 ships an accepted PEP: write is not None (and ??-shaped helpers only if your team wants them). When teaching the drafts, quote the PEPs, mark proposed, and keep runnable examples on today’s syntax.

Try this

  1. Run pep823_today.py on your interpreter — confirm SyntaxError.
  2. Read PEP 823 Motivation + Short-circuiting; skim Deferred Ideas so you do not invent ?() or !.
  3. Read PEP 824 and rewrite one x if x is not None else y in a scratch file as a comment showing the proposed ?? form — do not commit that syntax to production yet.
  4. Optional: the PEPs link a reference implementation and an online demo; use those only as sandboxes, not as a dependency.

Sources