Overview¶
Contents
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:
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 oninspect.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 havetyping.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
|
Class for special unique placeholder objects, akin to |
|
|
|
Initialize |
Provide |
|
Return False when |
Other members of the module
Dictionary that contains all instances of SentinelValue (and its subclasses). |
|
A lock that prevents race conditions when creating new |
|
|
Create an unique sentinel object. |
- class SentinelValue(module_name, instance_name)[source]¶
Class for special unique placeholder objects, akin to
None
andEllipsis
.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
orfunctools.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.
- __repr__()[source]¶
Provide
repr()
forSentinelValue
.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 asbool()
.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 theNone
object).So it is often handy to do
if not value
to check if there is no value (like if an attribute is set toNone
, 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 uniqueSentinelValue
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
- Return type