Revisiting PEP 505

It wouldn’t be, but I think your use of the term “valid” is a bit misleading. No one is saying this is to work around invalid code. The syntax proposal is to deal with data structures where an attribute is defined to be optional. So in your hypothetical, the type checkers would require that you use getattr() or hasattr() as appropriate to avoid raising an AttributeError. With the proposed syntax the type checkers would still do the same checks, but the suggestion is the overall written code would be more succinct and easier to follow. So both scenarios lead to the same outcome, just with different approaches to the coding.

For me, it’s more about making scripting easier than correctness of code as the proposed syntax doesn’t enable some new coding abilities that Python didn’t have before. And for long-lasting production code, being more verbose in this specific instance isn’t the end of the world.

Hmm, don’t we need to tell them first that the attribute might be missing?

class Foo:
    def __init__(self, bar: int = None):
        if bar is not None:
            self.bar: NotRequired[int] = bar

foo = Foo()
foo.bar # Error
foo.bar? # OK

But there might be some problems with arbitrary attributes: Typing: `NotRequired`-like for potentially-absent class attributes?.

1 Like

Good example (even if NotRequired is only for TypedDicts, but it’s not important). Now I catch there could be attributes defined conditionally, so they can be present or not. I tried your code in my IDE (PyCharm) and actually its static type checker does not emit a warning if I try to access bar. So with this kind of classes, the Guido’s proposal is useful.

My question is: is this pattern commonly used? Guido posted a real world example in TypeScript, but not in Python.

Well, yes, you’re right, it’s only a syntactic sugar, but it’s quite handy. NULL checks are frequent when you code.

1 Like

I was able to track down the source of several dozen ? in a web application. It was used to protect against a single possible missing HTML element. While that’s quite rare, if the element was missing, the entire call tree would execute, short-circuiting at every ?.

The use of optional fields is not rare in Python. This could lead to entire call trees short-circuiting on ? quite often, unless proper guards were implemented.

1 Like

The postfix notation seems quite confusing to me (especially when switching between JS and Python).
I think event?["timeRange"]?["startTime"] is good.
And yes, it has to convert AttributeErrors and KeyErrors to None for it to be fully useful.
I’m also eagerly waiting for this feature, it’s tiresome having to put guards around most key accesses from a json api.

What I can tell about the opinions of people regarding this PEP from what I read :

  • ? is a meaningful character for “safe” operations.
  • Let refer ??, ?., ?[...] as “safe or”, “safe access”, “safe indexation”, these would be useful but are cryptic, and quantitative usage of them would decrease readability of the code.

One possibility could be to introduce a “family of safe operators” with this kind of explicit names :
?or, ?dot, ?at, …

So that one can write :

maybe_some_value ?or some_value  # safe or 
maybe_object ?dot maybe_attribute ?dot attribute  # safe access
some_list ?at maybe_index  # safe indexation

Also possibly more operators :

some_dict ?get maybe_key  # safe dictionary access
?exist "maybe_some_object"  # safe object reference

This may not be as concise as the OP syntax but might be more explicit and readable.

Also, from my usage cases, I use None checks mainly within class constructors.
One usage I frequently require is about mutually exclusive arguments for contructors, so maybe some ?mutex (following the “family of safe operators” syntax just above) function can be pertinent :

class Person:
    def __init__(self, name, age=None, birth_year=None):
        self.name = name
        match ?mutex [age, birth_year]:  # asserting exactly one list element is not None
            case 0 : self.age = age
            case 1 : self.age = current_year - birth_year

I’m a bit late to the party, but I haven’t seen the following proposed:

The things that I seemingly type over and over again are “if a is None:”/ “if a is not None:” and “a if a is not None else b”. The latter is covered by the proposed ?? operator, but I agree with others that it feels a bit un-pythonic. A keyword like otherwise would be better, imo.

As for the former, why not let a? mean a is not None? Then you could write

if reader?:
    reader.feed_data(data)

for example.

Or (example from the PEP)

mangle_from_ = policy.mangle_from_ if policy? else True

meaning

mangle_from_ = True if policy is None else policy.mangle_from_

