PEP 824 – None-coalescing operators

- Author:

- Marc Mueller

- Sponsor:

- Guido van Rossum <guido at python.org>

- Discussions-To:

- Discourse thread

- Status:

- Draft

- Type:

- Standards Track

- Created:

- 20-Sep-2026

- Python-Version:

- 3.16

- Post-History:

- 22-Sep-2026

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.

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.

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:

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:

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

None-coalescing assignment operator

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 inverseif ... 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

Copyright

This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.