I was also thinking that. I’ve got a reasonable working implementation of MISSING = sentinel("MISSING"), but one which generates a class and an instance, rather than just using a class object as you suggest. AFAICT, the only drawbacks of this are using the same mechanism as Enum, logging and namedtuple of inspecting stack frames to find the module where it is defined, and using a naming hack instead of trying to set the class as a module attribute. I also didn’t like that the type isn’t directly available and would potentially have an awkward name, but being able to use Literal[MISSING] possibly makes that irrelevant.
I’m still not sure what would be preferable. (I originally intended to first think about it some more and get thoughts from a few initial reviewers.)
For those wondering, here is the (simplified) code:
import sys
if hasattr(sys, '_getframe'):
get_parent_frame = lambda: sys._getframe(2)
else:
def get_parent_frame():
"""Return the frame object for the caller's parent stack frame."""
try:
raise Exception
except Exception:
return sys.exc_info()[2].tb_frame.f_back.f_back
def sentinel(name):
"""Create a unique sentinel object."""
repr_ = f'<{name}>'
# This is a hack to get copying and unpickling to work without setting the
# class as a module attribute.
class_name = f'{name}.__class__'
class_namespace = {
'__repr__': lambda self: repr_,
}
cls = type(class_name, (), class_namespace)
# For pickling to work, the __module__ variable needs to be set to the
# name of the module where the sentinel is defined.
try:
module = get_parent_frame().f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
else:
cls.__module__ = module
sentinel = cls()
cls.__new__ = lambda self: sentinel
return sentinel