Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Literal for sentinel values #689

Open
srittau opened this issue Dec 3, 2019 · 20 comments
Open

Literal for sentinel values #689

srittau opened this issue Dec 3, 2019 · 20 comments
Labels
topic: feature Discussions about new features for Python's type annotations

Comments

@srittau
Copy link
Collaborator

srittau commented Dec 3, 2019

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: ...
@gvanrossum
Copy link
Member

gvanrossum commented Dec 4, 2019

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].)

@JukkaL
Copy link
Contributor

JukkaL commented Dec 4, 2019

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

@ilevkivskyi
Copy link
Member

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.

@JukkaL
Copy link
Contributor

JukkaL commented Dec 4, 2019

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.

@ilevkivskyi
Copy link
Member

What about having a naming convention for sentinel enums in stubs?

Yes, I was also thinking about this as a "quick fix".

@srittau
Copy link
Collaborator Author

srittau commented Dec 4, 2019

Good idea for a quick fix.

@asvetlov
Copy link

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.

@erictraut
Copy link
Collaborator

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.

@asvetlov
Copy link

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.

@erictraut
Copy link
Collaborator

erictraut commented Nov 25, 2020

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)

@srittau srittau added the topic: feature Discussions about new features for Python's type annotations label Nov 4, 2021
@srittau
Copy link
Collaborator Author

srittau commented Nov 4, 2021

Related: PEP 661 -- Sentinel Values

@CaselIT
Copy link

CaselIT commented Sep 14, 2022

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

@layday
Copy link

layday commented Sep 14, 2022

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?

@CaselIT
Copy link

CaselIT commented Sep 14, 2022

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

@layday
Copy link

layday commented Sep 14, 2022

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.

@CaselIT
Copy link

CaselIT commented Sep 14, 2022

Understood. That's a shame, since it makes Sentinel use very cumbersome when using type checkers

@AlexWaygood
Copy link
Member

AlexWaygood commented Sep 14, 2022

Understood. That's a shame, since it makes Sentinel use 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.

@CaselIT
Copy link

CaselIT commented Sep 14, 2022

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

@AlexWaygood
Copy link
Member

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 the flag = 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 :)

@pelme
Copy link

pelme commented Nov 17, 2022

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!

Julian added a commit to python-jsonschema/referencing that referenced this issue Nov 29, 2023
See python/typing#689 and PEP 661 which look related.

But really I still want my plugin that lets me separate between public
API types and private API types.
guillp added a commit to guillp/requests_oauth2client that referenced this issue Mar 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: feature Discussions about new features for Python's type annotations
Projects
None yet
Development

No branches or pull requests

10 participants