Re: PEP 505 (None-aware operators) for Python 3.11

Philipp Burch <[email protected]> Wed, 21 Sep 2022 14:33:50 +0200
Newsgroups gmane.comp.python.devel
Message-ID <[email protected]>
Hi Steven!

On 21.09.22 13:17, Steven D'Aprano wrote:
> The distinction you make between user-defined and non-user-defined
> classes doesn't hold water. If you allow that (say) `int|None` **might**
> be acceptable, then why would `Integer|None` **necessarily** be lazy and
> bad just because int is written in C and built into the intepreter while
> Integer is written in Python by a user?
> 
> The laziness/badness of `SomeType|None` must depend on the semantics of
> SomeType, not the implementation language or who wrote it.
> 
> "Our API was lazy and bad because it uses None, but then it got accepted
> into Python as a builtin, so now it is no longer lazy or bad." /s

I considered a while if "user-defined types/classes" is the right 
wording, but didn't find a better alternative. The point I'm making is 
that `X|None` could especially then hide bugs if X defines attributes 
that will often be accessed in a chain. For int or float, there are only 
a few methods, which are arguably rarely used and chaining is probably 
not used at all. Plus, providing nulltypes for primitives would feel 
very awkward.

Consider

```
x: int | None = 5
# ...
if x is not None:
     y = 1 << x.bit_cont()
```

The typo with bit_cont() is hidden whenever x is None, so it might be 
missed by unit tests with limited coverage. However, using a nullable 
variant of it would probably make it look like this:

```
class Int(int):
     # ...

class NoneInt(Int):
     # ...

x = Int(5)   # With the option for NoneInt()
# ...
x_bit_count = x.bit_cont()
if x_bit_count == NoneInt:
     y = 1 << x_bit_count
```

This is certainly not pretty for something basic as an int, plus there 
will most likely be a performance hit when replacing every int by Int(), 
not to mention compatibility issues with APIs/libraries expecting plain int.

On the other hand, more complex classes like in the mentioned publisher 
API could indeed profit from such null-types, since a chain like

```
book?.publisher?.owner?.namme
```

will hide the typo with namme whenever any of book, publisher or owner 
is None, so there is much more potential here to miss it in a unit test.

This is actually the distinction that I wanted to make, not in the 
technical sense if a class is defined in the standard library or not.

>> The problem is, that I never actually thought about his suggested way.
> 
> Some people have, and found that it doesn't always simplify the API that
> much, or at all. If you end up replacing `obj is None` with `obj == NULL`
> or `obj.isnull()` then you haven't really gained much.

Certainly. But without None-aware operators, the gain in readability can 
be large, if you can just write

```
name = book.publisher.owner.name
if name is not None:   # No NoneStr here, see above
     # ...
```

versus

```
if (book is not None) and (book.publisher is not None) and ...:
     # ...
```

Just to be clear: I've been thankful for the comment and explanation of 
Steve Dower, because I've never thought about it that way and now see 
constructs in my code, where such a concept really would have simplified 
things and improved robustness. For others to profit as well, I would be 
glad to see this mentioned in the docs, so that it can be found even 
when not actually looking for it.
But I certainly don't want to say that this concept must be followed 
everywhere or that "X|None" is necessarily a bad thing. After all, I'd 
still love to see Python supporting such None-aware operators.

Best regards,
Philipp