I have not read the entire thread, but coming from typescript, I really - like really - miss the ‘?’ operator. I see the initial proposal is from 2015, so - high time to reconsider this
I would expect it to behave like just like in typescirpt x = a.b?.c or 12 if b is None or c is None then the vlue is assing 12
This is already brief enough for the occasional real world cases, and even way more flexible. Which makes me question the need for extra (ugly) special operators (?., ??, …) for a very limited use case.
And Python already has chosen ‘nice’ no-new-symbol expression forms for other situations - list/generator comprehensions, ternary ... if ... else ...), global/nonlocal, super() , …
So alternatively, more universal mechanisms could be considered, like:
# just drop the lambda above. A magic pseudo function (like super())
x = try(obj.a[2].b, nan) # default on Attribute/Key/IndexError
x = try obj.a[2].b except nan
Or, extending the conditional expression:
x = (obj.a[2].b except None) # default on Attribute/Key/IndexError
x = (float(y) + z if COND else DEFAULT except(ValueError, TypeError) nan)
x = (obj.a[2].b if >= 0 else DEFAULT except nan) # if <comp-op> vs. `??`
This would cover way more than OC use cases and exposes typo prone code more salient.
See also: PEP 463. Ruby rescue (the only nice one from other languages I found)
Not sure if it’s been mentioned here, but a core use case that drives interest in this for me and my team is type-safe access to deeply nested protobuf message. As you may know, if you opt in to explicit field presence in protobuf with the optional keyword, you get Optional fields in the generated bindings.
That means that we have access patterns where a message is received, and the user cares about a specific field of a sub-message, and they have to write something like this:
def handle_message(message: Message):
foo = message.foo
if foo is None:
return None
bar = foo.bar
if bar is None:
return None
baz = bar.baz
if baz is None:
return None
use_bar(baz)
or, more esoterically
baz = (o := message.foo) and (o := o.bar) and o.baz
Following up on my answer, I think a notable consequence of any implementation like proxies or lambdas with exception handling necessarily includes the expression a.b.c somewhere in your code, which means one of two things:
Your type-checker is unhappy with you
You have to disable rules in your type-checker
Therefore, if you want type-checking, the language must provide an expression type that is meaningfully different which would signal to type-checkers “hey, this expression isn’t really going to be evaluated blindly”. That’s if you want to use lambdas or some mechanism that guards the final expression with an except clause. If you go the .? route, the type-checking problem is solved.
This PEP has been stuck in circles for AGES and even just that fact has been mentioned many times. For this reason, sorry if the following points have been brought up before.
For this endless cycle to end, what could help is to divide this PEP into 2 parts:
The first PEP is purely for None coalescing. (.? and optionally ?? and ??=)
Afterwards we can make new proposals for dunder methods or other ways to make this behaviour more customizable, not just work for None.
I feel like otherwise we will be forever stuck in this loop of people coming up with ideas that are either in the group 1 or group 2 and no consensus being found.
Inspired by C# and Python’s dict, for the first PEP I would additionally propose adding a safe get method to list, tuple (and possibly other sequences) to support lookup with a default, instead of having to handle IndexErrors.
This would perfectly allow coalescing nested structures, without having to special case any LookupErrors.
Split PEP into 2 parts, the first being just None support, the second one being for additional customizability.
In part 1, additionally add safe get to list, tuple (…) to allow easy None coalescing.
One small addition:
I think we could just add it to the Sequence abstract base class in general with the default pseudo implementation I described above. This should not break any backwards compatibility.
Also, if people don’t like get as a name, another idea would be the name at.
No. The interfaces defined by Sequence and co inside of collections.abc are effectively frozen. We can add new classes, but we can’t modify existing classes because implementing the interface does not require implementers to actually inherit from the class. See the big comment in _collections_abc talking about this issue.
With .get then it could just be dict_or_list.get(i).
Thinking types/ABCs first in Python is going to lead you to poor implementation decisions (as Cornelius already said, but allow me to go a bit deeper). All typing is duck-typing/structural-typing, and the types and ABCs are there as scaffolding, but are never required. So additions or changes to typing types or ABCs can only break legitimate uses, because they don’t automatically get the addition/change. It’s just not how Python works or is structured.[1]
And if you really want it to be, choose literally any other language. I like that Python is actually different here - there are more than enough languages that force you to code under remote control, and it’s so powerful that Python doesn’t restrict you like that. ↩︎
In my opinion the ?. operator would be very helpful! In document data models with a lot of optional values it can get very verbose in Python to navigate the properties nowadays, especially if you want code that is typed correctly.
If it helps, I’m now in favor of the stripped-down version, with only the ??, ??=, and ?. operators, and without magic for missing attributes/keys – just return None if the value is None rather than attempting a further getattr operation on None.
It would have simplified code I’ve written over the past year where there are quite a few classes (mine as well as 3rd party) with attributes whose type is Something | None. And ??= would have helped with arguments whose default is None.
If we strip this pep down to only instance attributes (?.), I feel we might miss a big opportunity for improvement: handling deeply nested data structures (like JSON responses).
I would like to see the scope include None-aware sub-scripting, allowing us to express this perhaps:
value = dct1?["y"]?[2]?["a"]?[0]
Instead of this typical helper-function overhead:
value = deep_get(dct1, ["y", 2, "a", 0])
Excluding ?[ ] leaves a significant gap in usability for data-heavy apps imho.
I would also be even happier if the pep went even further and supported None-aware assignment (deep set) functionality, i.e.
dct1?["y"]?[2]?["a"]?[0] = 0
But if this is is too complex for now, I’d definitely urge the introduction of None-aware sub-scripting to modernize Python’s data manipulation capabilities.
This could be expressed using only ?. although admittedly a bit more verbose.
value = dct1?.get("y")?.__getitem__(2)?.get("a")?.__getitem__(0)
Adding list/tuple.get(index, default=None) like suggested above might help here.
–
AFAIR the readability concerns especially with the none-aware subscript operator were one of the main reasons PEP 505 stalled in the first place. So I’d recommend against including it in the first step. [1]
It can always be proposed and evaluated separately at a later point once the community has some more experience with none-aware operators.↩︎
Let’s not go there. This was a very contentious issue in earlier discussions and caused the PEP to fail. Please don’t repeat that dead end in the discussion.
(The technical reason is that in order to implement it you’d have to catch exceptions, and there were wide-ranging concerns about that.)
Again, please withdraw the suggestion (or propose it in a follow-up PEP).
I believe that the ?. operator together with list.get()/tuple.get()(and, obviously, the existing dict.get()) would, to much extent, alleviate the pain of the lack of []?.
But who is going to index a list or tuple with an index they’re not sure of? I don’t think that’s nearly as common as accessing an attribute that might be None. And for JSON the most likely case is a dict with an optional key.
Let’s move list/tuple .get() to a follow-up PEP. I think it’s key here not to over-ask.
The Sequence.get(idx, default)/getitem(key, default) idea was born from one of the exception catching tangents (since it assumes a non-None container base) so it would definitely be out of scope of any narrowed proposal that focused on simplified handling of attributes that are set to None.