Naming a Sentinel

A sentinel here is a module-level singleton whose only job is to be recognised by identity – value is SENTINEL – so that it can never be confused with a value a caller might legitimately pass. The house rule, from the maintainer, covers the type: the sentinel object’s type class is named <SENTINEL>Type, the instance’s name in CamelCase with Type appended.

That rule says nothing about the object’s own name, which let the casings diverge with no rule naming any of them wrong. #937 closed that gap: the owner ruled for SCREAMING_SNAKE and accepted the breaking change outright, with no backport. So the type name derives from the object’s mechanically – title-case each underscore-separated word and append Type, with no per-sentinel exception:

#911’s housing ruling – one module for all of them – is why the table names a single defining module, and it is the only import path. The modules that use a sentinel import it from there but do not re-export it: #1744 dropped the per-module re-exports, under the ruling that a moved name keeps no re-export at its old path, ruled in review of #1715 (#1711). Import the object, and the type for an annotation, from pcapkit.corekit.sentinels.

Where a sentinel name already exists and already follows SCREAMING_SNAKE, keep it; renaming a published sentinel again costs every caller for no further gain.

ABSENT carries no leading underscore even though it is private – it is read in _declared_keywords() and discarded there, never leaving pcapkit.protocols.protocol. The owner ruled on #937 that dropping the underscore is fine so long as the documentation states that the type and the object are private and not for public use, which is what this page and AbsentType’s own docstring do in its place. So privacy here is documentation-only, and nothing in the name marks it out: when adding a sentinel, add it to the table above whether or not it is public.

What reaches users is the object only. The owner ruled on #911 that the objects alone – NULL and its siblings – are exported to users, so a public sentinel names its instance in pcapkit.corekit.sentinels’s __all__ and leaves the type out of it. The type stays importable by its dotted path there, for an annotation or an is guard; it is only import * that does not offer it. The __all__ there does name Sentinel, the abstract base every sentinel type shares, since that is no sentinel’s own type. The private sentinels, ABSENT and MISSING, are in neither, which is what private means here. AbsentType, ABSENT and their MISSING counterparts are documented on the sentinels API page as private rather than left off it, since nothing in the name says so.

Note

Every sentinel type subclasses Sentinel, the public abstract base that carries the singleton protocol, the repr and the immutability, and whose metaclass is SentinelMeta. So a new sentinel is short: subclass it, set __slots__ = () and _name to the object’s name, and add __bool__ if the sentinel stands for an absent value. NoValueType is the smallest example. Everything goes in the class body, since the type refuses any attribute set on it afterwards.

Why a Class and Not object()

A bare object() is just as safe under is, so safety is not the reason. The reason is legibility: a dedicated class can define __repr__, and that repr is what appears in a signature, in help() output and in a traceback. Compare what inspect.signature() renders for a method whose default is the sentinel:

# bare object(): an address, different every process
default: 'Any' = <object object at 0x7fc393324cf0>

# dedicated type with __repr__
default: 'Any' = NO_DEFAULT

Warning

Do not justify a dedicated class by claiming a subclass “could still compare equal via a custom __eq__”. A class that defines only __repr__ inherits identity __eq__ and is exactly as safe as object().

Ported code is exempt. A verbatim upstream port keeps its upstream’s sentinels as written: the value of a vendored copy is that it can still be diffed against upstream, and a house-style rewrite destroys that in exchange for a sentinel nobody outside the port ever sees. The rule above is for sentinels this package writes itself. The ruling was made for _NOT_FOUND = object() in the backport of functools.cached_property() that served interpreters below 3.8. With the floor at 3.8, cached_property is a re-export of functools.cached_property() itself (#2051).

Per-Sentinel Dunder Methods

Most of the protocol is shared and lives on Sentinel. Where the sentinels differ, the differences are needs, not inconsistencies:

__new__ returning a cached instance

Every sentinel has it, from the base, and returns the instance the metaclass made as the class statement ran. It guards against a caller constructing a second, non-identical sentinel that then fails every is check, and it hands back the cached instance rather than raising, as type(None)() does, so that mistake corrects itself. NullType documents the limit honestly: a module reload re-executes the class statement, so the guard does not survive one, and code holding the pre-reload instance will fail is. The module that matters there is pcapkit.corekit.sentinels, where the class statement lives; reloading one of the modules that imports a sentinel from there has no effect on it.

__bool__ returning False

NULL, NO_VALUE, ABSENT and MISSING have it, because each stands for an absent value and reads naturally in a boolean test. NO_DEFAULT deliberately does not: it is a marker meaning no default was supplied, it is only ever tested with is, and making it falsy would invite if not default: – which would then treat a caller’s genuine falsy default (0, '', None, False) the same as the sentinel, the very confusion the sentinel exists to prevent.

__copy__ / __deepcopy__ / __reduce__

Every sentinel has them, from the base. The owner ruled on #1842 that every sentinel is a true singleton, rather than gaining these only once a deep copy is found to reach it. __copy__ and __deepcopy__ return the object itself, and __reduce__ returns its module-level name, so pickle stores a reference to that name and every protocol loads the same object back. A guarded __new__ alone would not do, since pickle protocols 0 and 1 rebuild through copyreg._reconstructor, which calls object.__new__() directly. That path also limits pickles written before #1842: one of NO_VALUE, NO_DEFAULT or ABSENT at protocol 0 or 1 still loads, but as a second instance, because nothing on the class can intercept it.

__repr__

Every sentinel has it, from the base. It is the object’s own name – NULL, NO_VALUE, NO_DEFAULT, ABSENT, MISSING – as repr(None) is 'None', and str gives the same. That is what a signature, help() and a traceback show.

__setattr__ / __delattr__

Every sentinel has them, from the base, and they raise AttributeError for any name, as None.x = 1 does. __slots__ is empty, so there is no __dict__ to reach round them either. The metaclass does the same for the type, raising TypeError as type(None).x = 1 does, so neither _name nor the cached instance can be swapped out. It lets through only _abc_negative_cache and _abc_negative_cache_version, which _py_abc, the fallback abc uses without the C _abc module, rebinds during an isinstance() check (#1960). It also refuses to let a concrete sentinel type be subclassed, by a class statement or by register, as type(None) cannot be; Sentinel itself stays subclassable, being abstract. The owner asked for this in review of the change for #1842: a sentinel’s repr and its other behaviour follow Python’s own singletons, None, Ellipsis and NotImplemented.