None-coalescing operator and null‑coalescing assignment

[Pre-PEP] None-coalescing operator ?? and null-coalescing assignment ??=

Author: Rainwalker

Target version: Python 3.16 (or later)

Status: Pre‑PEP (discussion)


Note: This is a narrow subset of PEP 505 (no ?. or ?[]).


Problem

Currently, the standard way to provide a default value when something is None is:

value = get_value() if get_value() is not None else default

This has two major issues:

  1. Redundancy – the expression get_value() is written twice. When the expression is long or has side effects, this becomes error‑prone and hurts readability.
  2. Double evaluation & side effects – if get_value() is a function call, it is executed twice. Worse, if it mutates state (e.g., list.pop()), the second evaluation may produce a different value or corrupt data.

A workaround is to store the result in a temporary variable:

tmp = get_value()
value = tmp if tmp is not None else default

But this adds an extra line and still feels verbose.

The or operator is not suitable because it treats all falsy values (0, False, "", [], etc.) as triggers, not only None:

value = get_value() or default   # wrong when get_value() returns 0 or False

Similarly, assigning a default value only when a variable is None requires an explicit check:

if x is None:
    x = default

This is verbose and breaks the flow of expression‑oriented code.


Proposal

Introduce two new operators:

  1. None‑coalescing operator ??
value = get_value() ?? default
  • Evaluates the left‑hand side.
  • If the left‑hand side is not None, that value is returned and the right‑hand side is not evaluated (short‑circuiting).
  • If the left‑hand side is None, the right‑hand side is evaluated and returned.

Chainability

Multiple ?? operators can be chained. The expression returns the first non‑None value:

result = a ?? b ?? c ?? d

This is equivalent to:

if a is not None:
    result = a
elif b is not None:
    result = b
elif c is not None:
    result = c
else:
    result = d

Precedence

The ?? operator should have the same precedence as or. For example:

a + b ?? c * d   # parsed as (a + b) ?? (c * d)

None-coalescing assignment ??=

x ??= default

This is a shorthand for:

if x is None:
    x = default

However, unlike other augmented assignments (+=, |=, etc.), ??= evaluates the right‑hand side only if the left‑hand side is None (short‑circuiting). It does not return a value – it is a statement, just like +=.


Examples

# ?? – expression
first_title = web_search("python") ?? "No results found"
user = db.fetch(user_id) ?? default_user
data = api.get(key) ?? cache.get(key) ?? fallback_value

# Avoid double evaluation
value = expensive() ?? default   # instead of expensive() if expensive() is not None else default

# Safe with side‑effect functions
data = stack.pop() ?? backup_data

# ??= – assignment
key ??= compute_default(key)   # only compute if missing
config.timeout ??= 5.0
user.nickname ??= "Anonymous"

# Works with any assignment target (variable, attribute, subscript)
obj.attr ??= 42
lst[index] ??= 0

Why this belongs in Python

  • Other mainstream languages already have ?? – C#, JavaScript, PHP, Swift, Kotlin (Elvis operator). Many of them also have ??= (e.g., C#, JavaScript, PHP). This demonstrates utility and low learning curve.
  • Addresses real, frequent patterns – Many codebases contain x if x is not None else y or if x is None: x = default.
  • Does not break backward compatibility – ?? and ??= are currently syntax errors.
  • ??= is a natural extension – just like += complements +, ??= complements ??.

Open questions for discussion

  1. Precedence of ?? – Should it have the same precedence as or? (Proposed: yes.)
  2. Chaining of ?? – Should a ?? b ?? c be allowed? (Proposed: yes.)
  3. Interaction with await – Should await foo() ?? default be allowed? (Proposed: yes.)
  4. ??= for attributes/subscripts – Should it work with obj.attr ??= val and lst[i] ??= val? (Proposed: yes – same as regular assignment.)

Rejected alternatives

  • or – does not distinguish None from other falsy values.
  • if‑expression – verbose and repeats the expression.
  • coalesce built‑in function – cannot provide short‑circuiting without a lambda, which harms readability.
  • Separate PEP for ??= – better to propose together as a consistent pair.

Implementation sketch

The change would require:

  • Adding new tokens ?? and ??= to the grammar.
  • Adding new AST nodes (or reusing BinOp with a new operator for ??; a new assignment node for ??=).
  • Implementing short‑circuit evaluation for both.
  • Updating the precedence table.

No changes to existing syntax or semantics are needed.


Questions for the community

  • Are ?? and ??= desirable enough to warrant new operators?
  • Should we adopt the exact same semantics as in C#/JavaScript (only None triggers fallback)?
  • Are there any hidden pitfalls with short‑circuiting or precedence?

Thank you for reading! I look forward to your feedback.

9 Likes

See also PEP 505 – None-aware operators and previous discussions about this.

3 Likes

Community guidelines are to check for existing posts before starting a new ones. Is there anything new here which hasn’t been discussed previously?

edit: the post now makes more sense as being a subset of PEP 505 excluding safe navigation features.

I have studied PEP 505 and the historical discussions carefully. While the full PEP 505 (including ?. and ?[]) was deferred due to complexity and concerns about debugging, my proposal is a deliberately narrow subset – it only includes ?? and ??=, excluding the controversial parts entirely.

