Static type checking for sentinel values (PEP 661)
Hi typing folks, I'm continuing to pursue adding a tool for defining sentinel values to the standard library. See PEP 616 [1], previous discussion [2] and previous thread on this mailing list [3], for more background and details. The gist of the proposal is to add a function or class, e.g. Sentinel(), for defining sentinels in a standard way, providing nice properties such as clear reprs, support for strict type signatures and surviving copying/unpickling. Old: MISSING = object() New: MISSING = Sentinel("MISSING") Besides allowing use in the stdlib, having this be defined in a PEP and included in the stdlib means that we can agree how static type analysis would handle such sentinels. So here are some questions, where I especially would like input from people implementing static type analyzers: 1. The PEP currently suggests using Literal[MISSING] in type signatures. Is this reasonable? What would this require of static type analyzer authors and maintainers? 2. What would the type signature used during definition look like? I couldn't be MISSING: Literal[MISSING] = Sentinel["MISSING"], right? Would just using MISSING = Sentinel("MISSING") be fine, relying on type inference? I assume this would require special-casing in type checkers? 3. How does Final play into this? Note that the currently proposed implementation does NOT require that the variable name be identical to the string passed to Sentinel, nor that the Sentinel object can be found using the given name in the module's namespace in order for copying/unpickling to work. Is this reasonable or should it be changed? I'd like to add 1-2 paragraphs about type checking to the PEP, addressing the above, and anything else you think is important to have well defined. - Tal Einat [1] https://www.python.org/dev/peps/pep-0661/ [2] https://discuss.python.org/t/pep-661-sentinel-values/9126 [3] https://mail.python.org/archives/list/typing-sig@python.org/thread/NDEJ7UCDP...
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
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
I think the need for a custom sentinel value already takes us well beyond the domain of "an average Python developer". The most common sentinel in Python is `None`, and it works well in the vast majority of use cases. Plus, `None` enjoys support from the runtime (which enforces the fact that it is a singleton) and extensive support in all type checkers. For those reasons, `None` will continue to be used as a sentinel value in the vast majority of use cases even after support is added for custom sentinel values. I appreciate the need for custom sentinel values, but these use cases are relatively rare and are typically constrained to library authors who are (almost by definition) more advanced developers. Library authors are an important and vocal constituency, but they're small in number compared to library consumers. That's something we should keep in mind when considering the relative importance of "readability for the consumer" vs "conciseness of implementation for the author". The more I think about this, the more problems I see with the construct `MISSING = Sentinel("MISSING")`. It begs the question: should a type checker treat the symbol `MISSING` as a variable or a type alias? There are very different rules for the two, so it's important to specify which semantics are implied. Type aliases can be declared only once, whereas variables can be assigned multiple times (possibly within different conditional code blocks). For that reason, determining the type of a variable typically requires code flow analysis, except in the case where its type is explicitly annotated. For this reason (and others), variables are not allowed to be used within type annotations. Type annotations are allowed to reference symbols that have not yet appeared within the file (i.e. forward declarations), so code-flow ordering cannot be used for symbols that appear within a type annotation. Any use of a variable within a type annotation is therefore considered an error today by most type checkers, and it's important that we retain that constraint. For that reason, `MISSING` cannot be considered a variable if it's going to appear in type annotations like `Literal[MISSING]`. What if we treat `MISSING` as a type alias rather than a variable? Type checkers would then enforce that it is assigned only once — and only in the module (global) scope. However, unlike other type aliases, the value of `MISSING` is used at runtime (as a sentinel value). Even more problematic, if `MISSING` is a treated as a type alias, what type would be implied by the type annotation `MISSING` on its own? And why would it need to be wrapped in a `Literal`? The draft PEP 661 and Alex's proposal both suffer from these problems. My class-based proposal uses existing constructs and semantics — both at runtime and within the type system, so it doesn't introduce new special cases or inconsistencies. All of the type system rules are well established and clear. And the resulting type is easily understood by consumers a library that uses a custom sentinel value in its interface. I still haven't heard any reasons I find compelling for why the class-based proposal isn't the best approach. However, if the consensus is that we need to use "simple objects" rather than classes, then I would recommend against using `Literal` to describe the type. Instead, I would treat it as a type alias, in which case it should be used without a `Literal` wrapper. Here's how that would look in practice: ```python def func(value: int | MISSING = MISSING): ... ``` This suffers from the fact that (as I mentioned above) `MISSING` is used inconsistently as both a type alias and a value. The identifier `MISSING` appears twice in the function signature but has two different semantic meanings. I find this to be a bit confusing, but there is at least a precedent for it: `None`. You will see `None` used in a similar fashion as above: ```python def func(value: int | None = None): ... ``` -Eric -- Eric Traut Contributor to pyright & pylance Microsoft
On Mon, 18 Oct 2021 at 03:39, Eric Traut <eric@traut.com> wrote:
I think the need for a custom sentinel value already takes us well beyond the domain of "an average Python developer".
The most common sentinel in Python is `None`, and it works well in the vast majority of use cases. Plus, `None` enjoys support from the runtime (which enforces the fact that it is a singleton) and extensive support in all type checkers. For those reasons, `None` will continue to be used as a sentinel value in the vast majority of use cases even after support is added for custom sentinel values.
I appreciate the need for custom sentinel values, but these use cases are relatively rare and are typically constrained to library authors who are (almost by definition) more advanced developers. Library authors are an important and vocal constituency, but they're small in number compared to library consumers. That's something we should keep in mind when considering the relative importance of "readability for the consumer" vs "conciseness of implementation for the author".
The more I think about this, the more problems I see with the construct `MISSING = Sentinel("MISSING")`. It begs the question: should a type checker treat the symbol `MISSING` as a variable or a type alias? There are very different rules for the two, so it's important to specify which semantics are implied.
IMO, this would be better discussed on the sentinel PEP thread, as it's about the API design. I don't think we should be discussing API changes on a specialist list like the typing-SIG. If the question was *just* about how to annotate the existing design, then this might be the right place, but once we're saying that there's an issue with the agreed-on design "because of type annotation difficulties" that's something that should be brought up in the PEP discussion. (Personally, I feel that typing concerns shouldn't force us to choose a more awkward API, but I'd rather explain my reasoning in the context of the sentinel discussion where we have a concrete use case). Paul
On Mon, Oct 18, 2021 at 11:17 AM Paul Moore <p.f.moore@gmail.com> wrote:
On Mon, 18 Oct 2021 at 03:39, Eric Traut <eric@traut.com> wrote:
I think the need for a custom sentinel value already takes us well
beyond the domain of "an average Python developer".
The most common sentinel in Python is `None`, and it works well in the
vast majority of use cases. Plus, `None` enjoys support from the runtime (which enforces the fact that it is a singleton) and extensive support in all type checkers. For those reasons, `None` will continue to be used as a sentinel value in the vast majority of use cases even after support is added for custom sentinel values.
I appreciate the need for custom sentinel values, but these use cases
are relatively rare and are typically constrained to library authors who are (almost by definition) more advanced developers. Library authors are an important and vocal constituency, but they're small in number compared to library consumers. That's something we should keep in mind when considering the relative importance of "readability for the consumer" vs "conciseness of implementation for the author".
The more I think about this, the more problems I see with the construct
`MISSING = Sentinel("MISSING")`. It begs the question: should a type checker treat the symbol `MISSING` as a variable or a type alias? There are very different rules for the two, so it's important to specify which semantics are implied.
IMO, this would be better discussed on the sentinel PEP thread, as it's about the API design. I don't think we should be discussing API changes on a specialist list like the typing-SIG. If the question was *just* about how to annotate the existing design, then this might be the right place, but once we're saying that there's an issue with the agreed-on design "because of type annotation difficulties" that's something that should be brought up in the PEP discussion. (Personally, I feel that typing concerns shouldn't force us to choose a more awkward API, but I'd rather explain my reasoning in the context of the sentinel discussion where we have a concrete use case).
Paul
If this discussion leads to re-considering the chosen design, then I will indeed bring that up for continued discussion on the dedicated PEP thread (on discuss.python.org). At this point I'm still gathering information and trying to get a better understanding of what the different designs would mean for type checkers. Eric and Alex's remarks have been very helpful in that respect, including suggesting alternatives and explaining how they would have different repercussions in terms of type checking. Certainly though, those wanting to follow the discussion on the PEP don't necessarily have to follow this thread as well, and I won't make any decisions based solely on the discussion here. - Tal Einat
On Mon, Oct 18, 2021 at 10:22 PM Tal Einat <taleinat@gmail.com> wrote:
On Mon, Oct 18, 2021 at 11:17 AM Paul Moore <p.f.moore@gmail.com> wrote:
On Mon, 18 Oct 2021 at 03:39, Eric Traut <eric@traut.com> wrote:
I think the need for a custom sentinel value already takes us well
beyond the domain of "an average Python developer".
The most common sentinel in Python is `None`, and it works well in the
vast majority of use cases. Plus, `None` enjoys support from the runtime (which enforces the fact that it is a singleton) and extensive support in all type checkers. For those reasons, `None` will continue to be used as a sentinel value in the vast majority of use cases even after support is added for custom sentinel values.
I appreciate the need for custom sentinel values, but these use cases
are relatively rare and are typically constrained to library authors who are (almost by definition) more advanced developers. Library authors are an important and vocal constituency, but they're small in number compared to library consumers. That's something we should keep in mind when considering the relative importance of "readability for the consumer" vs "conciseness of implementation for the author".
The more I think about this, the more problems I see with the construct
`MISSING = Sentinel("MISSING")`. It begs the question: should a type checker treat the symbol `MISSING` as a variable or a type alias? There are very different rules for the two, so it's important to specify which semantics are implied.
IMO, this would be better discussed on the sentinel PEP thread, as it's about the API design. I don't think we should be discussing API changes on a specialist list like the typing-SIG. If the question was *just* about how to annotate the existing design, then this might be the right place, but once we're saying that there's an issue with the agreed-on design "because of type annotation difficulties" that's something that should be brought up in the PEP discussion. (Personally, I feel that typing concerns shouldn't force us to choose a more awkward API, but I'd rather explain my reasoning in the context of the sentinel discussion where we have a concrete use case).
Paul
If this discussion leads to re-considering the chosen design, then I will indeed bring that up for continued discussion on the dedicated PEP thread (on discuss.python.org). At this point I'm still gathering information and trying to get a better understanding of what the different designs would mean for type checkers. Eric and Alex's remarks have been very helpful in that respect, including suggesting alternatives and explaining how they would have different repercussions in terms of type checking.
Certainly though, those wanting to follow the discussion on the PEP don't necessarily have to follow this thread as well, and I won't make any decisions based solely on the discussion here.
I'll also just mention, in regard to Paul's closing remark, that there may indeed be a balance to strike between typing concerns and other concerns. First and foremost, I want to try to ensure that everyone affected gets a chance to voice their thoughts and concerns. I hope to find a solution that doesn't require a hard compromise. - Tal
Am 17.10.21 um 11:24 schrieb Tal Einat:
Hi typing folks,
I'm continuing to pursue adding a tool for defining sentinel values to the standard library. See PEP 616 [1], previous discussion [2] and previous thread on this mailing list [3], for more background and details.
The gist of the proposal is to add a function or class, e.g. Sentinel(), for defining sentinels in a standard way, providing nice properties such as clear reprs, support for strict type signatures and surviving copying/unpickling.
Without going into implementation details, I would also like to echo that I don't like the Literal syntax. This seems like unnecessary visual noise. Literal was necessary for strings due to the ambiguous nature of quoted annotation and was extended to other literals for consistency. I don't think either of these arguments are applicable for sentinel values. - Sebastian
On Tue, 19 Oct 2021 at 09:50, Sebastian Rittau <srittau@rittau.biz> wrote:
Am 17.10.21 um 11:24 schrieb Tal Einat:
Hi typing folks,
I'm continuing to pursue adding a tool for defining sentinel values to the standard library. See PEP 616 [1], previous discussion [2] and previous thread on this mailing list [3], for more background and details.
The gist of the proposal is to add a function or class, e.g. Sentinel(), for defining sentinels in a standard way, providing nice properties such as clear reprs, support for strict type signatures and surviving copying/unpickling.
Without going into implementation details, I would also like to echo that I don't like the Literal syntax. This seems like unnecessary visual noise. Literal was necessary for strings due to the ambiguous nature of quoted annotation and was extended to other literals for consistency. I don't think either of these arguments are applicable for sentinel values.
Maybe what needs to be expressed here is the actual intention, which is "argument x must be specified with a value of type T, or it must be omitted (and the default value used)". In other words, a usage like f(x=MISSING) would be rejected by the type checker (assuming that MISSING was not a value of type T), but f() would be valid. The code inside f would be checked on the basis that x is either type T or it's the default value. If `Optional` wasn't already taken to mean "or None", I'd write this as `def f(x=MISSING: Optional[T])` Yes, there are edge cases where people might need to explicitly pass the sentinel, but they are rare enough that having to annotate that with a "don't check the types, I know what I'm doing" directive is reasonable in that case. I don't think it's possible to express that with type annotations at the moment, but maybe it's worth considering? Paul
It is possible, but it's a little annoying :-) Here's a pattern I've used before: ``` if typing.TYPE_CHECKING: AnyBaseClass = typing.Any else: AnyBaseClass = object class Missing(AnyBaseClass): pass MISSING = Missing() def f(x: int = MISSING): ... ``` Could actually be quite nice if the runtime let you subclass from Any. On Tue, 19 Oct 2021 at 02:22, Paul Moore <p.f.moore@gmail.com> wrote:
On Tue, 19 Oct 2021 at 09:50, Sebastian Rittau <srittau@rittau.biz> wrote:
Am 17.10.21 um 11:24 schrieb Tal Einat:
Hi typing folks,
I'm continuing to pursue adding a tool for defining sentinel values to the standard library. See PEP 616 [1], previous discussion [2] and previous thread on this mailing list [3], for more background and
details.
The gist of the proposal is to add a function or class, e.g. Sentinel(), for defining sentinels in a standard way, providing nice properties such as clear reprs, support for strict type signatures and surviving copying/unpickling.
Without going into implementation details, I would also like to echo that I don't like the Literal syntax. This seems like unnecessary visual noise. Literal was necessary for strings due to the ambiguous nature of quoted annotation and was extended to other literals for consistency. I don't think either of these arguments are applicable for sentinel values.
Maybe what needs to be expressed here is the actual intention, which is "argument x must be specified with a value of type T, or it must be omitted (and the default value used)". In other words, a usage like f(x=MISSING) would be rejected by the type checker (assuming that MISSING was not a value of type T), but f() would be valid. The code inside f would be checked on the basis that x is either type T or it's the default value. If `Optional` wasn't already taken to mean "or None", I'd write this as `def f(x=MISSING: Optional[T])`
Yes, there are edge cases where people might need to explicitly pass the sentinel, but they are rare enough that having to annotate that with a "don't check the types, I know what I'm doing" directive is reasonable in that case.
I don't think it's possible to express that with type annotations at the moment, but maybe it's worth considering? Paul _______________________________________________ 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: hauntsaninja@gmail.com
Paul Moore writes:
Maybe what needs to be expressed here is the actual intention, which is "argument x must be specified with a value of type T, or it must be omitted (and the default value used)".
I believe this can be expressed using overloads: ``` from typing import overload from sentinel import Sentinel MISSING = Sentinel("MISSING") @overload def f() -> int: ... @overload def f(arg: str) -> str: ... def f(arg: str | Sentinel = MISSING) -> int | str: if arg is MISSING: return 42 return arg ``` MyPy is actually pretty happy about this (try it out on Mypy playground here: https://mypy-play.net/?mypy=latest&python=3.10&flags=show-error-codes%2Cstrict&gist=975fc21b03d3131eca08529d0e0e506d) — the only bit it doesn't understand is that `if arg is MISSING` acts as a type-narrowing clause, so you have to alter the last line of the runtime implementation to `return typing.cast(str, arg)`. However: That's pretty complicated if we're saying you need to do that every time you want to use a sentinel. I don't like that this leaves open the "edge case" for when people do need to explicitly pass in the sentinel value. With ^this way of typing the function, MyPy will raise an error if anybody does explicitly pass in the sentinel value. That's very desirable for something like dataclasses.MISSING, where users of the library really shouldn't ever be using it directly. But, there will surely be some cases where it's not desirable, and that will cause bug reports. Best, Alex
On 19 Oct 2021, at 10:21, Paul Moore <p.f.moore@gmail.com> wrote:
On Tue, 19 Oct 2021 at 09:50, Sebastian Rittau <srittau@rittau.biz> wrote:
Am 17.10.21 um 11:24 schrieb Tal Einat: Hi typing folks,
I'm continuing to pursue adding a tool for defining sentinel values to the standard library. See PEP 616 [1], previous discussion [2] and previous thread on this mailing list [3], for more background and details.
The gist of the proposal is to add a function or class, e.g. Sentinel(), for defining sentinels in a standard way, providing nice properties such as clear reprs, support for strict type signatures and surviving copying/unpickling.
Without going into implementation details, I would also like to echo that I don't like the Literal syntax. This seems like unnecessary visual noise. Literal was necessary for strings due to the ambiguous nature of quoted annotation and was extended to other literals for consistency. I don't think either of these arguments are applicable for sentinel values.
Maybe what needs to be expressed here is the actual intention, which is "argument x must be specified with a value of type T, or it must be omitted (and the default value used)". In other words, a usage like f(x=MISSING) would be rejected by the type checker (assuming that MISSING was not a value of type T), but f() would be valid. The code inside f would be checked on the basis that x is either type T or it's the default value. If `Optional` wasn't already taken to mean "or None", I'd write this as `def f(x=MISSING: Optional[T])`
Yes, there are edge cases where people might need to explicitly pass the sentinel, but they are rare enough that having to annotate that with a "don't check the types, I know what I'm doing" directive is reasonable in that case.
I don't think it's possible to express that with type annotations at the moment, but maybe it's worth considering? Paul _______________________________________________ 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
participants (6)
-
Alex Waygood -
Eric Traut -
Paul Moore -
Sebastian Rittau -
Shantanu Jain -
Tal Einat