typing
typing copied to clipboard
Literal for sentinel values
This came up python/typeshed#3521: Currently I can't think of a way to type sentinel values that are often constructed by allowing a certain instance of object as the argument. For the example above it would be useful to be able to do something like this:
class PSS(AsymmetricPadding):
MAX_LENGTH: ClassVar[object]
def __init__(self, mgf: MGF1, salt_length: Union[int, Literal[MAX_LENGTH]]) -> None: ...
Alternatively we could add a type like Singleton type to typing:
class PSS(AsymmetricPadding):
MAX_LENGTH: Singleton
def __init__(self, mgf: MGF1, salt_length: Union[int, MAX_LENGTH]) -> None: ...
It would be nice if this could be written using Final and Literal:
# Does not work (yet)
A: Final = object()
def f(a: Literal[A]): ...
f(A)
If you're willing to rewrite the code some more (beyond adding type annotations), you can usually solve this with an enum, since enums are allowed in Literal:
# This does work
class AA(Enum):
A = 0
A: Final = AA.A
def f(a: Literal[AA.A]):
...
f(A)
(Alas, I haven't found a way to alias A = AA.A and be able to write Literal[A].)
You can use AA as the type instead of Listeral[AA.A]:
...
A: Final = AA.A
def f(a: AA):
...
f(A)
This mypy issue has related discussion: python/mypy#7642
Yeah it would be good to have some special-casing for ad-hoc sentinels. One of the main downsides I see for enum solution is that error messages if making a stub for existing library (as in original message) can be cryptic.
What about having a naming convention for sentinel enums in stubs? Maybe something like this:
class _Sentinel(Enum):
_SENTINEL = 0
This way even if the type leaks out of the stub, at least there is a hint about what it means, and the underscore prefix suggests that this is something internal to the module/stub.
What about having a naming convention for sentinel enums in stubs?
Yes, I was also thinking about this as a "quick fix".
Good idea for a quick fix.
Speaking about adding embedded annotations (not type stubs) I can say that I like @gvanrossum proposal (a combination of Final and Literal) very much.
It naturally reflects what is going on in the Python runtime.
Here's another possible solution using NewType.
_MaxLengthSentinel = NewType("_MaxLengthSentinel", object)
class PSS(AsymmetricPadding):
MAX_LENGTH: _MaxLengthSentinel
def __init__(self, mgf: MGF1, salt_length: Union[int, _MaxLengthSentinel]) -> None:
...
By naming the sentinel with an underscore, it will be considered private, so the only way to get to its instance is through the MAX_LENGTH class variable.
This solution appears to work fine with existing standards and type checker implementations.
The recipe with NewType has flaws:
def f(arg: Union[str, SENTINEL_TYPE) -> None:
if arg is SENTINEL:
arg = "default"
reveal_type(arg) # the type is still union
is doesn't remove SENTINEL type as arg is None does in mypy.
Yeah, the implementation would need to include an additional check, something like this:
def f(arg: Union[str, SENTINEL_TYPE]) -> None:
if arg is SENTINEL:
arg = "default"
else:
assert isinstance(arg, str)
reveal_type(arg)
Related: PEP 661 -- Sentinel Values
Related: PEP 661 -- Sentinel Values
If I read this pep correctly it would not cover the classic usage of sentinel flags, that are checked with is
Implementing the example given using the classic is check
def foo(value: int | Sentinel = MISSING) -> int:
if value is MISSING:
return 0
else:
return value + 1
would fail type checkers without special support Sentinels them since value would still have type int | Sentinel
In the Discourse thread the author said that they've "decided to forego type signatures specific to each sentinel". Could you please comment there if you believe is comparisons are vital?
Thanks for pointing to the discussion
As per https://discuss.python.org/t/pep-661-sentinel-values/9126/44 the proposed solution seem to implement Literal[MISSING]. So the Pep text seems outdated. That's unless I haven't missed anything in the discussion
The above function would be typed as def foo(value: int | Literal[MISSING]= MISSING) -> int: making is work with type checkers
No, the latest version of the PEP does not support sentinel literals. See https://github.com/taleinat/python-stdlib-sentinels/blob/main/pep-0661.rst#specific-type-signatures-for-each-sentinel-value.
Understood. That's a shame, since it makes Sentinel use very cumbersome when using type checkers
Understood. That's a shame, since it makes
Sentineluse very cumbersome when using type checkers
I think it was probably the correct decision for the PEP, since the PEP wasn't primarily typing-focused. I don't think this rules out the possibility of adding special-casing to type checkers in the future to better handle PEP 661 sentinel values. That could always be proposed in a future PEP.
Sure, but at that point I don't understand what's the point of adding Sentinel.
If we don't care about type checking support I think the classic flag = object() (or the flag = type('flag', (), {})() works fine enough
Sure, but at that point I don't understand what's the point of adding Signature. If we don't care about type checking support I think the classic
flag = object()(or theflag = type('flag', (), {})()works fine enough
(The discuss.python.org thread is the better place to discuss that, and I see you've already posted there :)
I needed to add type hints to a function with a default sentinel object idiom, i.e.
RAISE_ERROR = object()
def get_something(name, default=RAISE_ERROR):
...
I solved it by using @overload:
RAISE_ERROR = object()
import typing
@typing.overload
def get_something(name: str) -> str: ...
@typing.overload
def get_something(name: str, default: str) -> str: ...
def get_something(name, default=RAISE_ERROR):
...
It doesn't type check the function body but this was fine in my case since the function itself was trivial and I was mostly interested in adding type hints to the signature!