I think this is very easy to teach and a real quality-of-life improvement.

I’d also advocate writing the type Optional[int] as int? in type annotations. You can think of it as pattern matching: if you have a variable a of type int?, then checking with if a?: means we can narrow a to the type int within the if.

EDIT: removed example that was perhaps confusing

2 Likes

Sorry, this is the opposite of obvious or clear to me.

7 Likes

Isn’t it just equivalent to

if not a:

except that it will do the right thing with False and any other falsy values?

EDIT: Well, anyway, you can still write a is None if you think not a? is confusing.

You can write int | None.

Yes, and you can also write a if a is not None else b instead of a ?? b, but I thought we were looking for shorthands for very common code patterns?

Well, sometype | None is quite short. I’m not saying I don’t like sometype?. On the contrary, I find it useful and simple. But are you sure we want yet another way to express Optional[sometype]?

1 Like

An idea :bulb: (not the solution, something i hope inspiring, a bit mind-twisting though)

Think about the “neutral elements” in mathematics (you add 0, or multiply by 1, you change nothing. Also you multiply by zero you get zero… I am not going to talk about associativity, commutativity, etc… right now).

I can say the requirement here is to have access to methods able to manage the “Noneness” of different objects in a concise way, but the difficulty is to make it not cryptic. Thus the idea here (I am trying to say it correctly) is to “infer” non-trivial “Noneness” properties into a “neutral” element.
→ Basically I’ve tried to encase Noneness properties management into operations involving one “neutral-safe” element.
Probably the code below will be clearer than any further explanation (elmt stands for ‘element’) :

# -*- coding: utf-8 -*-

class SafeElement:
    def __init__(self, elmt=None):
        self._elmt = elmt
    
    def __call__(self, elmt=None):
        if elmt is None:
            return self._elmt
        else:
            return SafeElement(elmt)
    
    def __bool__(self):
        return False if self._elmt is None else True
    
    def __or__(self, elmt):
        return self if self else SafeElement(elmt)

    __add__ = __or__
        
    def __and__(self, elmt):
        elmt = SafeElement(elmt)
        return SafeElement() if not self or not elmt else SafeElement(True)

    __mul__ = __and__
    
    def _mutex(self, elmt):
        if self and (elmt is not None):
            raise MutexError(self(), elmt)
        else:
            return self + elmt

    __mod__ = _mutex
    
    # __pow__ = _mutex  # not used because __pow__ is right-associative
    
    def __rpow__(self, other):
        return self._mutex(other)

    def __at__(self, idx):
        if type(self()) is list:
            if type(idx) is int:
                if idx < len(self()):
                    return SafeElement(self()[idx])
        elif type(self()) is dict:
            if type(idx) is str:
                if idx in self().keys():
                    return SafeElement(self()[idx])
        return SafeElement()
    
    __matmul__ = __at__
    
    def __repr__(self):
        return "SafeElement : " + self().__repr__()

class MutexError(Exception):pass

# =============================================================================
# The "neutral-safe" element
# =============================================================================

Ω = SafeElement()

# =============================================================================
# Usage
# =============================================================================

# Basic checks for None
bool(Ω)         # False
bool(Ω(1))      # True
bool(Ω(0))      # True
bool(Ω(None))   # False

# Something tricky
Ω       # SafeElement : None
Ω()     # None
Ω(1)    # SafeElement : 1
Ω(1)()  # 1

# Safe "OR" operator
(Ω + 1 + 2)         # SafeElement : 1
(Ω + None + 2)      # SafeElement : 2

(Ω + 1 + 2)()       # 1
(Ω + None + 2)()    # 2

# Safe indexation
oak = {'tree':{'branch':{'leaf':'chlorophyll'}}}
(Ω(oak)@'tree'@'branch'@'leaf')()               # 'chlorophyll'
(Ω(oak)@'tree'@'branch'@'door'@'corridor')()    # None

# Safe mutex
Ω % None % 2                        # SafeElement : 2
Ω % None % 2 % None % None          # SafeElement : 2
(Ω % None % 2 % None % None)()      # 2
Ω % 1 % 'This raises a MutexError'  # raise MutexError

