But there are already short-circuit operators in Python which do not obey this property. Feel free to read the lengthy discussion above if you haven’t already, but one point brought up was that breaking up
e = (a and b) and c
into
t1 = a and b
e = t1 and c
Has different semantics - resulting in a.__bool__() being called twice in the second one, but only once in the first (depending on the Python version). The same is true for or.
This gives precedent for making ?. a short-circuit operator - you already can’t break down some Python expressions accurately because of their short-circuiting nature - so adding another one, especially when the short-circuiting is a major use-case, seems reasonable to do.
I still feel that what’s happening for and/or is an optimization or a quality-of-implementation issue, as opposed to the semantics being proposed for ‘a?.b.c’ and ‘(a?.b).c’ . The ‘?’ semantics does not depend on a normally read-only operation being able to log a side-effect: It is the difference between raising an AttributeError and returning None.
I’m not convinced (yet) that using such a different AST for None-aware access operators makes sense. Yes, it would simplify the short-circuiting, at what cost though?
In addition to the NoneAwareAttribute and NoneAwareSubscript nodes, we’d likely need to add at least three more AttributeTail, SubscriptTail and CallTail. If I didn’t miss something, the expression a.b?.c[0].func() would then be parsed roughly like this:
Reusing parts of the Attribute, Subscript and Call nodes might be challenging as one is left-recursive whereas the *Tail variants are right-recursive. This will not only affect CPython itself for the bytecode generation where we might need to duplicate or refactor code but also every other consumer of the AST. It might also be at least a bit surprising that a normal attribute access (.) can be parsed as two separate nodes depending on the context.
What I like about just adding primary '?' '.' NAME and primary '?' '[' slices ']' to the primary rule is that it shows these can be used almost interchangeably with the other access operators. Yes, consumers will need to adjust their Attribute, Subscript and Call node handling to add support for short-circuiting, but IMO that’s a fairly minor ask compared to the alternative mentioned above.
–
In comparison, adding a group attribute to all expression nodes is a bit tedious but it does make a surprising amount of sense actually. In a way it’s similar to the other location attributes. It also helps that a group will only ever have exactly one topmost expression node.
Being amenable to decomposition is a desirable quality, but not a universal or guaranteed one. Your example is wrong - it draws on a bug in a specific CPython version - but none the less I will concede the point that this is certainly possible to compromise on decomposeability, if something sufficiently valuable is gained in return.
I’m not sure that it is. ?? and ??= are straightforward, normal binary operators. ?. is subtle and hard to explain.
Good point. The implicit chaining of comparisons is a bit different, as the middle expression is duplicated.
Once you’ve expanded -7 < -6 < -5 to (-7 < -6) and (-6 < -5) it is much clearer what is going on and parentheses can be added, or removed, without changing the meaning.
Is there an expansion of chained ?. and . operators that is robust to having parentheses added or removed? If there were it would make the semantics a lot clearer and easier to reason about.
The transformation I keep in mind when thinking about ?. is this
base?.tail
base.tail if (base is not None) else None
In practice the lookup of base is cached so you’ll need to add a temporary variable but that can make it difficult to see what’s actually going on. Especially for more complex cases.
(_t.tail) if ((_t := base) is not None) else None
While base can be replaced with any number of expressions, including groups, tail is limited to attribute access, subscript, their none-aware variants and calls. That’s due to the left-recursive grammar in the primary rule and similar to base.tail.
Another discussion point the last few days was if the short-circuiting behavior should be removed. I’ve added a new section Short circuiting is difficult to understand to my draft to (a) capture this argument and (b) point out why I think the short-circuiting behavior as a whole should not be removed from the draft.
To reiterate the arguments here
A lot of the recent discussion has centered around corner cases in the short-circuiting behavior. They are important to get right, for sure (and I believe we have done that now) but seeing all these posts here can make the behavior seem more complicated than it actually is in practice.
Say I don’t know anything about ?. and ?[ ], all that’s really needed to know is that if the LHS / base subexpression evaluates to None the RHS / tail will be skipped and the result will be set to None. If it is any other value, it will just do a “normal” attribute access or subscript lookup. In the majority of cases this should be more than enough information to understand how the operators will work. See also the How to Teach This section in the draft.
On a technical level, removing short-circuiting means each subsequent attribute access or subscript would need to be changed to their None-aware variants. Similar to the maybe a.b proposal, this will hide potential error cases if a subsequent not-optional attribute suddenly starts to return None as well. Having short-circuiting allows the developer to be more explicit. More details can be found in the Remove short-circuiting section in rejected ideas.
The short-circuiting behavior as it’s implemented specified right now is identical to that of other major languages like JS, TS and C#.