Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 824 – None-coalescing operators

PEP 824 – None-coalescing operators

Author:
Marc Mueller
Sponsor:
Guido van Rossum <guido at python.org>
Discussions-To:
Pending
Status:
Draft
Type:
Standards Track
Created:
20-Sep-2026
Python-Version:
3.16

Table of Contents

Abstract

This PEP proposes adding two new operators.

  • The “None-coalescing” operator ??
  • The “None-coalescing assignment” operator ??=

The general idea is to provide a conditional operator, similar to or, which instead of truthiness, checks for None values.

The “None-coalescing” operator evaluates the left-hand side, checks whether it is not None, and if not, returns the result. If the value is None, the right-hand side is evaluated and returned.

The “None-coalescing assignment” operator will only assign the right-hand side to the left-hand side if the left-hand side evaluates to None.

They are roughly equivalent to:

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

# a ??= b
if a is None:
    a = b

Motivation

First officially proposed over ten years ago in (the now deferred) PEP 505, the idea to add None-coalescing operators has been along for some time now, discussed at length in numerous threads, most recently in [1] and [2]. This PEP aims to capture the current state of discussion and proposes a specification for addition to the Python language. In contrast to PEP 505, it will only focus on the two None-coalescing operators. See the Deferred Ideas section for more details.

None-coalescing operators are not a new invention. Several other modern programming languages have so called “null coalescing” operators, including TypeScript [3], ECMAScript (a.k.a. JavaScript) [4] [5], C# [9], Dart [11] [12], Swift [13], Kotlin [14], PHP [15] [16] and more.

The general idea is to provide a conditional operator, similar to or, which instead of truthiness, checks for None values.

Explicit checks for None

It is common in Python to use None as a default value that cannot be confused with other inherent default values like [], "" or 0. Most code working with such functions either does some kind of validation to make sure a valid value is actually returned, maybe returning early or raising an exception if it is not, or provides a default / fallback value. To do this, it is common to use is None / is not None checks.

class User:
    name: str | None
    @property
    def age(self) -> int | None: ...

def show_user_age(user: User):
    age = user.age
    if age is None:
        age = "unknown"
    print(f"The user age is {age}")

Though the intent is clear, it is quite verbose. The target name has to be repeated multiple times just to be able to assign a default value to the target variable. It is possible to write this in a more concise form using an if-expression. However, that has its own issues.

def show_user_age(user: User):
    age = user.age if user.age is not None else "unknown"
    print(f"The user age is {age}")

Especially for fairly simple value expressions, it is not uncommon to just repeat them, like in the example above, instead of using an assignment expression. This could cause problems if the value expression has side effects which would be executed twice now. Furthermore, there is not a single agreed upon way to write these expressions. Instead of putting the value first, some might prefer to invert the check to use is None instead. Reader need to constantly be aware of these challenges, increasing the mental load.

Using the “None-coalescing” operator ?? instead, helps to keep the expression short and predictable while still clearly communicating the intent.

def show_user_age(user: User):
    age = user.age ?? "unknown"
    print(f"The user age is {age}")

Overwrite None values

Sometimes it might be necessary to assign a fallback value inside an object. To do so, the expression is usually written twice. Once for the is None check, and again for the assignment.

def fix_user_name(user: User):
    if user.name is None:
        user.name = "unknown"

Using the “None-coalesce assignment” operator ??= helps to avoid repeating the expression. Especially for more complex once, this will make it easier to read and write.

def fix_user_name(user: User):
    user.name ??= "unknown"

Defaults for function arguments

Function argument defaults are evaluated in the parent scope. That is a common issue in cases where the default value is a mutable object or depends on the function context itself. In these cases, a typical solution is to allow None as argument and assign the fallback value inside the function itself.

def show_user_name(user: User | None):
    if user is None:
        user = create_default_user()
    print(f"The user name is {user.name}")

This could be rewritten as:

def show_user_name(user: User | None):
    user ??= create_default_user()
    print(f"The user name is {user.name}")

Specification

The None-coalescing operator

The ?? operator is added. It first evaluates the left-hand side. The result is cached, so that the expression is not evaluated again. If the value is not None, the cached result is returned. If it is None, the right-hand side expression is evaluated and returned instead.

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

Precedence

The precedence of ?? will be lower than or but higher than conditional expressions. Parentheses can be added as necessary to modify the precedence of individual expressions. A few examples of how implicit parentheses would be placed:

# x or y ?? 2
(x or y) ?? 2

# "Hello" if x ?? True else 0
"Hello" if (x ?? True) else 0

AST changes

A new Coalesce operator is added to boolop for use in BoolOp nodes.

