Tidbits | Aug. 16, 2018

Pro-Tip – Sentinel values in Python

by Flavio Curella

Sometimes it is necessary to differentiate between an argument that has not been provided, and an argument provided with the value None. For that purpose, we create what's called a 'sentinel value'.

For example, let's assume you want to define a Field class. Field instances must have a value or declare a default, and None could be a perfectly valid value to have:

class Field:
    def __init__(self, default=sentinel):
        self.value = default

    def set(self, value):
        self.value = value

    def get(self):
        if self.value is sentinel:
            raise ValueError("this field has no value!")

eula_accepted = Field()
eula_accepted.get()  # raises `ValueError`

eula_accepted = Field(default=None)
eula_accepted.get()  # doesn't raise. `None` means the EULA hasn't been neither accepted or reject yet.

The most common approach is to declare the sentinel value with object():

sentinel = object()

This approach is the quickest and most common, but it has some issues. To quote Joseph Jevnik:

One is that they don't repr well so they make debugging harder. Another issue is that they cannot be pickled or copied. You also cannot take a weak reference to a sentinel which can break some caching code and makes them harder to use.

For example:

sentinel = object()

repr(sentinel)
# '<object object at 0x10823e8d0>'

To work around this issue, some people create their own Sentinel class. But I've found a quicker way in the unittest.mock module.

from unittest.mock import sentinel

NotSet = sentinel.NotSet

repr(NotSet)
# 'sentinel.NotSet'

If you don't feel like importing from unittest in your application code, you could install the mock package, or "hide it under the rug" by aliasing somewhere in your code base and importing it from there:

# in `myproject.types`
from unittest.mock import sentinel
# somewhere else in your project:
from myproject.types import sentinel

An alternative to unittest.mock.sentinel is to declare your own sentinel class and use it as a value:

class NotSet:
    pass

# PS: Remember to use the class _itself_.
def fn(default=NotSet):
    pass

This will give you a not really pretty, but useful enough repr:

repr(NotSet)
# "<class '__main__.NotSet'>"

Of course, you could go one step further and declare your own repr:

class NotSet:
    def __repr__(self):
        return 'NotSet'

repr(NotSet)
# 'NotSet'

Of all the options, I think using unittest.mock.Sentinel is my favorite. Importing from unittest in my application code is a compromise that I'm willing to make in exchange for having something ready to use.

It is often necessary to differentiate between an argument that has not been provided, and an argument provided with the value `None`. For that purpose, we create what's called a 'sentinel value'.

2018-08-16T15:35:04.890368 2018-08-17T09:28:06.520900 2018 python,sentinel