The SC has decided to officially defer PEP 661. We’re happy to reconsider it for Python 3.15.
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].
Else, we’ll have to provide a Pydantic
UNSET
sentinel as an experimental feature as theUnsetType
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.
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.
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.