Revisiting PEP 505 – None-aware operators

Which of course cuts both ways.

An anecdote: I learned Perl long before Python, and my O’Reilly Perl book was dogeared and well-worn, with lots of corners folded down as bookmarks to things that I always had to look up because they never stuck in my brain. When I got the first O’Reilly Python book, it stayed in near-new condition for years because Python just fit the way I thought and I so rarely had to look things up. 30-ish years later, it’s mostly the same. The things I have to look up for Python are almost never syntactic, except for some rarely used constructs like (for me) the match statement or exception groups. That’s something I’m sure most of us love about Python - it’s still incredibly easy to read, even when you encounter the dark corners of the language’s features.

A lesson that came out of the latest Steering Council election was that folks want more clarity and transparency, and feedback earlier in the PEP process, so that it feels a lot less like lofty pronouncements from on high. As a member of the 2026 council – and I’m just one of 5 voices – I’m trying to give more insight into the way I think and the questions I ask about proposals, especially ones that touch syntax. I care deeply in keeping Python Pythonic, whatever that means[1]. I’m just one voice, and while I have strongly held opinions, I also try to keep an open mind, and have actually been swayed for many proposals over years, PEP 750 template strings being a recent example.

PEP 505 predates the Steering Council model, so those authors really only had to convince Guido. That PEP is technically Deferred, although I would recommend that PEP 505 be formally Rejected and a new PEP be written for any future proposal along these lines.


  1. to someone who’s not Dutch :smiley: ↩︎

9 Likes

As I’ve mentioned a few times in this thread, this is an assertion I want to challenge. It may be technically true for any particular use case, but I am wondering if in practice this ability to intermix nullable and non-nullable attribute access is a use case worth supporting.

6 Likes

It should feel overwhelming. Considering all of this conditionality and the possibility of values being null is important and the markers are showing you what the code does which is nontrivial logic that is usually expressed much more verbosely in Python using at least an indented block.

Your suggestion was something like

x = maybe a.b.c.d.e

with the expectation that any part of the expression could fail. If I have to review or read this code this I should see it as:

x = a?.b?.c?.d?.e

There are four possible ways that something can be missing. I have to consider all four of those cases to understand how this code interacts with the state of object a. I also have to wonder if the author of the code considered those 4 possible ways. Most likely only one of them is relevant and I would much rather see which one:

x = a.b?.c.d.e

Now it is possible that b is present or missing but there are only two cases to consider. It is clear here that the author has singled out b and that is the reason that they did not just use normal attribute access for the whole expression. If I want to understand why this is there then I can go and think about why b might be missing and what that means in the context of the whole program/operation and whether or not setting x to None is reasonable in that case. I do not need to go and investigate whether c, d or e could ever be missing.

The fact that a missing attribute or an unchecked None leads to some kind of error at runtime (and in static typing) is generally a good thing because it shows you where there are bugs: you want the AttributeError if the attribute is not there and the fix is usually fixing a typo or adding the attribute or something.

I would expect maybe to be a magnet for people to write code that fails in unexpected ways like:

  • You add maybe because b might be missing.
  • Later on a bug appears and now c is sometimes missing but it goes unnoticed.
  • Now the wrong code path is taken for many objects that do have a valid b.

This is precisely analogous to having an overly broad try/except that either catches too many exception types or has too much code in the try block. At least if you review code like

x = a?.b?.c?.d?.e

then you can ask the author to justify the question marks and remove most of them.

With maybe I would expect never ending arguments where I tell someone not to use it because it is not correct and is a bug magnet but the alternative is more verbose and they desperately want the conciseness of maybe. It is not good to create this tight contention between correctness vs conciseness/elegance/etc.

14 Likes

That’s my point. I would definitely ask whether the intention was that only b could be missing, and request additional details on why that assumption is made. And I’d probably insist on some kind of comment for a future reader. Regardless of the number and placements of ?, I don’t think anything about that line of code is obvious.

2 Likes

My workflow around this in TS involves not putting any ‘?’ in at first, then looking for type errors in my IDE (usually red squiggles) and adding ‘?’ as needed only.

Anyways, I am on vacation and this is probably my last interaction with this thread for a while.

I also have a feeling that there’s not a lot of changing of minds happening at this point, so maybe it’s a good time for som reflection.

6 Likes

Or a new PEP :smiley:

3 Likes

a) it’s easy also to forget a “normal” null check. No problem. Or the linter will warn you, of you’ll get an exception at runtime

b) see this example:

>>> class A:  pass
>>> a = A()
>>> a.b = A()
>>> a.b.c = A()
>>> a.b = None
>>> a.b.c
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'c'

It seems to me clear that I “forgot” a ? after b

