Revisiting PEP 505 – None-aware operators

There are other none-aware operator candidates worth discussing:

  • None-conditional function invocation: fn?()
  • None-aware await: await?
  • None-conditional assignment: a?.b = c
  • Non-none-coalesing operator

I’m particularly interested in a ‘non-none-coalescing’ operator.

None-conditional function invocation – fn?()

This is mentioned as a rejected idea in @cdce8p’s PEP. I agree that ?( is sufficiently different in nature from ?. and ?[, but I guess most users would expect its presence among them.

I actually didn’t know for a long time you could do fn?.() in Javascript/Typescript. I don’t recall ever encountering it. I wonder why did they decided to make it ?.( and not ?( for brevity. Either way, its use is likely too rare to be worth it, but I also don’t see much other reason against it, since, to me, it’s pretty clear what the meaning is.

Also, it would be a bit of a shame to see a.foo?.__call__(arguments) become idiomatic rather than a.foo?(arguments).

None-aware await – await?

If await is given an expression involving ?. then a TypeError is bound to occur. The ‘none-aware await’ operator await? helps by returning None instead of raising TypeError when given None.

I saw that there was some awkwardness surrounding the use of ?. in an await expression in C#. The ‘null-conditional await’ operator is currently being proposed for that language. A SO answer suggests the following workaround:

await (this.MyObject?.MyMethod() ?? Task.CompletedTask)

The problem is dodged in Typescript since any value is awaitable in that language. Although you could write something similar to ?? Task.CompletedTask by ?? Promise.resolve(null).

The issue we have for Python is that we don’t have a succinct way to write a resolved Future. Without await?, if ?. were added to the language we’d end up with a lot of this stuff in async code:

v = obj?.method()
if v: await v

Or

completed_future = Future()
completed_future.set_result(None)
...
await obj?.method() ?? completed_future

Null-conditional assignment – a?.b = c

This was added very recently to C# (v14). This feature lets you use ?. on the LHS of an assignment. The C#-ers seem to like it because apparently the following code pattern is very common:

if foo is not None:
    foo.bar = baz

Useful?

Non-none coalescing operator – &?

If ?? is like or, what is the analog to and? It would be the ‘non-none-coalescing’ operator which can be thought of as the inverse of the none-coalescing operator. It says: “if the left operand is none, keep the none, otherwise take the right-hand operand”.

The obvious symbol candidate for a ‘non-none-coalescing’ operator that comes to mind is ‘!?’. I’m going to elect ‘&?’ though since it parallels with and better without suggesting any… unnecessary inverse-ness.

This operator doesn’t currently exist in any language, but I think it has a lot of use potential and the pattern occurs a lot in my code. The operator is essentially used to replace the ternary: None if foo is None else bar(foo)foo &? bar(foo).

My issue with the ternary is that many prefer the is not version in order to put the juicier part at the front of the expression, while others like to deal with the null first. My own preference seems to depend on the weather, and having to make the decision every time slows me down. If we had a non-none-coalescing operator there would be one obvious way to write this pattern.

2 Likes

Thanks for the details post @Pyprohly!

None-conditional function invocation – fn?()

While writing the draft and working on the implementation, I thought about this as well. In the end, I decided against including it and instead moved it to the Deferred ideas section. A lot of the potential use cases I’ve seen in my own projects were some kind of optional callback functions, e.g.:

class A:
    value_cb: Callback[..., int] | None

def func(var: A):
    if var.value_cb is not None:
        value = var.value_cb()

While var.value_cb?() would certainly be useful, it’s not as big of a deal as traversing nested objects with optional attributes. From my experience optional functions are just much more likely to be the last part in an expression, compared to attributes and subscripts.

Therefore, I believe it makes sense to focus on ?. and ?[ ] for the initial PEP. fn?() can always be added later on once we have some more experience with how the None-aware access operators are used.

The original language proposal for JS mentioned that it would have been difficult for the parser to distinguish it from a ternary expression a ? b : c.

None-aware await – await?

I agree that using ?. in an await expression will likely lead to errors at some point. As I’ve mentioned in the section on await, this is not all that different from awaiting an optional variable. I expect type checkers will emit an error for it, just like they do for optional variables today. Even linters (like flake8, pylint and ruff) might flag these cases since they can be detected from looking at the AST alone.

It’s important to keep in mind that the draft doesn’t attempt to solve every possible case with a nice syntax. Sure await? could be added but I think this is out of the scope for now.


Another case I came across were in comparisons with optional values.

if a in b?.c.d: ...

This will be problematic as well but can also be solved with existing language constructs today.

if a in (b?.c.d or ()): ...

# even better with '??'
if a in (b?.c.d ?? ()): ...

If someone is motivated enough, exploring something like in? might also be interesting later on.

Null-conditional assignment – a?.b = c

This will not work with the current definition. Keep in mind that a?.b is more or less equivalent to

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

Assigning c here would be equal to (None) = c for a = None. That doesn’t really make sense which is why I explicitly disallowed assignment.

The existing short-circuiting behavior was already discussed heavily. I think extending it to cover a?.b = c will be a bridge too far.

Non-none coalescing operator – &?

On of the arguments I made for the ?? operator is that it’s confusing to have ... if not None else ... and the inverse ... if None else ... in the same code base, sometimes even the same code block, see the motivation section. With ?? it’s always the variable expression first and the fallback second. As such reading it will require a bit less mental energy as you don’t need to look out for these kinds of subtly differences. I’d therefore be against adding an inverse operator like &? since it negates this benefit.

11 Likes

Null safe operator was yet again for this year the most popular Javascript syntax feature: 2025 State of JS Results

16 Likes

That’s a frequent use case.

The new syntax would also help type linters narrow types with precision and confidence. Currently it’s difficult to get rid of None warnings after something was typed T|None.

I tried this at one point, but it doesn’t properly cover all use cases, and type linters do not bind the new type:

def notnone[T](value: Any | None, default: T) -> T:
    return value if value is not None else default
def f(x=None):
    x = notnone(x, [])
    x.append(1) # None warning unless framed with isinstance(x, list)
    return x

There may be non-syntax solutions to some use-cases with functions defined in typing as long as type linters are ordered to honor their semantics.

1 Like

You can kinda hack it with some overloads:

@overload
def fallback[D](val: None, default: D) -> D: ...
@overload
def fallback[T](val: T, default: Any = ...) -> T: ...
def fallback[T, D](val: T | None, default: D = None) -> T | D:
    return val if val is not None else default

def f(x: int | None = None):
    if x is not None:
        assert_type(fallback(x, [1,2,3]), int)
    else:
        assert_type(fallback(x, [1,2,3]), list[int])

But at that point why do we even need the notnone/fallback?

def f(x: int | None = None):
    y = x if x is not None else [1,2,3]
    return y

Ends up with the same type signature

1 Like

I’m curious what type checker you’re using where this is the case. A simple assert x is not None or if x is None: x = default (depending on the circumstances) solves these problems in all the checkers I’ve used.

2 Likes

It is indeed difficult to write a version of notnone that works across type checkers; my attempts were in Add not_none by JelleZijlstra · Pull Request #7 · hauntsaninja/useful_types · GitHub .

But that’s a problem we can solve independently; PEP 505 won’t help with it. So I don’t think it makes much sense to discuss it here.

4 Likes