expr = BoolOp(boolop op, expr* values)
    | ...

boolop = And | Or | Coalesce

Grammar changes

A new ?? token is added, as well as a new coalesce rule. Every rule which previously referenced the disjunction rule is updated to refer to the coalesce rule instead.

coalesce:
    | disjunction ('??' disjunction)+
    | disjunction

disjunction:
    | conjunction ('or' conjunction)+
    | conjunction

The None-coalescing assignment operator

The ??= operator is added. It performs a conditional assignment. As such it will first evaluate the left-hand side and check that the value is None and only then evaluate and assign the result from the right-hand side. If the first value is not None, the evaluation and assignment of the right-hand side are skipped.

# a ??= b
if a is None:
    a = b

Subexpression caching

Subexpressions on the left-hand side are only evaluated once before being cached. This is similar to augmented assignments.

# a.b.c ??= d
if (_t1 := a.b).c is None:
    _t1.c = d

# func().var[other()] ??= x
if (_t2 := func().var)[(_t3 := other())] is None:
    _t2[_t3] = x

AST changes

A new BoolAssign AST node is added. Similar to AugAssign, it stores a target and value expression as well as the boolean operator Coalesce.

stmt = ...
    | AugAssign(expr target, operator op, expr value)
    | BoolAssign(expr target, boolop op, expr value)

Grammar changes

A new ??= token is added. Additionally, the assignment rule is extended to include the “None-coalesce assignment”.

assignment:
    | NAME ':' expression ['=' annotated_rhs]
    | ('(' single_target ')'
        | single_subscript_attribute_target) ':' expression ['=' annotated_rhs]
    | (star_targets '=')+ annotated_rhs !'=' [TYPE_COMMENT]
    | single_target augassign ~ annotated_rhs
    | single_target boolassign ~ annotated_rhs

boolassign:
    | '??='

Backwards Compatibility

Existing programs will continue to run as is. So far code which used either ?? or ??= raised a SyntaxError.

Security Implications

There are no new security implications from this proposal.

How to Teach This

In a practical sense it might be helpful to think of the “None-coalescing” operator ?? as a special case for the conditional or operator, with the caveat that ?? checks for is not None instead of truthiness. As such it makes sense to include ?? when teaching the other conditional operators and and or.

The “None-coalescing assignment” operator ??= can be best thought of as a conditional assignment operator. As it is closely related to ??, explaining these together would make sense. Though it looks related to binary assignment operators like += as well, it is worth pointing out the distinction between these. Since ??= is a conditional assignment operator, the right-hand side will be skipped entirely in some case, while += always evaluates both sides.

Reading expressions out loud

Reading expressions out loud is always lossy. This PEP does not intent to define an unambiguous way of speaking these operators. The following is therefore merely meant as a suggestion.

None-coalescing operator

Code Pattern Example
user.age ?? "unknown"
“… or … if None” “user dot age or unknown if None
“… coalesce with …” “user dot age coalesce with unknown”

None-coalescing assignment operator

Code Pattern Example
user.name ??= "unknown"
“if … is None, assign …” if user dot name is None, assign unknown”
“assign … to … if None” assign unknown to user dot name if None

Reference Implementation

A reference implementation is available at https://github.com/cdce8p/cpython/tree/pep824-none-coalescing-operators. An online demo can be tested at https://pep823-and-pep824-demo.pages.dev/.

Deferred Ideas

None-aware access operators

PEP 505 also suggest the addition of the “None-aware access” operators ?. and ?[ ]. As the “None-coalescing” operators have their own use cases, the “None-aware access” operators were moved into a separate document, see PEP-823. Both proposals can be adopted independently of each other.

Other conditional assignment operators

While this PEP is focused on the “None-coalescing assignment” operator, it is worth pointing out that the logic behind it could easily be extended to cover and= / or=. This would match the existing conditional assignment operators &&= and ||= in other programming languages such as ECMAScript (a.k.a. JavaScript) [6] [7], Ruby [17] and Perl [18].

Rejected Ideas

Add new (soft-) keyword

Python does have a history of preferring keywords over symbols. For example, though a lot of languages use && and || as conditional operators, Python uses and and or respectively. As such it was suggested to use a new (soft-) keyword, e.g. otherwise, instead of ??.

While the keywords and and or help avoid ambiguity with the binary operators & and |, there is not a corresponding binary operator for ??. Furthermore, both keywords are well established in spoken and written language and therefore immediately obvious to the reader. Not to mention they are also quite short with just two and three characters.

