86

In JavaScript, if I'm not sure whether every element of the chain exists/is not undefined, I can do foo?.bar, and if bar does not exist on foo, the interpreter will silently short circuit it and not throw an error.

Is there anything similar in Python? For now, I've been doing it like this:

if foo and foo.bar and foo.bar.baz:
    # do something

My intuition tells me that this isn't the best way to check whether every element of the chain exists. Is there a more elegant/Pythonic way to do this?

Flimm
  • 136,138
  • 45
  • 251
  • 267
Xeoth
  • 1,285
  • 1
  • 11
  • 22
  • 12
    What you want is PEP 505 https://www.python.org/dev/peps/pep-0505 – brandonscript Apr 21 '21 at 15:17
  • That's exactly what I'm looking for but the maybe-dot and maybe-subscript operators haven't been added to Python (yet?) – Xeoth Apr 22 '21 at 16:09
  • Maybe? I'm not certain because I'm not privileged enough to use Py 3.8 yet, but it appears to have been ratified? – brandonscript Apr 23 '21 at 00:12
  • 2
    It's not in Python 3.8. PEP 505 is marked as Deferred which means no progress is being made. See discussion at https://discuss.python.org/t/pep-505-status/4612. – John Mellor Jun 28 '21 at 22:19
  • Are you asking for Python objects or Python dictionaries? – Flimm Feb 15 '23 at 11:49
  • I'm pretty sure it was objects, not dicts. That said, neither of them support optional chaining, so the question is open to both. – Xeoth Apr 17 '23 at 17:06

11 Answers11

30

If it's a dictionary you can use get(keyname, value)

{'foo': {'bar': 'baz'}}.get('foo', {}).get('bar')
Aliaksandr Sushkevich
  • 11,550
  • 7
  • 37
  • 44
23

You can use getattr:

getattr(getattr(foo, 'bar', None), 'baz', None)
theEpsilon
  • 1,800
  • 17
  • 30
  • 10
    This is't more readable than what OP has `if foo and foo.bar and foo.bar.baz:` – Kuldeep Jain Mar 30 '22 at 23:51
  • @KuldeepJain While it may not be more readable, `if foo and foo.bar and foo.bar.baz:` does not work because it will raise an `AttributeError`. This answer at least works like optional chaining in that it won't raise an exception and you'll get `None` back if a part of the chain is missing. – Andria Jun 01 '23 at 19:42
  • Yeah, makes sense in case `bar` or `baz` is missing it will. Thanks for clarifying this. – Kuldeep Jain Jun 02 '23 at 18:37
18

Most pythonic way is:

try:
    # do something
    ...
except (NameError, AttributeError) as e:
    # do something else
    ...
theEpsilon
  • 1,800
  • 17
  • 30
soumya-kole
  • 1,111
  • 7
  • 18
13

You can use the Glom.

from glom import glom

target = {'a': {'b': {'c': 'd'}}}
glom(target, 'a.b.c', default=None)  # returns 'd'

https://github.com/mahmoud/glom

11

I like modern languages like Kotlin which allow this:

foo?.bar?.baz

Recently I had fun trying to implement something similar in python: https://gist.github.com/karbachinsky/cc5164b77b09170edce7e67e57f1636c

Unfortunately, the question mark is not a valid symbol in attribute names in python, thus I used a similar mark from Unicode :)

6

Combining a few things I see here.

from functools import reduce


def optional_chain(obj, keys):
    try:
        return reduce(getattr, keys.split('.'), obj)
    except AttributeError:
        return None

optional_chain(foo, 'bar.baz')

Or instead extend getattr so you can also use it as a drop-in replacement for getattr

from functools import reduce


def rgetattr(obj, attr, *args):
    def _getattr(obj, attr):
        return getattr(obj, attr, *args)
    return reduce(_getattr, attr.split('.'), obj)

With rgetattr it can still raise an AttributeError if the path does not exist, and you can specify your own default instead of None.

Hielke Walinga
  • 2,677
  • 1
  • 17
  • 30
  • 1
    the `optional_chain` example has a couple bugs. the reduce should be returned, and `root` doesn't exist, it should be `obj` – sutherlandahoy Sep 02 '22 at 09:49
5

Combining some of the other answers into a function gives us something that's easily readable and something that can be used with objects and dictionaries.

def optional_chain(root, *keys):
    result = root
    for k in keys:
        if isinstance(result, dict):
            result = result.get(k, None)
        else:
            result = getattr(result, k, None)
        if result is None:
            break
    return result

Using this function you'd just add the keys/attributes after the first argument.

obj = {'a': {'b': {'c': {'d': 1}}}}
print(optional_chain(obj, 'a', 'b'), optional_chain(obj, 'a', 'z'))

Gives us:

{'c': {'d': 1}} None
Bernard Swart
  • 430
  • 6
  • 7
2

Classes can override __getattr__ to return a default value for missing attributes:

class Example:
    def __getattr__(self, attr): # only called when missing
        return None

Testing it:

>>> ex = Example()
>>> ex.attr = 1
>>> ex.attr
1
>>> ex.missing # evaluates to `None
>>>

However, this will not allow for chaining:

>>> ex.missing.missing
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'missing'

Nor will it deal with attempts to call methods that are absent:

>>> ex.impossible()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable

To fix this, we can make a proxy object:

class GetAnything:
    def __getattr__(self, attr):
        return self
    def __call__(self, *args, **kwargs): # also allow calls to work
        return self
    def __repr__(self):
        return '<Missing value>'

# Reassign the name to avoid making more instances
GetAnything = GetAnything()

And return that instead of None:

