Overview

sentinel-value is a Python package, that helps to create Sentinel Values - special singleton objects, akin to None, NotImplemented and Ellipsis.

It implements the sentinel() function (described by PEP 661), and also SentinelValue class for advanced cases (not a part of PEP 661).

Usage example:

>>> from sentinel_value import sentinel

>>> MISSING = sentinel("MISSING")

>>> def get_something(default=MISSING):
...     ...
...     if default is not MISSING:
...         return default
...     ...

why not just object()?

So, why not just MISSING = object()?

Because sentinel values have some benefits:

  • better repr()

  • friendly to typing

  • friendly to pickle

  • friendly to hot code reloading features of IDEs

So this is not radical killer feature, but more a list of small nice-to-have things.

function sentinel()

sentinel() function is the simple way to create sentinel objects in 1 line:

>>> MISSING = sentinel("MISSING")

It produces an instance of SentinelValue, with all its features (uniqueness, pickle-ability, etc), and it just works in most cases.

However, there are some cases where it doesn’t work well, and you may want to directly use the underlying class SentinelValue, described below.

class SentinelValue()

A little bit more advanced way to create sentinel objects is to do this:

>>> from sentinel_value import SentinelValue

>>> class Missing(SentinelValue):
...     pass

>>> MISSING = Missing(__name__, "MISSING")

Such code is slightly more verbose (than using sentinel()), but, there are some benefits:

  • It is portable (while sentinel() is not, because it relies on inspect.currentframe).

  • It is extensible. You can add and override various methods in your class.

  • Class definition is obvious. You can immediately find it in your code when you get AttributeError: 'Missing' object has no attribute '...'

  • Can be used with functools.singledispatch()

  • Friendly to typing on older Python versions, that don’t have typing.Literal

Type Annotations

PEP 661 suggests to use typing.Literal, like this:

from typing import Literal
from sentinel_value import sentinel

NOT_GIVEN = sentinel("NOT_GIVEN")

def foo(value: int | Literal[NOT_GIVEN]) -> None:
...     return None

But, there is a problem: mypy type checker thinks it is an error:

mypy main.py

main.py:6: error: Parameter 1 of Literal[...] is invalid  [misc]
main.py:6: error: Variable "main.NOT_GIVEN" is not valid as a type  [valid-type]
main.py:6: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
Found 2 errors in 1 file (checked 1 source file)

Maybe such typing.Literal expressions will be supported in the future, but at least now (November 2021, mypy v0.910, Python v3.10.0) it is broken, and you cannot use Literal[SENTINEL_VALUE] for type hinting.

So, for now, the only way to have proper type annotations is to avoid sentinel() function and instead make your own sub-classes of SentinelValue, like this:

>>> from typing import Union
>>> from sentinel_value import SentinelValue

>>> class NotGiven(SentinelValue):
...     pass

>>> NOT_GIVEN = NotGiven(__name__, "NOT_GIVEN")

>>> def foo(value: Union[int, NotGiven]) -> None:
...     return None

This way it works like a charm, and it doesn’t even require typing.Literal.

Naming Convention

PEP 661 doesn’t enforce any naming convention, however, I (the author of this Python package) would recommend using UPPER_CASE for sentinel objects, like this:

>>> NOT_SET = sentinel("NOT_SET")

or, when subclassing SentinelValue:

>>> class NotSet(SentinelValue):
...     pass

>>> NOT_SET = NotSet(__name__, "NOT_SET")

Why? Because:

  • Sentinel values are unique global constants by definition, and constants are NAMED_LIKE_THIS

  • This naming scheme gives slightly less cryptic error messages. For example, this:

    AttributeError: 'NotSet' object has no attribute 'foo'
    

    reads slightly better (at least to my eye) than this:

    AttributeError: 'NotSetType' object has no attribute 'foo'
    

API reference

class SentinelValue

SentinelValue(module_name, instance_name)

Class for special unique placeholder objects, akin to None and Ellipsis.

SentinelValue.__new__(cls, module_name, …)

SentinelValue.__init__(module_name, …)

Initialize SentinelValue object.