In comparison, a new (soft-) keyword would likely not have the same benefits. There is no established short name for it, so while ideas like otherwise could be added, they do not convey an inherent meaning and for that reason do not provide an immediate advantage. In contrast, the ?? operator is well-known from other major programming languages.

Lastly, using a (soft-) keyword for the “None-coalescing assignment” operator poses additional questions and readability concerns.

a = otherwise b

a otherwise= b

Add ?? as a binary operator

PEP 505 originally suggested to add ?? as another binary operator. As such it would have bound more tightly than the proposed specification.

Though this would have worked fine, it would have suggested that ?? is similar to other binary operators like + or **. This is not the case. While binary operators first evaluate the left- and right-hand side before performing the operation, for ?? only the left-hand side is evaluated if the values is not None. The “None-coalescing” operator is much more closely related to the conditional operators or and and which also short-circuit the expression for truthy and falsy values respectively.

Furthermore, setting the precedence between or and conditional expressions matches other languages which have implemented the operator, like JS [8] and C# [10].

Add ??= as AugAssign

PEP 505 also suggested to add ??= as an AugAssign node.

So far AugAssign is only used for binary operators, including ??= which is a conditional assignment operator would therefore be confusing. Furthermore, AugAssign statements always evaluate the left- and right-hand side, without any short-circuiting. This is a major difference compared to “None-coalescing” assignments.

Make ??= atomic

It was proposed to make the “None-coalescing” assignment operator atomic to avoid race conditions when different threads try to use ??= on the same target. As the operator first checks if the left-hand side evaluates to None before assigning a value, it is possible that two threads see None and try to overwrite one another.

This was rejected since other operators with multiple steps, for example +=, are not atomic either. In cases where race conditions are an issue, existing synchronization primitives like locks should be used instead.

Common objections

Just use a conditional expression

The “None-coalescing” operators can be considered syntactic sugar for existing conditional expressions and statements. As such some questioned whether they would add anything meaningful to the language as a whole.

As shown in the Motivation section, there are clear benefits to using the “None-coalescing” operators. To summarize them again:

  • They help avoid repeating the variable expression or having to introduce a temporary variable.
  • Clear control flow, no more if ... is not None else ... and the inverse if ... is None else ... in the same code blocks, reducing the mental load while reading code.
  • More concise while also being more explicit.

Proliferation of None in code bases

One of the reasons why PEP 505 stalled was that some expressed their concern how “None-coalescing” and “None-aware” operators will affect the code written by developers. If it is easier to work with None values, this will encourage developers to use them more. They believe that e.g. returning an optional None value from a function is usually an anti-pattern. In their ideal world the use of None would be limited as much as possible, for example with early data validation.

It is certainly true that new language features affect how the language as a whole develops. Therefore any changes should be considered carefully. However, just because None represents an anti-pattern for some, has not prevented the community as a whole from using it extensively. Rather the lack of “None-coalescing” operators has stopped developers from writing concise expressions and instead often leads to more complex code which is more difficult to read than necessary, see the Motivation section for more details.

None is not special enough

Some mentioned that None is not special enough to warrant dedicated operators.

None-coalescing” operators have been added to a number of other modern programming languages. Furthermore, adding ?? and ??= is something which was suggested numerous times since PEP 505 was first proposed over ten years ago.

In Python None is frequently used to indicate the absence of something better or a missing value. As such it is common to look specifically for None values, for example, to provide a default or fallback value.

There are better default values than None

It was pointed out that there are better domain-specific values to signal the absence of a value. For example using "" or an empty collection as default where appropriate can eliminate unnecessary code.

While this is helpful to keep in mind, it does have its limitations whenever the inherent default value is also a valid one. As example, for number values 0 often cannot be used since there is no way to differentiate it between being used as a default or as an actual value.

It also does not work for custom types since those often do not have default types at all.

Use custom sentinels instead of None

In Python 3.15, PEP 661 added the option to define custom sentinels using sentinel(...). This addressed an issue in cases where None itself is a valid value and thus could not be used as sentinel.

In general though, None will still be preferred if a sentinel is needed, simply because it already exists for that exact purpose and is easier to use.

Late-bound function argument defaults

Some suggested PEP 671, currently in draft and last updated 2022, might be a better solution for the problem describe in Defaults for function arguments. While it arguably could be helpful in some cases, using the syntax suggested in PEP 671 here just shifts the responsibility upstream because the function signature itself would need to be changed. Instead of user: User | None as argument, it would be user: User => create_default_user(). It would be up to the caller now to make sure None is never passed to the function and the argument is omitted instead.

PEP 671 cannot help though with other use cases like overwriting None values inside an object, as shown in Overwrite None values.

Footnotes