Use Sentinel Values in Python 3.15

Use Sentinel Values in Python 3.15

Many functions take an optional argument and need to know whether the caller supplied it. The simplest way is to give the argument a default of None and check for that. This stops working when None is a value the caller might pass on purpose. Consider a lookup that reads a setting by name. A setting may legitimately be stored as None, so a result of None could mean the setting holds that value, or that the setting does not exist at all.

The usual solution is to create a unique object and compare against it by identity:

import copy

_MISSING = object()


def get_setting(settings, name, default=_MISSING):
    if name in settings:
        return settings[name]
    if default is _MISSING:
        raise KeyError(name)
    return default


settings = {'theme': 'dark', 'proxy': None}

print(get_setting(settings, 'theme'))  # dark
print(get_setting(settings, 'proxy'))  # None
print(get_setting(settings, 'lang', 'en'))  # en
print(repr(_MISSING))  # <object object at 0x...>
print(copy.deepcopy(_MISSING) is _MISSING)  # False

The approach works. Asking for proxy returns the stored None rather than being mistaken for a missing key, and asking for lang returns the supplied default. Without a default, the same call raises KeyError.

It has three drawbacks: a meaningless representation, an identity that copy.deepcopy does not preserve, and no type that lets a checker tell this object apart from other values.

Since Python 3.15, the built-in sentinel creates objects designed for this purpose:

import copy

MISSING = sentinel('MISSING')


def get_setting(settings, name, default=MISSING):
    if name in settings:
        return settings[name]
    if default is MISSING:
        raise KeyError(name)
    return default


settings = {'theme': 'dark', 'proxy': None}

print(get_setting(settings, 'theme'))  # dark
print(get_setting(settings, 'proxy'))  # None
print(get_setting(settings, 'lang', 'en'))  # en
print(repr(MISSING))  # MISSING
print(copy.deepcopy(MISSING) is MISSING)  # True

Each drawback is addressed: the object prints as its name, copy.deepcopy returns the same object, and the sentinel can be used directly in a type expression such as str | MISSING.

Leave a Comment

Cancel reply

Your email address will not be published.