I believe ?? deserves a fresh look for two reasons:

  1. It solves the real problem that or cannot distinguish 0/False from None.
  2. More importantly, ??= provides atomicity that the manual if x is None: x = default cannot offer – no race condition between the check and the assignment, which matters in concurrent contexts.

Given that this is the safest, most focused part of PEP 505, I think it’s worth reconsidering on its own merits.

Looking forward to your further thoughts.

6 Likes

You’re right to call this out. I should have been more explicit about prior discussions in the opening post. My apologies.

That said, I do believe there’s something new here compared to PEP 505 and previous ?? discussions:

  1. Scope narrowing – Previous proposals (like PEP 505) bundled ?? with ?. and ?[], which raised major concerns about hiding AttributeError and making debugging harder. My proposal is only ?? and ??= – the safest, most focused subset.

  2. Atomicity – This is the key new argument. The manual if x is None: x = default pattern has a race condition between the check and the assignment. ??= can be implemented as an atomic operation (check + assign in one step), which is impossible with the current pattern. This matters for shared state in concurrent contexts (threads, asyncio, caches).

  3. The or problem remains real – Python still lacks a concise way to say “use this value, but if it’s None, fall back to that one”. Using or for this is a common source of bugs when 0, False, or '' are legitimate values.

Previous discussions rejected the family of operators, not necessarily this single operator on its own. I think it’s worth revisiting the narrow version.

Thank you for holding me to the guidelines – I’ll update the opening post to reference PEP 505 and prior discussions clearly.

4 Likes

I have no strong opinion on this, mostly because I don’t particularly need this functionality. The reason I reacted is that there is, a lot of, previous discussions about this functionality that you don’t acknowledge in your initial post.

You’re right – I should have referenced PEP 505 in the opening post. I’ll edit it now to add a clear note. Thanks for the feedback.

Thanks for your post @Rainwalker. May I suggest you catch up on Revisiting PEP 505 – None-aware operators? That’s where the last discussion has happened. Would be great if we could keep it all together.

As for the particular idea, I’m working on a PEP for ?? and ??= already. You can find a link to an earlier draft at the bottom of that post. Will need to do at least one more round of rewrites and haven’t had the time to do it yet unfortunately. Hopefully soon though so it can target 3.16.

3 Likes

Thanks for letting me know. I didn’t realize someone was already working on this.

I’ll follow the existing discussion you mentioned. Let me know if I can help — I think the atomicity argument for ??= is a strong point worth including.

If it has two major issues, it does not make it the standard approach.

This could be the modern approach:

if (value := get_value()) is None:
    value = default

Or any explicit if value is None check. Alternatively, you can add a default argument in the get_value() function if it is expected to return None, and so on.

dict.get() is a useful precedent for returning a default value, with None as the default. In other words, it is better to follow existing patterns for handling missing values.


If ?? were only used in cases where a default value is already available, that is quite a narrow use case in general, and it is often just as easy to provide a default argument in the function or method.


It is also useful to study previous discussions on similar ideas. For example, the argument that another language has a given syntax has been refuted many times, not only for this syntax but for others as well.

2 Likes

Thank you for your edit!

Thanks for the feedback.

A few points:

  1. := + if still requires a temporary variable and two lines. ?? is more concise and avoids the race condition in concurrent contexts.

  2. Adding a default parameter isn’t always possible (third-party functions, built-ins, attributes).

  3. dict.get() is good for dicts, but ?? works uniformly for variables, attributes, subscripts, and expressions.

  4. Other languages aren’t the main argument — they just show the concept works and is intuitive.

What race condition does := suffer that ?? is intended to avoid?


I would be happy with a builtin:

coalesce(value, default)

It is better than saying “value double question mark”. By the way, how should ?? be read?

Others will have their own opinions, but I read it as ‘else’:

could_be_none ?? "Default"

Reads as “could be none, else default”

2 Likes

Race condition: if (value := get()) is None: value = default is still two operations (check + assign). Another thread/task could modify value between them. ??= is atomic – the check and assign happen as one step.

Builtin coalesce: A function can’t short-circuit. With coalesce(x, expensive_default()), expensive_default() always runs, even when x is not None. That defeats the purpose.

1 Like

Thank you for your advice. I like it, but I’d prefer “otherwise”, because “else” is defined.

1 Like

Is this atomic?

a += 1

If not why should ??= be atomic with the side effect of locking overhead?

2 Likes

You’re right – a += 1 is not atomic (it does a read-modify-write).

The atomicity I’m claiming for ??= is more limited: it ensures the check for None and the assignment (if needed) happen as a single step, without another thread seeing the target in between.

This is still better than the manual if x is None: x = default, where the check and assignment are two separate statements. A thread could see x as None after the check but before the assignment.

No locking overhead – just the same bytecode-level guarantee as +=, but applied to the check-then-assign pattern.

Yes, there was a lengthy discussion on this in the PEP 505 thread.
To be clear, I’m not saying it should be the keyword, just that I read it in my head that way - where there is no syntax or keyword rules :slight_smile:

1 Like

Then it not very useful when running multiple threads I assume.