PEP 661: Sentinel Values

Proposed solution fits needs for pure-Python code. How about CPython parts, written in C? Or C extension modules? I’m pretty sure there is a zoo of poor-man replacements for this. E.g. the reduce() function uses NULL as default for the initial argument. perm() uses k=None.

Maybe it was discussed in the looong thread somewhere, but I would appreciate if that use case will be mentioned in the PEP text.

It wasn’t really discussed. I think it makes sense to provide a PySentinel_New(name, module) C API function. I’m not sure it’s needed for the cases you mention (C functions where NULL or None is acceptable), but it would make sense for replacing e.g. typing.NoDefault.

What should the interface look like? I’m leaning towards PySentinel_New(const char *name, const char *module).

1 Like

Sure, acceptable. But in mentioned cases its a replacement for missing values. Note also, that NULL breaks function signature.

Why not PyObject *module?

Why not a raw string? I am very open to being convinced about this, I don’t write much C.

My thinking was that for the typing.NoDefault marker, I’d want to be able to write something like:

no_default = PySentinel_New("NoDefault", "typing");
PyModule_AddObjectRef(m, "NoDefault", no_default):

I imagine that’s what most use cases would approximately look like.

I think this would happen in module initialization code where it’s natural to have a reference to the module object.

In the typing case, the module object is for the _typing module, but we want the object to pretend it is in typing. The same would go for functools.Placeholder, which is actually created in the _functools module.

I think the same thing will happen commonly in third-party code. For example, many numpy classes say they are in the numpy module, but in fact are presumably defined in some internal helper module.

2 Likes

Thanks @taleinat for accepting @Jelle’s offer for help. I have done my (admittedly very small) part with a review of Jelle’s PR.

While reviewing I started to wonder whether the PEP should provide guidance for type checkers on how to handle:

MISSING = sentinel('MISSING')
MISSING = sentinel('MISSING')

It is very contrived but wouldn’t be surprised if this pattern could emerge in very long modules. Is this a redefinition of MISSING that is allowed? Or should this raise an error? Or should each type checker be allowed to choose the behaviour/strictness they want?

I don’t remember this being discussed in this thread, but I think we can all agree that at this point it is quite hard to remember all the various discussions we have had in it :sweat_smile:

1 Like