SentinelValue.__repr__()

Provide repr() for SentinelValue.

SentinelValue.__bool__()

Return False when SentinelValue is treated as bool().

Other members of the module

sentinel_value_instances

Dictionary that contains all instances of SentinelValue (and its subclasses).

sentinel_create_lock

A lock that prevents race conditions when creating new SentinelValue objects.

sentinel(instance_name[, repr])

Create an unique sentinel object.

class SentinelValue(module_name, instance_name)[source]

Class for special unique placeholder objects, akin to None and Ellipsis.

Useful for distinguishing “value is not set” and “value is set to None” cases as shown in this example:

>>> NOT_SET = SentinelValue(__name__, "NOT_SET")

>>> value = getattr(object, "some_attribute", NOT_SET)
>>> if value is NOT_SET:
...    print('attribute is not set')
attribute is not set

If you need a separate type (for use with typing or functools.singledispatch()), then you can create a subclass:

>>> from typing import Union

>>> class Missing(SentinelValue):
...     pass

>>> MISSING = Missing(__name__, "MISSING")

# Here is how the Missing class can be used for type hinting.
>>> value: Union[str, None, Missing] = getattr(object, "some_attribute", MISSING)
>>> if value is MISSING:
...    print("value is missing")
value is missing
__init__(module_name, instance_name)[source]

Initialize SentinelValue object.

Parameters
  • module_name (str) – name of Python module that hosts the sentinel value. In the majority of cases you should pass __name__ here.

  • instance_name (str) – name of Python variable that points to the sentinel value.

Return type

None

static __new__(cls, module_name, instance_name)[source]
__repr__()[source]

Provide repr() for SentinelValue.

By default, looks like this:

<MISSING>

You’re free to override it in a subclass if you want to customize it.

static __bool__()[source]

Return False when SentinelValue is treated as bool().

Sentinel values are always falsy.

This is done because most sentinel objects are “no value” kind of objects (they’re like None, but just not the None object).

So it is often handy to do if not value to check if there is no value (like if an attribute is set to None, or not set at all):

>>> NOT_SET = SentinelValue(__name__, "NOT_SET")

>>> value = getattr(object, "foobar", NOT_SET)

# Is the value None, or empty, or not set at all?
>>> if not value:
...    print("no value")
no value

If this doesn’t fit your case, you can override this method in a subclass.

sentinel_value_instances: Dict[str, SentinelValue] = {}

Dictionary that contains all instances of SentinelValue (and its subclasses).

This dictionary looks like this:

{
    "package1.module1.MISSING": SentinelValue("package1.module1", "MISSING"),
    "package2.module2.MISSING": SentinelValue("package2.module2", "MISSING"),
    "package2.module2.ABSENT": SentinelValue("package2.module2", "ABSENT"),
}

When a SentinelValue object is instanciated, it registers itself in this dictionary (and throws an error if already registered). This is needed to ensure that, for each name, there exists only 1 unique SentinelValue object.

sentinel_create_lock = <unlocked _thread.lock object>

A lock that prevents race conditions when creating new SentinelValue objects.

Problem: when you start multiple threads, they may try to create sentinel objects concurrently. If you’re lucky enough, you get duplicate SentinelValue instances, which is highly undesirable.

This sentinel_create_lock helps to protect against such race conditions.

The lock is acquired whenever a new SentienlValue object is created. So, when multiple threads try to create sentinel objects, then they’re executed in sequence, and the 1st thread really creates a new instance, and other threads will get the already existing instance.

sentinel(instance_name, repr=None)[source]

Create an unique sentinel object.

Implementation of PEP 661 https://www.python.org/dev/peps/pep-0661/

>>> MISSING = sentinel("MISSING")
>>> value = getattr(object, "value", MISSING)
>>> if value is MISSING:
...     print("value is not set")
value is not set
Parameters
  • instance_name (str) – Name of Python variable that points to the sentinel object. Needed for serialization (like pickle) and also nice repr().

  • repr (Optional[str]) – Any custom string that will be returned by func:repr. By default, composed as {module_name}.{instance_name}.

Return type

SentinelValue