class Example:
    def __getattr__(self, attr): # only called when missing
        return GetAnything

Now it chains as desired:

>>> Example().missing_attribute.missing_method().whatever
<Missing value>
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • One might ask: "why not just inherit from the `GetAnything` class instead of defining another `__getattr__`?" If you do that, then something like `Example().missing_method().valid_attribute` *would unexpectedly succeed*, supposing that the `Example` instance has a `valid_attribute` but no `missing_method` - the `missing_method` proxy call would return the same `Example` instance, allowing the `valid_attribute` to be looked up. This is probably not what you want. Once a lookup has "failed", it makes sense to keep coming back to the same object - but not the first time. – Karl Knechtel Feb 10 '23 at 23:03
1

Python 3.10 introduced the match statement in PEP-634, with the tutorial in PEP-636 being a nice reference.

This statement allow these sorts of "chained" operations to be performed, but note that they are statements and not expressions.

For example, OP could instead do:

match foo:
    case object(bar=object(baz=baz)) if baz:
        # do something with baz

The reason for needing object is that everything is a subtype of it and hence it always succeeds. It then goes on to check that the attribute exists, which might fail. Exceptions wouldn't be thrown if the attribute didn't exist, just the case wouldn't match and it would move onto the next one (which in this case doesn't exist, so nothing would be done).

A more realistic example would check something more specific, e.g.:

from collections import namedtuple

Foo = namedtuple('Foo', ['bar'])
Bar = namedtuple('Bar', ['baz'])

def fn(x):
    match x:
        case Foo(bar=Bar(baz=baz)):
            return baz

print(fn(Foo(bar=Bar(baz='the value'))))
print(fn(None))
print(fn(1))

which would output:

the value
None
None

If instead you wanted to destructure into dictionaries, you might use something like:

foo = {'bar': {'baz': 'the value'}}

match foo:
    case {'bar': {'baz': baz}}:
        print(baz)
Sam Mason
  • 15,216
  • 1
  • 41
  • 60
0

Here's some syntactic sugar to make chaining with getattr look more like the fluent interfaces of other languages. It's definitely not "Pythonic", but it allows for something simpler to write.

The idea is to abuse the @ operator added in Python 3.5 (to support matrix multiplication in Numpy). We define a class r such that its instances, when matrix-multiplied on the right of another object, invoke getattr. (The combination @r, of course, is read "attr".)

class r:
    def __init__(self, name, value=None):
        self._name = name
        self._value = value
    def __rmatmul__(self, obj):
        return getattr(obj, self._name, self._value)

Now we can chain attribute accesses easily, without having to modify any other classes (and of course it works on built-in types):

>>> 'foo'@r('bar')@r('baz') # None
>>>

However, the order of operations is inconvenient with method calls:

>>> 'foo bar'@r('split')()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'r' object is not callable
>>> ('foo bar'@r('split'))()
['foo', 'bar']
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
0

I use reduce to achieve Javascript-like optional chaining in Python

from functools import reduce


data_dictionary = {
    'foo': {
        'bar': {
            'buzz': 'lightyear'
        },
        'baz': {
            'asd': 2023,
            'zxc': [
                {'patrick': 'star'},
                {'spongebob': 'squarepants'}
            ],
            'qwe': ['john', 'sarah']
        }
    },
    'hello': {
        'world': 'hello world',
    },
}


def optional_chaining_v1(dictionary={}, *property_list):
    def reduce_callback(current_result, current_dictionary):
        if current_result is None:
            return dictionary.get(current_dictionary)
        if type(current_result) != dict:
            return None
        return current_result.get(current_dictionary)
    return reduce(reduce_callback, property_list, None)


# or in one line
optional_chaining_v1 = lambda dictionary={}, *property_list: reduce(lambda current_result, current_dictionary: dictionary.get(current_dictionary) if current_result is None else None if type(current_result) != dict else current_result.get(current_dictionary), property_list, None)

# usage
optional_chaining_v1_result1 = optional_chaining_v1(data_dictionary, 'foo', 'bar', 'baz')
print('optional_chaining_v1_result1:', optional_chaining_v1_result1)
optional_chaining_v1_result2 = optional_chaining_v1(data_dictionary, 'foo', 'bar', 'buzz')
print('optional_chaining_v1_result2:', optional_chaining_v1_result2)

# optional_chaining_v1_result1: None
# optional_chaining_v1_result2: lightyear


def optional_chaining_v2(dictionary={}, list_of_property_string_separated_by_dot=''):
    property_list = list_of_property_string_separated_by_dot.split('.')

    def reduce_callback(current_result, current_dictionary):
        if current_result is None:
            return dictionary.get(current_dictionary)
        if type(current_result) != dict:
            return None
        return current_result.get(current_dictionary)
    return reduce(reduce_callback, property_list, None)


# or in one line
optional_chaining_v2 = lambda dictionary={}, list_of_property_string_separated_by_dot='': reduce(lambda current_result, current_dictionary: dictionary.get(current_dictionary) if current_result is None else None if type(current_result) != dict else current_result.get(current_dictionary), list_of_property_string_separated_by_dot.split('.'), None)

# usage
optional_chaining_v2_result1 = optional_chaining_v2(data_dictionary, 'foo.bar.baz')
print('optional_chaining_v2_result1:', optional_chaining_v2_result1)
optional_chaining_v2_result2 = optional_chaining_v2(data_dictionary, 'foo.bar.buzz')
print('optional_chaining_v2_result2:', optional_chaining_v2_result2)

# optional_chaining_v2_result1: None
# optional_chaining_v2_result2: lightyear