[Python-ideas] PEP 505: None-aware operators

Steve Dower steve.dower at python.org
Thu Jul 19 17:28:47 EDT 2018


Thanks everyone for the feedback and discussion so far. I want to 
address some of the themes, so apologies for not quoting individuals and 
for doing this in one post instead of twenty.

------


* "It looks like line noise"

Thanks for the feedback. There's nothing constructive for me to take 
from this.

* "I've never needed this"

Also not very actionable, but as background I'll say that this was 
exactly my argument against adding them to C#. But my coding style has 
adapted to suit (for example, I'm more likely to use "null" as a default 
value and have a single function implementation than two 
mostly-duplicated overloads).

* "It makes it more complex"
* "It's harder to follow the flow"

Depends on your measure of complexity. For me, I prioritise "area under 
the indentation" as my preferred complexity metric (more lines*indents 
== more complex), as well as left-to-right reading of each line (more 
random access == more complex). By these measures, ?. significantly 
reduces the complexity over any of the current or future alternatives::

def f(a=None):
     name = 'default'
     if a is not None:
         user = a.get_user()
         if user is not None:
             name = user.name
     print(name)

def f(a=None):
     if a is not None:
         user = a.get_user()
         name = user.name if user is not None else 'default'
         print(name)
     else
         print('default')

def f(a=None):
     user = a.get_user() if a is not None else None
     name = user.name if user is not None else 'default'
     print(name)

def f(a=None):
     print(user.name
           if (user := a.get_user() if a is not None else None) is not None
           else 'default')

def f(a=None):
     print(a?.get_user()?.name ?? 'none')

* "We have 'or', we don't need '??'"

Nearly-agreed, but I think the tighter binding on ?? makes it more 
valuable and tighter test make it valuable in place of 'or'.

For example, compare:

     a ** b() or 2 # actual:   (a ** b()) or 2
     a ** b() ?? 2 # proposed:  a ** (b() ?? 2)

In the first, the presence of 'or' implies that either b() or __pow__(a, 
b()) could return a non-True value. This is correct (it could return 0 
if a == 0). And the current precedence results in the result of __pow__ 
being used for the check.

In the second one, the presence of the '??' implies that either b() or 
__pow__(a, b()) could return None. The latter should never happen, and 
so the choices are to make the built-in types propagate Nones when 
passed None (uhh... no) or to make '??' bind to the closer part of the 
expression.

(If you don't think it's likely enough that a function could return 
[float, None], then assume 'a ** b?.c ?? 2' instead.)

* "We could have '||', we don't need '??'"

Perhaps, though this is basically just choosing the bikeshed colour. In 
the absence of a stronger argument, matching existing languages 
equivalent operators instead of operators that do different things in 
those languages should win.

* "We could have 'else', we don't need '??'"

This is the "a else 'default'" rather than "a ?? 'default'" proposal, 
which I do like the look of, but I think it will simultaneously mess 
with operator precedence and also force me to search for the 'if' that 
we actually need to be comparing "(a else 'default')" vs. "a ?? 'default'"::

     x = a if b else c else d
     x = a if (b else c) else d
     x = a if b else (c else d)

* "It's not clear whether it's 'is not None' or 'hasattr' checks"

I'm totally sympathetic to this. Ultimately, like everything else, this 
is a concept that has to be taught/learned rather than known intrinsically.

The main reasons for not having 'a?.b' be directly equivalent to 
getattr(a, 'b', ???) is that you lose the easy ability to find typos, 
and we also already have the getattr() approach.

(Aside: in this context, why should the result be 'None' if an attribute 
is missing? For None, the None value propagates (getattr(a, 'b', a)), 
while for falsies you could argue the same thing applies. But for a 
silently handled AttributeError? You still have to make the case that 
None is special here, just special as a return value vs. special as a test.)

* "The semantics of this example changed from getattr() with ?."

Yes, this was a poor example. On re-reading, all of the checks are 
indeed looking for optional attributes, rather than looking them up on 
optional targets. I'll find a better one (I've certainly seen and/or 
written code like this that was intended to avoid crashing on None, but 
I stopped my search of the stdlib too soon after finding this example).

* "Bitwise operators"

Uh... yeah. Have fun over there :)

* "Assumes the only falsie ever returned [in some context] is None"

I argue that it assumes the only falsie you want to replace with a 
different value is None. In many cases, I'd expect the None to be 
replaced with a falsie of the intended type:

     x = maybe_get_int() ?? 0
     y = maybe_get_list() ?? []

Particularly for the second case, if you are about to mutate the list, 
then it could be very important that you don't replace the provided 
reference with your own, just because it happens to be empty right now 
(this is the logging example in the PEP). Supporting a default value in 
the get() calls is obviously a better way to do this though, provided 
you have the ability to modify the API.

But for me, the ?? operator makes its strongest case when you are 
getting attributes from potentially None values:

     x = maybe_get()?.get_int() ?? 0
     y = maybe_get()?.get_list() ?? []

In this case, using 'or' may replace an intentionally falsie value with 
your own, while using a default parameter is still going to leave you 
with None if the first maybe_get() returned nothing.

"?." without "??" feels like a trap, while "??" without "?." feels 
largely unnecessary. But both together lets you turn many lines of code 
into a much shorter snippet that reads left-to-right and lets you assume 
success until you reach the "??". That, for me, is peak readability.



More information about the Python-ideas mailing list