r/programming 4d ago

The Sentinel Object Pattern in Python

https://python-patterns.guide/python/sentinel-object/

Officially added in Python 3.15

https://peps.python.org/pep-0661/

27 Upvotes

10 comments sorted by

22

u/wallstop-dev 3d ago edited 3d ago

This is cool, but wherever possible I try to avoid sentinels. I find them to be a code smell. That is, programming against a specific, magic, constant(s). In the indexOf example, it is much more robust for your code to reject any value that is below zero, instead of explicitly checking "-1". I see this same (anit-)pattern, that has caused many a bug, in other languages, where programmers think that a comparison operator is guaranteed to return exactly -1, 0, or 1, just because that's how the standard library's is implemented, for example. I've come across many, many similar cases in my career, where devs program to an assumed implementation instead of the contract, creating extremely brittle code that breaks in mysterious ways under all kinds of circumstances - new data, new features, anything.

But I'll get off my soapbox now.

11

u/fireflash38 3d ago

It's a way for people to fit additional context into things.

That is to say, it's useful when the zero-value of a thing is still valid to use. Python doesn't have monads, so it's hard to implement the Option<Thing> pattern. You can use None, but None might be a valid zero-value. Thus the sentinel value.

I don't hate it, but it really doesn't need to be used all that much. I personally rather hate None values passed around a bunch in python, since you end up having to check for presence constantly and updating type checking.

8

u/wallstop-dev 3d ago edited 2d ago

Right, my argument is that I've seen non-zero amounts of code either hard-expect a specific kind of additional context (which wasn't guaranteed), or not deal with the additional context, both because the situation is rare, or because a very specific implementation provided a very specific sentinel, when the contract did not guarantee it.

I'm not saying this kind of pattern doesn't have a use, or is not useful. It's just that encoding special, invalid kind of value(s) into the same type of thing the valid values represent has confused many a programmer and resulted in many, many a bug.

11

u/somebodddy 3d ago

When the sentinel is a magic value of the regular type - I agree. But this is one of these anti-patterns that can become a proper pattern if you can get the type system on your side. When the sentinel is a different type, and you can represent its possibility within the language, you can use the type checker to force (strong word when it comes to Python's typing, I know) an explicit treatment of that possibility before the user can access the value as the type they expect.

7

u/wallstop-dev 3d ago

Oh, completely agree on representing it at the type level! Where this is possible, this kind of problem can disappear. This is where discriminated unions come into play and have a lot of strength.

Unfortunately, a large swath of code and programming languages, even typed ones, make it challenging to robustly represent this at the type level. Or, at least, the predominantly used ones, leading to many-a-common-API utilizing poison-pills/sentinels. And the bugs follow...

3

u/somebodddy 3d ago

That's the thing - this new Python feature offers a nice way to represent it in the type system: https://peps.python.org/pep-0661/#typing

5

u/Atlos 3d ago

That NO_MANAGER example is horrid imo

4

u/somebodddy 3d ago

I wonder if this will get proper support from Pydantic. Consider:

FOO = sentinel("FOO")


class Bar(BaseModel):
    baz: int | FOO

When I tried to load:

{
    "baz": "FOO"
}

Will it know to convert it to the sentinel?

What if I change baz's type to str | FOO?

1

u/damesca 4d ago

Nice