Explain Codes LogoExplain Codes Logo

Is there a Python equivalent of the C# null-coalescing operator?

python
null-coalescing
python-idioms
best-practices
Nikita BarsukovbyNikita Barsukov·Oct 28, 2024
TLDR

In Python, you can use the or operator as an equivalent to the C# ?? null-coalescing operator. The or operator will return the first truthy value (the value that evaluates to True), otherwise, it provides the second operand:

nothing_here = None plan_b = "secondary" outcome = nothing_here or plan_b # "secondary"

Note: Our trusty or operator, providing backup plans since... well, probably not that long ago, but who's counting, right?

Unfortunately, in Python, or considers any falsy value (0, False, [], "" etc.) as equivalent to None. To specifically check against None, leverage the := operator, also known as the walrus operator (cause it looks like a walrus, apparently):

outcome = plan_b if (nothing_here := plan_b) is not None else nothing_here

Warning: Feed the walrus wisely, they can be grumpy!

Alternative Null-Coalescing Methods

When you're navigating through the dictionary jungle, Python's .get() method is a handy machete. It tries to fetch a value from a dictionary, but if that key is as elusive as a chameleon, it provides a default:

preferences = {'color_pattern': 'camouflage'} color_pattern = preferences.get('color_pattern', 'default') # 'camouflage' missing_key = preferences.get('missing_key', 'default') # 'default'

Note to self: default is slang for "keyless" in Python dictionary land.

Null-coalescing for Nested Dictionaries

Python's .get() can be chained to handle nested dictionaries, ensuring you won't bang head on a TypeError:

superpreferences = {'preferences': {'color_pattern': 'camouflage'}} color_pattern = superpreferences.get('preferences', {}).get('color_pattern', 'default') # 'camouflage'

Note: Chains are not just for rappers anymore, programmers love them too!

Python's getattr() can fetch attributes from objects, but beware of its dark side—it raises exceptions faster than your angry manager:

class Preferences: color_pattern = 'camouflage' prefs = Preferences() color_pattern = getattr(prefs, 'color_pattern', 'neon') # 'camouflage'

Looks like getattr() also prefers camouflage over neon. Who knew?

Bored of the current Python null-coalescing situation? Always keep an eye on PEP 505—like season 100 of your favorite T.V. show, it could be coming... eventually.

Recognizing Pitfalls

The or operator doesn't discriminate—it evaluates both operands. This can be a letdown when operand number two is as slow as a sloth or as unpredictable as an escaped python:

# Find the first truthy value first_found = 0 or expensive_operation() # expensive_operation() will run even if 0 is falsy

Wow, who thought or could be so needy and demanding?

Be wary using reduce and lambda for null-coalescing, as they don't have manners to short circuit:

from functools import reduce # Costly, potentially runs all costly_functions() even if earlier ones return a truthy value value = reduce(lambda x, y: x or y, [costly_function(), another_costly_function()], 'default')

Reduce: Providing poor short circuiting performance since... well, let's just say it's been a while.

The {} fallback, when used with .get(), helps in hunting down nested dictionary keys without triggering traps (aka exceptions).

Pro Strats for Null-Coalescing

For multi-level attribute access, be a coding ninja and use recursion:

def safe_getattr(object, attribute_path, backup=None): attributes = attribute_path.split('.') for attr in attributes: object = getattr(object, attr, None) if object is None: return backup return object config = Config() value = safe_getattr(config, 'preferences.color_pattern', 'neon') # 'neon'

Well, neon is snazzy too, I guess.

The defaultdict can be your ever-reliable plan B, making sure a wandering key won't leave you empty-handed:

from collections import defaultdict preferences = defaultdict(lambda: defaultdict(str)) preferences['color_pattern'] = 'camouflage' color_pattern = preferences['color_pattern'] # 'camouflage' missing_key_also = preferences['missing_key_also'] # Returns '' (empty string) as a default

Good ol' defaultdict, always there when your keys go AWOL.