c) doesn’t .? raise a SyntaxError?

It could also be an addition to the PEP (or to the new PEP), instead of a substituition.

It’s difficult to find a keyword for ?. . Maybe a dot b dot c? (I’m joking)

To be honest, I don’t see the expression so much complicated to understand. Anyway, as I said, for me ??= is OK anyway.

1 Like

Thinking of what you want to do if a is None is a different task than thinking of what you want to do if b is None. It’s a mistake to think of them as the same thing because they represent different pieces of information.

If we have this code somewhere in our code base:

x = a?.b.c?.d

Assuming it’s good and correct, that means someone has thought about what we want to do if a is None or c is None, and it has been figured out that b won’t be None.

Now if someone makes a change to the code so that b can be None, the new type-checker error that shows up will remind us to think about what we want to do if b is None (which might not be the same thing we want to do if a or c are None)

With your proposal, we don’t have that reminder to think about what we want to do if b is None, so it will do the wrong thing.

Deciding what to do depends on what each variable represents.
In these toy examples you can’t see that because you can’t see any of what the variables represent. But that information is very relevant to deciding what you want the program to do for each of them.

11 Likes

Just noting that I don’t read uses of the ?. operator token-by-token, because that’s not how I read Python code. This quote sums up why I don’t read it that way and why I wouldn’t write such code:

A maybe or otherwise would suffice. I don’t see the value in using multiple ?. operators in a single line. It’s already clear that AttributeError exceptions are not being handled. Why is it necessary to tie the use of ?. to the optionality of each object field? This raises the question: can you use ?. if the object field is not optional?


Personally, I would simply wrap the entire line in a try/except block.

6 Likes

This is much appreciated, thank you @barry. I’d like to take a moment to respond to your concerns, in particular regarding the none-aware attribute access operator ?.

Why care which part of an expression can be None?

I find it helpful to think about this w.r.t. inputs and outputs. The output can be None as soon as one ?. is used, so it will always need to handle that. However, the input branches to consider are different. @oscarbenjamin already pointed that out in his excellent response. Having a?.b.c throw an AttributeError if b is suddenly None is a very much desired effect. Consider an API response, the invisible contract is that if a is not None, it will always have an object with attribute c. If the response changes so that b can be None, I’d want my code to fail as this is something I haven’t considered initially.

In a sense this is similar to try: ... except AttributeError: .... While possible, it’s considered undesirable because it can mask potential issues.

Alternative proposal: maybe a.b

I’d like to challenge the notion that a separate keyword would be easier to read than an operator. That might be true for simple expressions, once it gets more complicated though, it will almost certainly need an additional pair of brackets to clarify the intent.

if maybe a.b.some_call() == some_value or do_some_other_call():
    ...

maybe binds more closely than == but that isn’t obvious when reading it. In practice formatters would likely add brackets: if (maybe a.b.some_call()) == ...

Spelling ?.

There is certainly a learning curve to it but not much more than any other new syntax feature. It’s good to keep in mind, as others have pointed out already, that ?. is used by a number of other languages. Python doesn’t exist in a vacuum and many developers frequently switch between different languages. Reusing an existing established operator for that makes sense.

What helped me to get the spelling right, was to just consider ? for a moment. It’s a modifier to the base of the expression, so it should always come first. It doesn’t matter if it’s an attribute access ?. or index lookup ?[ ].

5 Likes

Spend some time working on a prototype implementation. Pablo created a great demo for PEP 810 a few months ago, so I figured something like that would help for the operators in PEP 505 (or the followup) as well. It likely still contains some bugs and the example code is quite contrived, but you can try ?., ?[ ], ??, ??= and even maybe: https://pep505-demo.pages.dev

The CPython implementation can be found here: GitHub - cdce8p/cpython at syntax-none-aware-pep505

8 Likes

Thanks for the response @cdce8p (and @oscarbenjamin). It’s helpful to understand your thinking, which will hopefully find its way into a future PEP.

I can appreciate that you’d want to be explicit about which attributes you expect to be optional and which you expect to be required. Let’s say you have this code:

try:
    x = a?.b.c?.d.e
except AttributeError as e:
    recover()

Presumably, you’d want to know whether c was missing from b or e was missing from d. I’m not sure whether you could dig that out of the exception in a reliable way.

Rhyming with existing languages is helpful, but gosh I can’t help wanting to write .? because to me, it more clearly says “the attribute access is optional”. Yes, we’ll all Just Learn It, but I still find it mildly disconnected.

5 Likes

I’m not even sure I know what the semantics are supposed to be after all the discussions but PEP 505 describes it as

# a?.b
_v = a
if _v is not None:
    _v = _v.b

