I agree with Eric on the usability point -- I think that the proposal for hinting sentinels with e.g. `Literal[A]` suffers from the fact that it is *particularly* clear that `A` represents a sentinel value. However, I disagree with Eric on his other points. This pattern: ``` from typing import final class SentinelMeta(type): def __repr__(cls): return f"<{cls.__name__}>" @final class MISSING(metaclass=SentinelMeta): ... ``` is far from easy to implement for an average Python developer. It requires knowledge of metaclasses, an advanced concept, and a non-obvious import from `typing`. It also takes six lines of code -- having to do that every time you want a sentinel with a nice repr is extremely tiresome. The whole reason why a PEP has been proposed -- and gained a lot of popular support -- is precisely because this is a common need for which there is no easy, obvious solution. Perhaps you might argue that the metaclass is not really necessary; I would argue that the repr of a sentinel object matters a great deal (in fact, the repr of a sentinel was what initially triggered the writing of the PEP: https://mail.python.org/archives/list/python-dev@python.org/thread/ZLVPD2OIS... ). Here is a different proposal, which I am sure would be more difficult for type-checkers to implement, but would -- in my opinion -- be much more user-friendly: 1. Add a `__class_getitem__` method to the `sentinel.Sentinel` class that is proposed in the PEP, allowing it to be parameterised in the same way as `typing.Literal` etc. 2. Have type-checkers raise an error if `Sentinel` is parameterised by anything that is not an instance of `Sentinel`. 3. In all other respects, type-checkers should treat `Sentinel[A]` the same as they would treat `Literal[A]`. 4. I do not think it is necessary for type-checkers to raise an error if the name of the sentinel does not match the specified repr of the sentinel. There are many Python objects that are assigned to variable names that do not match their reprs; in this respect, sentinels are quite different from `TypeVar`s. With this proposal, you would create and use the sentinel like this: ``` from sentinel import Sentinel MISSING = Sentinel("<MISSING.>") def func(arg: int | Sentinel[MISSING] = MISSING) -> None: if arg is MISSING: print('Got a sentinel!') else: print(2 * arg) ``` Much as with Eric's proposal, this would also require type-checkers to understand that `if arg is MISSING` acts as a type-narrowing clause. I have no insight as to exactly how difficult this would be for type-checkers to implement, but thought I would throw it out there as an idea. Thoughts? Best, Alex On Sun, Oct 17, 2021 at 7:31 PM Eric Traut <eric@traut.com> wrote:
The pattern that's being proposed here (`A = Sentinel("A")`) is similar to `TypeVar`, which type checkers special case. In particular, they enforce that the literal name passed to the `TypeVar` constructor matches the name of the variable to which it is assigned. Following this precedent, one could say that type checkers should enforce the same constraint for `Sentinel`, and a name mismatch would be flagged as an error. Only an assignment to a matching variable name (`A = Sentinel("A")`) would be allowed.
This proposal differs from `TypeVar` in that the resulting variable is not a type that can be used by itself within a type annotation. Rather, it's a value that is used at runtime and must be wrapped in some other type (like `Literal`) to be used within a type annotation.
I think that using `Literal[A]` to spell the type has problems from a usability perspective. It's not clear from looking at this type that it represents a sentinel. Increasingly, Python developers are relying on type information within function signatures to guide them as they code. If I were to see a parameter with type `int | Literal[A]`, I would be confused about what it means.
Let me offer a different proposal that addresses the usability problem and has the additional benefit that it already works with all type checkers today (doesn't require any new special casing), and it doesn't require any new runtime support. It wouldn't even require a new PEP, just a small change to the existing typing.pyi type stub that would be entirely backward compatible with all versions of Python. The proposal is to simply use a class as a sentinel. For readability, we would define a new type alias within `typing.pyi` called `Sentinel` that simply aliases `type[_T]`.
```python # This type alias would be added to typing.pyi. Sentinel = type[_T] ```
```python # Defining a new sentinel would look like this. Note the `final` decorator. @final class MISSING: pass ```
```python # Using the sentinel in a type signature would look like this. def func(value: int | Sentinel[MISSING] = MISSING): if value is MISSING: print("Received sentinel") return
reveal_type(value) # should reveal "int" ```
This proposal would benefit from one small improvement to mypy and pyright (and presumably the other type checkers). We'd want to add type narrowing support for the `A is B` pattern where `B` is a final class. That's relatively easy to do and applies more broadly to use cases beyond sentinels.
The latest draft of PEP 661 considers something similar to my proposal but rejects it for reasons that I don't understand. (Refer to the "Using class objects" header in the "Rejected Ideas" section.)
My proposal above doesn't allow for customization of the `__repr__`. If that's deemed important by the developer who defines the sentinel, it can be accommodated through the use of a metaclass, requiring just a few additional lines of code:
```python class SentinelMeta(type): def __repr__(cls): return f"<{cls.__name__}>"
@final class MISSING(metaclass=SentinelMeta): ... ```
In summary, I think this proposal addresses all of the requirements of a sentinel, is typing friendly (and supports strict typing), addresses usability issues for language servers, doesn't require any new runtime support, doesn't require any special-case handling in type checkers, doesn't introduce any backward compatibility issues, could be rolled out immediately rather than waiting for multiple years, and eliminates the need for a PEP and the associated ratification process.
Thoughts?
-Eric
-- Eric Traut Contributor to pyright & pylance Microsoft _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: alex.waygood@gmail.com