Questions about `?.` syntax

(split from PEP 505: status?)

These proposals look very interesting. As said above, ?. exists in many languages and it’s very useful.

The ability to short-circuit property access if it does not exist would be useful in many cases. For instance

def foo(logger: logging.Logger | None, **kwargs):
    # bunch of stuff
    if logger is not None:
        logger.info("a bunch of stuff was done")
    # some other stuff
    if logger is not None:
        logger.info("some other stuff was done")

Having these checks every time seems wasteful in space hindering readability.

I could go around it by creating a proxy object or a function, but that creates more surface for not much in my opinion.

On the other hand the following code

def foo(logger: logging.Logger | None, **kwargs):
    # bunch of stuff
    logger?.info("a bunch of stuff was done")
    # some other stuff
    logger?.info("some other stuff was done")

Is readable to anybody.

This is even more true for nested checks

class Bar:
    baz: Callable[[], str] | None

class Foo:
    bar: Bar | None


# current
foo: Foo | None = ...

value: str | None = None
if (
    foo is not None
    and foo.bar is not None
    and foo.bar.baz is not None
):
    value = foo.bar.baz()


# idea
value: str | None = foo?.bar?.baz?()

If None is not “special” enough, why not extend to any object as an “optional” attribute getter like so?

class Foo:
    foo: X

class Bar:
    bar: Y

value: Foo | Bar = ...
result: X | None = value?.foo

So we’d have some sort of __getoptattr__(self, name) that would return None if the attribute does not exist.
Has this been proposed before?

That looks like conditionally switching on types? Why not use polymorphism or a match statement?

Be it with if or match, I think we fall in the same issues I mention above

  • it can’t be chained
  • IMO it hinders readability when needed repeatedly

My last example was focusing on the “None is not special” argument, but my core idea is more about the first two examples before.

Basically I could write and use

def optional[T, U](x: X, f: Callable[[T], U], exc_cls: Type[Exception] = AttributeError) -> U:
    try:
        return f(x)
    except exc_cls:
        return None

class A:
    x: str

class B:
    y: A

class C:
    z: B


c: C = ...
result: str | None = optional(c, lambda c: c.z.y.x)

This does not look like a good solution to me, because the AttributeError could come from a completely other thing than the attribute I want actually missing (e.g. it could come from a property getter with a mistake in it’s implementation, in this case I would want an AttributeError to be raised).

Is it possible that you misunderstood the PEP 505 ?. operator? Or were you proposing something new? PEP 505 is about None-aware operators, and ?. checks if the attribute is none—not missing. Your missing attribute idea is like a rudimentary protocol check (just one attribute rather than a set), and a protocol is a rudimentary instance check (just checks attributes rather than true inheritance). So, it seems to be off topic for this thread, and should be in its own thread?

If you decide to start a new thread for your idea, my personal opinion is that you should rarely switch on types, but use polymorphism instead. (If you have to switch on types, isinstance and match are good ways to do it.) And since it’s so rare, I don’t see why it would be “needed repeatedly”. So, if you do start a thread, it might be useful to provide a real world example.

Thanks for your insight. I understand my above reply can lead to another idea which I can start in another thread.

Originally, I posted my reply in response to the argument against PEP 505 that “None is not that special”.

What interests me is more the behavior of ?. than underlying philosophical discussions, and I was trying to point that out. My own proposals then were merely to try to show that there are different ways to think about ?. which can result in the same desired behavior.

All in all, my point is that it seems to me that using ?. to chain attribute/method evaluation which can possibly short-circuit to an empty value is convenient and common enough in popular/mainstream languages that it could be considered in Python.

@NeilGirdhar how can I best continue this discussion?

Feel free to start a new thread or ask a moderator to clip your reply into a new thread for you :smile:

Alternative readable proposal:

class NullLogger:
    def info(self, *args):
        pass

def foo(logger: logging.Logger | NullLogger, **kwargs):
    # bunch of stuff
    logger.info("a bunch of stuff was done")
    # some other stuff
    logger.info("some other stuff was done")

Basically, when you want a no-op, pass in a real object that doesn’t do the operation. That puts the responsibility on the caller (of foo in this case) to decide to skip the operation, rather than creating a new facet of the public API to support that case.

(And yes, if you want strict type hints then the callee also has to “support” it, or at least know about your alternative type. One reason why I dislike typing, but if you derive your NullLogger from logging.Logger or just do a cast then you should be fine (there might even be a built-in NullLogger? I don’t remember…))

1 Like

Thanks for proposing this snippet. In my opinion it does not generalize well.

The discussion about PEP 505 is active here: Introducing a Safe Navigation Operator in Python - #5 by srittau, let’s consider my own post closed.