I chose the Ω unicode character to instanciate one “neutral-safe” element. Once the element is created, several operations can propagate the Noneness properties and eventually you obtain the ‘safe access’, ‘safe or’, ‘safe indexing’, and ‘safe mutex’ operations, just by introducing one element with propagating properties.
One main drawback of the implementation here above is to require to __call__ the SafeElement instance to get the element back.

1 Like

I’m a -1 for this for three reasons:

  1. It makes Python more hieroglyphic like, something I dislike in other languages. I like the joke that you turn your pseudo code into a running program by changing the file type from txt to py!
  2. The main use cases are parsing external files like JSON. There already exists better solutions, e.g. GitHub - lovasoa/marshmallow_dataclass: Automatic generation of marshmallow schemas from dataclasses., GitHub - rnag/dataclass-wizard: Simple, elegant, wizarding tools for interacting with Python's dataclasses., GitHub - lidatong/dataclasses-json: Easily serialize Data Classes to and from JSON, GitHub - konradhalas/dacite: Simple creation of data classes from dictionaries., Welcome to Pydantic - Pydantic, and https://pandas.pydata.org/. Probably a ton more I don’t know about also. One of these could be added to python if necessary to be in the language.
  3. It would be better to revise PEP 712 – Adding a “converter” parameter to dataclasses.field | peps.python.org. I think other rejected PEPs have already been suggested also.

Sorry -1.

I’ve also posted this on the original thread because I can’t decide which one is current - sorry for double post.

I’m a -1 for this for three reasons:

  1. It makes Python more hieroglyphic like, something I dislike in other languages. I like the joke that you turn your pseudo code into a running program by changing the file type from txt to py!
  2. The main use cases are parsing external files like JSON. There already exists better solutions, e.g. GitHub - lovasoa/marshmallow_dataclass: Automatic generation of marshmallow schemas from dataclasses., GitHub - rnag/dataclass-wizard: Simple, elegant, wizarding tools for interacting with Python’s dataclasses., GitHub - lidatong/dataclasses-json: Easily serialize Data Classes to and from JSON, GitHub - konradhalas/dacite: Simple creation of data classes from dictionaries., Welcome to Pydantic - Pydantic, and https://pandas.pydata.org/. Probably a ton more I don’t know about also. One of these could be added to python if necessary to be in the language.
  3. It would be better to revise PEP 712 – Adding a “converter” parameter to dataclasses.field | peps.python.org. I think other rejected PEPs have already been suggested also.

Sorry -1.

If I’m reading this correctly, all of these are variations on ONE alternative solution, which is “validate everything first”. This is not a solution in all situations, and it’s not really a fair representation to give six links to variations of the same thing.

I have yet to see any good solution for the variation where you don’t want to predefine every single detail of the entire JSON tree - which is an extremely common use-case for me. I have used these kinds of operators frequently in multiple other languages.

Practicality beats purity.

5 Likes

It definitely belongs on the other thread. This one’s a meta conversation about how to break the original conversation out of its loop of new people reposting old points without realising because they didn’t want to read the 100s of already existing posts (which, I want to say in the gentlest/most unaccusatory way, does include your post I’m afraid :grimacing:).

4 Likes

I would push back on ?? and ??=, mostly because I think

def f(a: str, b: list[str] ?= [a]):
  ...

is superior to

def f(a : str , b: list[str] | None = None):
  b ??= [a]
  ...

I haven’t added to the original discussion because it’s long enough already, and someone else has already made this point.

Trying to stick to the topic of the OP: I can’t see a good way out of this circle. You can try to count hearts, but placing hearts in the middle of a 150-post thread feels pointless. You can try to use polls, but when I tried that in a thread of my own it didn’t particularly clarify the discussion. And you can reply to me and explain that function defaults aren’t the only use-case for ??=, but I’m sure someone in the other thread already did, so that just perpetuates the circle.

2 Likes

I add that if PEP 671 – Syntax for late-bound function argument defaults passes, my opposition to ?? evaporates.