If that is the intended behaviour then it is not that the attribute access (.b) is optional but rather than the object a whose attributes we query is optional. In that case I think that having the question mark on the left makes more sense: a?.b (if we have a then this is a.b) rather than a.?b (does a have a b?).

14 Likes

Good point, I concur.

3 Likes

I’m probably missing something, but to me the whole a.b.c.d… feels like using the wrong data structure?

On the one hand, my intuition is that if there is a nested structure with “paths” that may or may not exist, it is some sort of a tree.

json_result: dict[str, dict[str, dict] = ...

tree_result = Tree(json_result)
leaf = tree_result.get('trunk', 'branch', 'stick', 'twig') # .get(self, *args, *, missing=None)

On the other hand, my intuition for .field attribute access is that represent fields that we know exist.
If these attributes are suppose to exist, it feels like there is a “real” structure that can go in a class and constructed “properly”. It is then the responsibility of the class to give ergonomic access to possibly-missing fields..

class Extrucolator
    ...

    @property
    def is_ready(self):
        return self.babilobator and self.babiobator.clobnicator

    @property
    def clobnicator(self):
        return self.babilobator.clobnicator if self.is_ready else None

extrucolator = Extrucolator(...)
clobnicator = extrucolator.clobnicator

I know there were a lot more examples than just this heavily nested access, but then, if it’s not heavily nested, I’m not sure what is wrong with a.b if a is not None else None.

I mean, I’m definitely missing something, but I’m not seeing what is left between “looser data structures”, “tighter data structures”, and “two things in a one-liner”.

Okay for the first example I guess a tree would be at least thrice as controversial as null operator; maybe a pandas-style “accessor” pattern would work better?

json_result: dict[str, dict[str, dict] = ...

value = json_result.as_tree['a','b','c','d']
8 Likes

The AttributeError for it is actually quite helpful.

class A:
    b = None

a = A()
x = a?.b.c?.d.e
Traceback (most recent call last):
  File "//main.py", line 5, in <module>
    x = a?.b.c?.d.e
        ^^^^^^
AttributeError: 'NoneType' object has no attribute 'c'

It’s also not so much about knowing what exactly failed but rather that it fails at all. Without it a user won’t be able to distinguish between it wasn’t in the response and the response didn’t have the expected format. Both would just evaluate to None which is handled by the code.

I believe that’s still the current suggested syntax (and also the one I implemented). Real world examples often tend to omit the temporary variable which makes this look somewhat foreign. It’s necessary though as we actually want to cache the base name / attribute lookup value and not have to evaluate it twice.

A few real examples below. I replaced the variable names to make it easier to read. A number of these conditions spanned multiple lines. Most of these where actually combined with not and used as guard clauses to exit functions early (either with return or raise).

if not (a and a.b == val):  # not (a?.b == val)
    ...

if not (a and a.lower()):  # not a?.lower()
    ...

x = a.b if a is not None else None  # x = a?.b

if key in d and d[key].do_something():  # d.get(key)?.do_something()
    ...

if a.b and key in a.b and a.b[key]:  # a.b?.get(key)
    ...

if a.b and a.b[0].c and a.b[0].c.d and a.b[0].c.d[0].e:  # a.b?[0].c?.d?[0].e
    ...

if key in d and d[key][other]:  # d.get(key)?[other]
    ...

if (b := a.get(key)) and b.get(other) == 2:  # a.get(key)?.get(other) == 2
    ...

if (b := a.get(key)) and b.strip().lower():  # a.get(key)?.strip().lower()
    ...

if a and a.b and a.b.c:  # a?.b?.c
    ...

if (c := a.b) and c.startswith(key):  # a.b?.startswith(key)
    ...

if d and key in d and d[key]:  # d?.get(key)
    ...
7 Likes

As long as the attributes have different names, it should be obvious. Otherwise you might need to do a bit more debugging, but that’s nothing unusual.

Yeah, there’s a guideline known as the “law of Demeter” that says you should avoid using long chains of attribute accesses like this, because it scatters knowledge about the precise topology of your data structures all over your code, making modifications difficult. So the existence of something like could be seen as supporting an anti-pattern.

1 Like
If a is not None and a.b.c is not None:
    x = a.b.c.d
else
    x = None

I don’t see value in turning easy to read logic into something resembling a regular expression.

3 Likes

I would actually prefer to have all of these spelled out rather than mentally desugaring them. I found it harder to compare them with their more compact syntax alternatives.

This trades readability for writability, since the syntactic sugar does not reduce cognitive load: the reader still mentally expands it into its explicit form.

It feels like a form of compression that cannot be meaningfully operated on, e.g., it leaves no obvious place to insert metrics or debugging logic.

7 Likes