This particular issue wasn’t discussed. We haven’t clearly defined this in the typing spec either for similar use cases. However, we just amended the conformance suite (remove type statement restrictions unsupported by spec or PEP by carljm · Pull Request #2249 · python/typing · GitHub) to no longer require an error when redefining a type alias; this feels similar.

In any case, this is something that can be changed later with a change to the typing spec.

1 Like

Tal agreed to add me as a coauthor, and I’m hoping to get the PEP implemented in Python 3.15.

I just merged a PEP change to make sentinel a builtin instead of a new module and add a C API: PEP 661: Update by JelleZijlstra · Pull Request #4923 · python/peps · GitHub . This change is now live at PEP 661 – Sentinel Values | peps.python.org . I also have a draft implementation at https://github.com/JelleZijlstra/cpython/pull/4 .

I also reopened PEP 661 -- Sentinel Values · Issue #258 · python/steering-council · GitHub to ask the Steering Council for a decision.

18 Likes

Sorry, I’ve grepped through this discussion and the PEP itself, I’ve only found the justification of adding this as builtin due to naming problems: peps/peps/pep-0661.rst at d7f07812decf396f2a50b8b55a43b43d63d63e69 · JelleZijlstra/peps · GitHub But, I’ve never found why it was decided not to make it typing.sentinel. As it was proposed here PEP 661: Sentinel Values - #328 by Jelle

The post you link is talking about a specific sentinel (typing.NoDefault), not the sentinel class itself.

The SC asked to make it a builtin: PEP 661: Sentinel Values - #234 by barry. The motivation was to make the feature easier to use. Putting it in typing wouldn’t make much sense because sentinels are not a typing-specific feature.

4 Likes

The PEP’s Abstract section says “Sentinels can be defined in Python with the sentinel() built-in class” but the Specification section is not entirely clear on the class aspect. Is sentinel a class? Is isinstance(sentinel(“FOO”), sentinel) true?


The PEP says “Sentinels do not support weakrefs”. Given that there is no global sentinel registry and sentinel objects can presumably be garbage collected, what prevents them from supporting weakrefs? Maybe for efficiency (to avoid the __weakref__ slot)?


Overall reading the PEP, sentinels remind me a lot of ES6 Symbol (there are also Lisp/Ruby symbols, but they have different semantics). The similarity was briefly mentioned up thread by @cben but not discussed further. In JS, Symbol is mainly used (AFAIK) to avoid conflicts, for example if you want to stash something on an object, it’s better to do so using a Symbol than a string, because it’s unique to you. The main differences seem to be:

  1. The __module__ property on sentinels, which according to the PEP are only added for picklability. The picklability does raise some questions, e.g.
from pickle import loads, dumps

a = sentinel("a")
a2 = sentinel("a")
assert a is not a2
assert loads(dumps(a)) is loads(dumps(a2)) is a is not a2

that feels a bit weird.

  1. Symbol allows “anonymous” symbols. Seems like Python could support sentinel(), by making such instances not picklable.
  2. JS has some optional Symbol registry thing, but I think we can disregard it for comparison.

Overall the similarity is enough that it makes me think if it might make sense to basically rename sentinelsymbol, adding some nice convergence between the world’s two most popular languages. Well, I’m not seriously proposing doing so at this extremely late point, and Python does have object(), but still wanted to mention it, since the potential use cases for “give me a unique, type-able, repr-able value” are greater than the sentinel use case.


For sentinels defined in a class scope, to avoid potential name clashes, or when a qualified repr would be clearer, one should pass the desired qualified name explicitly

IIUC, this is also needed for pickling to work. Might be worth an explicit mention in this sentence.

I also wonder how this jibes with:

If the name passed to sentinel() does not match the name the object is assigned to, type checkers should emit an error.

1 Like

Pickle will raise on dumps(a2) because it verifies the identity of the singleton being pickled. The object at __module__ with the returned qualified name from __reduce__ must be the same object being reduced. Thus a2 = sentinel("a") is unpicklable and anonymous sentinels are also unpicklable.


The current qualified name example of MyClass_NotGiven = sentinel('MyClass.NotGiven') is malformed with pickle in mind. It should demonstrate how qualified names are used in practice with a class, like is currently shown in the Additional Notes.

To ensure compatibility with pickle singletons it should be clear that the given name is always qualified and the text should not imply that unqualified names are an option (unless the object doesn’t need to be pickled.)

2 Likes

There is a lot of inertia behind sentinel but I don’t mind whichever name we end up with. sentinel has a narrow definition while the more broad term symbol is closer to how I and most others were going to use it anyways considering all the examples we’ve given so far.

I think another language having its own public API term for a unique object meant to avoid collisions means a name change is worth considering. It could be bad for Python to be stuck with something more contrarian, but it could also be that ES6 is being the odd one here.

The current origin for sentinel appears to be some public PyPI packages using the term, and its uncertain how much thought those modules put into their decision. We probably accepted it because it was easy not to confuse with other terminology. symbol is actually used a lot for programming language internals and might’ve been more confusing to discuss. In particular, public symbols tend to refer to reflection in most languages.

sentinel vs symbol was partially discussed early on, but that was more about whether the objects would collide across modules. This could be another example of symbol causing a minor confusion:

It’d be nice to have clear rationale for the name choice.

1 Like

On behalf of the Python Steering Council, I’m very happy to share that we have accepted PEP 661 for Python 3.15. Thank you @taleinat for creating the PEP and @Jelle for reviving it in time for 3.15. We especially appreciate how all of the SC’s earlier feedback was addressed nicely in the latest iteration of the PEP, resolving all of our previous concerns. Congrats!

41 Likes

Thank you to the Steering Council for the quick action on this PEP!

The implementation is in gh-148829: Implement PEP 661 by JelleZijlstra · Pull Request #148831 · python/cpython · GitHub . Please help review it so we can get this feature merged in an excellent state for Python 3.15.

14 Likes

Also thank you @taleinat for starting this PEP many years ago and allowing Jelle to help push it over the finish line. Can’t wait to use this!

5 Likes

Hi all, first post here ^^

I came across this PEP through a YouTube video covering upcoming 3.15 features, and since it was just accepted I figured it wasn’t too late to share a few thoughts. I haven’t read the full thread (337 posts is a lot!), so apologies if any of this has already been covered.

I’m not a CPython internals person, I mostly encounter sentinels from the typing side, as a way to express “this argument was not provided” in a signature.

One small concrete thing first

The 3.15 docs don’t mention the subclassing restriction at all. A user who tries class MySentinel(sentinel): ... gets a TypeError with no explanation and no pointer toward Enum as the grouped-sentinel alternative. Even if the ban stays for good reasons, that seems worth a documentation note regardless.

On the subclassing restriction itself

I didn’t find a rationale for the __init_subclass__ block anywhere, not in the PEP body, not in Rejected Ideas. The concern I can imagine is that class MISSING(sentinel): ... looks like a sentinel instance rather than a factory, which is fair. But the useful pattern doesn’t look like that:

class FalsySentinel(sentinel):
    def __bool__(self) -> bool:
        return False

MISSING = FalsySentinel("MISSING")

Lowercase name, explicit instantiation, the ambiguity mostly goes away. And __bool__ customisation was listed as a dropped feature in the PEP due to “added complexity,” but with subclassing it would just be… normal Python. No new API surface needed.

That said, I think I can see why the ban might exist technically. The __init__ uses sys._getframemodulename(1) with a hardcoded frame depth to capture the calling module for pickling, and a super().__init__() call from a subclass shifts that frame. That’s a real problem in pure Python. But since sentinel ships as a C builtin, that seems like something addressable at that level, which makes me genuinely unsure whether the ban reflects a considered design decision, or just that nobody needed subclassing badly enough to do the extra work.

I’d love to hear if that was actually discussed somewhere in the thread!

1 Like

This should indeed be mentioned in the docs, thanks for catching that. Please send a PR or issue (I’ll try to get to it but might forget).

As for the motivation, subclassing builtins is often somewhat problematic and can lead to confusing behavior. What you mention about module inference is one example. That’s why I preferred to keep the behavior more restricted here for now. That said, this is something we can revisit in the future, and if good use cases emerge we can enable subclassing in 3.16.

4 Likes

Done! Filed the issue at gh-149119 and went ahead with a PR at gh-149120.

Was a good excuse to make my first open source contribution. Thanks for the nudge!

8 Likes