PEP 661: Sentinel Values

The SC has decided to officially defer PEP 661. We’re happy to reconsider it for Python 3.15.

7 Likes

In Pydantic, we’d like to introduce an UNSET sentinel (one of the most requested features), and we would really like to make use of stdlib sentinels. In particular, not having to introduce an UnsetType and being able to use UNSET both as a type expression and a value is a convenient feature.

As this was deferred for 3.15, I wanted to know if the implementation would also be ported to typing_extensions as sentinels have a special meaning in typing? There’s already a precedent with @warnings.deprecated. If so, it potentially means we won’t have to wait until 3.15 to make use of it [1].


  1. Else, we’ll have to provide a Pydantic UNSET sentinel as an experimental feature as the UnsetType won’t be necessary with this PEP. ↩︎

Yes, I think it’d make sense to add a backport in typing-extensions. Would be good to first make sure the API isn’t going to change too much though, so we don’t run into compatibility problems later if we need to change the implementation in typing-extensions.

7 Likes

Thanks Barry (and other SC members). When I can find the time to follow this up I’ll schedule some meeting hours time to discuss further.

6 Likes

I have nothing useful to offer on the specifics, but I would very much like to see this or something very similar adopted.

What I will add to this discussion is that typing.Optional is discouraged, and pylance can mark it as deprecated if deprecateTypingAliases is set. Unfortunately the repository and issue that I filed no longer exists, but the response was roughly that Optional would have been deprecated by the typing community if it hadn’t been so widely used. If Optional really is to be discouraged, it would be nice to have a better way to cover its use in a sentilelish way.

I mention this because I, and I suspect others, have been using Optional in a specific way because we weren’t aware of sentinels.

I have been typing.Optional in a sentinel-like way just to indicate intent to the human reader of the code. That is, even though Optional[T] is equivalent to T | None, the distinction tells me that sorts of meanings to ascribe to 'None`.

Another place I use None where a more specific sentinel would be useful is for values that have not yet been computed. In many of those cases I could (and perhaps should) be using @cached_property, but even so, I found myself wanting sentinels before I was aware of the concept explicitly.

After the inclusion of sentinels in typing-extensions (with sentinel instances being unpicklable until the PEP decides on the correct implementation), I started playing around with an unset sentinel in Pydantic (PR), and tried figuring out what would be the best pickling behavior:

# In `unset.py`:
UNSET = Sentinel('UNSET')

# In `main.py`:
from unset import UNSET


class Model(BaseModel):
    f: int | UNSET = UNSET


m = Model()

Ideally, when pickling m, m.f should be the same unset.UNSET instance, so that special-casing of the sentinel (e.g. exclusion of the f field on serialization) can be performed using identity comparison, e.g. with the following pseudo-code:

m_dict = {}
for k, v in m.__dict__.items():
   if v is unset.UNSET:
       continue
   m_dict[k] = v

The registry approach would fulfill this use case: when unpickling UNSET, the Sentinel constructor would return the already existing unset.UNSET instance.

Alternatively, the UNSET sentinel could be defined as an instance of an UnsetType class (as msgspec does currently), but we wouldn’t be able to leverage the possibility to use the sentinel instance as a type annotation.

1 Like