None-aware operators (PEP 823 / 824) — draft for 3.16
None-aware operators (PEP 823 / 824) — draft for 3.16
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 syntaxUntil 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
adaCase 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 NoneCase 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
xProposed 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 = 1is aSyntaxError(Load context only).a?.b.c[0].f()short-circuits the whole primary tail ifaisNone.(a?.b).cdoes not — the group ends the short-circuit.- Precedence for
??(824): lower thanor, 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
- Run
pep823_today.pyon your interpreter — confirmSyntaxError. - Read PEP 823 Motivation + Short-circuiting; skim Deferred Ideas so you do not invent
?()or!. - Read PEP 824 and rewrite one
x if x is not None else yin a scratch file as a comment showing the proposed??form — do not commit that syntax to production yet. - Optional: the PEPs link a reference implementation and an online demo; use those only as sandboxes, not as a dependency.
Sources
- PEP 823 – None-aware access operators (Draft, Python-Version: 3.16)
- PEP 824 – None-coalescing operators (Draft, Python-Version: 3.16)
- Historical context: deferred PEP 505