-
Notifications
You must be signed in to change notification settings - Fork 304
Add sentinels (PEP 661) to the spec #2277
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
base: main
Are you sure you want to change the base?
Changes from all commits
d9961b2
de6d9ea
19e4152
e6dd6e3
84d6fd8
36a2158
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| conformant = "Unsupported" | ||
| conformance_automated = "Fail" | ||
| errors_diff = """ | ||
| Line 36: Expected 1 errors | ||
| Line 39: Expected 1 errors | ||
| Line 20: Unexpected errors ['specialtypes_sentinels.py:20: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type]', 'specialtypes_sentinels.py:20: error: Variable "specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type]'] | ||
| Line 22: Unexpected errors ['specialtypes_sentinels.py:22: error: Expression is of type "Any", not MISSING? [assert-type]', 'specialtypes_sentinels.py:22: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type]'] | ||
| Line 24: Unexpected errors ['specialtypes_sentinels.py:24: error: Variable "specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type]'] | ||
| Line 26: Unexpected errors ['specialtypes_sentinels.py:26: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type]'] | ||
| Line 28: Unexpected errors ['specialtypes_sentinels.py:28: error: Expression is of type "Any", not Cls.IN_CLASS? [assert-type]', 'specialtypes_sentinels.py:28: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type]'] | ||
| """ | ||
| output = """ | ||
| specialtypes_sentinels.py:14: error: Incompatible default for parameter "x" (default has type "Sentinel", parameter has type "int") [assignment] | ||
| specialtypes_sentinels.py:20: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:20: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases | ||
| specialtypes_sentinels.py:20: error: Variable "specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:22: error: Expression is of type "Any", not MISSING? [assert-type] | ||
| specialtypes_sentinels.py:22: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:22: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases | ||
| specialtypes_sentinels.py:24: error: Variable "specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:24: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases | ||
| specialtypes_sentinels.py:26: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:26: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases | ||
| specialtypes_sentinels.py:28: error: Expression is of type "Any", not Cls.IN_CLASS? [assert-type] | ||
| specialtypes_sentinels.py:28: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:28: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| conformance_automated = "Pass" | ||
| errors_diff = """ | ||
| """ | ||
| output = """ | ||
| ./specialtypes_sentinels.py:14:10: Default value for argument x incompatible with declared type int [incompatible_default] | ||
| ./specialtypes_sentinels.py:36:6: Incompatible argument type for x: expected int | Literal[<<MISSING>>, <SPECIAL>] but got <Cls.IN_CLASS> [incompatible_argument] | ||
| ./specialtypes_sentinels.py:39:6: Incompatible argument type for x: expected int | Literal[<Cls.IN_CLASS>] but got <<MISSING>> [incompatible_argument] | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| conformant = "Unsupported" | ||
| conformance_automated = "Fail" | ||
| errors_diff = """ | ||
| Line 36: Expected 1 errors | ||
| Line 39: Expected 1 errors | ||
| Line 20: Unexpected errors ['Expected a type form, got instance of `type[int] | UnionType | Any` [not-a-type]'] | ||
| Line 22: Unexpected errors ['assert_type(Sentinel, Unknown) failed [assert-type]', 'Expected a type form, got instance of `Sentinel` [not-a-type]'] | ||
| Line 24: Unexpected errors ['Expected a type form, got instance of `type[int] | UnionType` [not-a-type]'] | ||
| Line 26: Unexpected errors ['Expected a type form, got instance of `type[int] | UnionType` [not-a-type]'] | ||
| Line 28: Unexpected errors ['assert_type(Sentinel, Unknown) failed [assert-type]', 'Expected a type form, got instance of `Sentinel` [not-a-type]'] | ||
| Line 30: Unexpected errors ['assert_type(Unknown, int) failed [assert-type]'] | ||
| """ | ||
| output = """ | ||
| ERROR specialtypes_sentinels.py:14:20-27: Default `Sentinel` is not assignable to parameter `x` with type `int` [bad-function-definition] | ||
| ERROR specialtypes_sentinels.py:20:14-37: Expected a type form, got instance of `type[int] | UnionType | Any` [not-a-type] | ||
| ERROR specialtypes_sentinels.py:22:20-32: assert_type(Sentinel, Unknown) failed [assert-type] | ||
| ERROR specialtypes_sentinels.py:22:24-31: Expected a type form, got instance of `Sentinel` [not-a-type] | ||
| ERROR specialtypes_sentinels.py:24:24-37: Expected a type form, got instance of `type[int] | UnionType` [not-a-type] | ||
| ERROR specialtypes_sentinels.py:26:14-32: Expected a type form, got instance of `type[int] | UnionType` [not-a-type] | ||
| ERROR specialtypes_sentinels.py:28:20-37: assert_type(Sentinel, Unknown) failed [assert-type] | ||
| ERROR specialtypes_sentinels.py:28:24-36: Expected a type form, got instance of `Sentinel` [not-a-type] | ||
| ERROR specialtypes_sentinels.py:30:20-28: assert_type(Unknown, int) failed [assert-type] | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| conformant = "Unsupported" | ||
| conformance_automated = "Fail" | ||
| errors_diff = """ | ||
| Line 36: Expected 1 errors | ||
| Line 39: Expected 1 errors | ||
| Line 20: Unexpected errors ['specialtypes_sentinels.py:20:20 - error: Variable not allowed in type expression (reportInvalidTypeForm)', 'specialtypes_sentinels.py:20:30 - error: Variable not allowed in type expression (reportInvalidTypeForm)'] | ||
| Line 22: Unexpected errors ['specialtypes_sentinels.py:22:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure)', 'specialtypes_sentinels.py:22:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)'] | ||
| Line 24: Unexpected errors ['specialtypes_sentinels.py:24:30 - error: Variable not allowed in type expression (reportInvalidTypeForm)'] | ||
| Line 26: Unexpected errors ['specialtypes_sentinels.py:26:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)'] | ||
| Line 28: Unexpected errors ['specialtypes_sentinels.py:28:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure)', 'specialtypes_sentinels.py:28:28 - error: Variable not allowed in type expression (reportInvalidTypeForm)'] | ||
| Line 30: Unexpected errors ['specialtypes_sentinels.py:30:21 - error: "assert_type" mismatch: expected "int" but received "int | Unknown" (reportAssertTypeFailure)'] | ||
| """ | ||
| output = """ | ||
| specialtypes_sentinels.py:14:20 - error: Expression of type "Sentinel" cannot be assigned to parameter of type "int" | ||
| "Sentinel" is not assignable to "int" (reportArgumentType) | ||
| specialtypes_sentinels.py:20:20 - error: Variable not allowed in type expression (reportInvalidTypeForm) | ||
| specialtypes_sentinels.py:20:30 - error: Variable not allowed in type expression (reportInvalidTypeForm) | ||
| specialtypes_sentinels.py:22:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure) | ||
| specialtypes_sentinels.py:22:24 - error: Variable not allowed in type expression (reportInvalidTypeForm) | ||
| specialtypes_sentinels.py:24:30 - error: Variable not allowed in type expression (reportInvalidTypeForm) | ||
| specialtypes_sentinels.py:26:24 - error: Variable not allowed in type expression (reportInvalidTypeForm) | ||
| specialtypes_sentinels.py:28:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure) | ||
| specialtypes_sentinels.py:28:28 - error: Variable not allowed in type expression (reportInvalidTypeForm) | ||
| specialtypes_sentinels.py:30:21 - error: "assert_type" mismatch: expected "int" but received "int | Unknown" (reportAssertTypeFailure) | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| conformant = "Unsupported" | ||
| conformance_automated = "Fail" | ||
| errors_diff = """ | ||
| Line 36: Expected 1 errors | ||
| Line 39: Expected 1 errors | ||
| Line 20: Unexpected errors ['specialtypes_sentinels.py:20:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation', 'specialtypes_sentinels.py:20:30: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation'] | ||
| Line 22: Unexpected errors ['specialtypes_sentinels.py:22:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo`'] | ||
| Line 24: Unexpected errors ['specialtypes_sentinels.py:24:30: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a type expression'] | ||
| Line 26: Unexpected errors ['specialtypes_sentinels.py:26:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation'] | ||
| Line 28: Unexpected errors ['specialtypes_sentinels.py:28:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo`'] | ||
| Line 30: Unexpected errors ['specialtypes_sentinels.py:30:9: error[type-assertion-failure] Type `int | Unknown` does not match asserted type `int`'] | ||
| """ | ||
| output = """ | ||
| specialtypes_sentinels.py:14:11: error[invalid-parameter-default] Default value of type `Sentinel` is not assignable to annotated parameter type `int` | ||
| specialtypes_sentinels.py:20:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation | ||
| specialtypes_sentinels.py:20:30: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation | ||
| specialtypes_sentinels.py:22:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo` | ||
| specialtypes_sentinels.py:24:30: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a type expression | ||
| specialtypes_sentinels.py:26:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation | ||
| specialtypes_sentinels.py:28:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo` | ||
| specialtypes_sentinels.py:30:9: error[type-assertion-failure] Type `int | Unknown` does not match asserted type `int` | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| conformant = "Unsupported" | ||
| conformance_automated = "Fail" | ||
| errors_diff = """ | ||
| Line 36: Expected 1 errors | ||
| Line 39: Expected 1 errors | ||
| Line 20: Unexpected errors ['specialtypes_sentinels.py:20: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type]', 'specialtypes_sentinels.py:20: error: Variable "tests.specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type]'] | ||
| Line 22: Unexpected errors ['specialtypes_sentinels.py:22: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type]'] | ||
| Line 24: Unexpected errors ['specialtypes_sentinels.py:24: error: Variable "tests.specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type]'] | ||
| Line 26: Unexpected errors ['specialtypes_sentinels.py:26: error: Variable "tests.specialtypes_sentinels.IN_CLASS" is not valid as a type [valid-type]'] | ||
| Line 28: Unexpected errors ['specialtypes_sentinels.py:28: error: Variable "tests.specialtypes_sentinels.IN_CLASS" is not valid as a type [valid-type]'] | ||
| Line 30: Unexpected errors ['specialtypes_sentinels.py:30: error: Expression is of type "int | Any", not "int" [misc]'] | ||
| """ | ||
| output = """ | ||
| specialtypes_sentinels.py:14: error: Incompatible default for parameter "x" (default has type "Sentinel", parameter has type "int") [assignment] | ||
| specialtypes_sentinels.py:20: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:20: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases | ||
| specialtypes_sentinels.py:20: error: Variable "tests.specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:20: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases | ||
| specialtypes_sentinels.py:22: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:22: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases | ||
| specialtypes_sentinels.py:24: error: Variable "tests.specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:24: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases | ||
| specialtypes_sentinels.py:26: error: Variable "tests.specialtypes_sentinels.IN_CLASS" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:26: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases | ||
| specialtypes_sentinels.py:28: error: Variable "tests.specialtypes_sentinels.IN_CLASS" is not valid as a type [valid-type] | ||
| specialtypes_sentinels.py:28: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases | ||
| specialtypes_sentinels.py:30: error: Expression is of type "int | Any", not "int" [misc] | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| from typing_extensions import Sentinel, assert_type | ||
|
|
||
| # > Sentinel objects may be used in type annotations if they are defined using | ||
| # > a simple assignment of the form ``NAME = sentinel('NAME')`` in the | ||
| # > global scope or in a class body that is not within a function. | ||
|
|
||
| MISSING = Sentinel("<MISSING>") # name is not required to match the variable name | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If it's still generally considered good style where possible to have the names match (and if it isn't, why is this case different from e.g. See discussion in astral-sh/ty#4091
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to do this by turning off the error code for ty in our checks. That way, we enforce that the type checker has a mode that allows this; a type checker that always errors on mismatched names is not compliant. In general I'd rather avoid "E?" as it makes the spec weaker in its checks.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, we were planning to make this the same error code that enforces name matching on
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd be willing to relax the check for TypeVar etc. too but that's a little out of scope for this PR. I do think the case for having a mismatched name is stronger for sentinels, which have more runtime uses than other objects where we might want a name match.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I see the rationale for it being different from TypeVar etc. We can just make it a different error code in ty. Feels somewhat odd to have two different error codes for what looks like an identical pattern, but if it's expected that some users will want to enforce same-name for TypeVar et al but not for sentinel, then it's worth it. |
||
| SPECIAL = Sentinel("SPECIAL") | ||
|
|
||
| class Cls: | ||
| IN_CLASS = Sentinel("Cls.IN_CLASS") | ||
|
|
||
|
|
||
| def func1(x: int = MISSING) -> None: # E: incompatible default | ||
| pass | ||
|
|
||
| # > Type checkers must support narrowing union types involving sentinels using the | ||
| # > ``is`` and ``is not`` operators | ||
|
|
||
| def func2(x: int | MISSING | SPECIAL = MISSING) -> None: | ||
| if x is MISSING: | ||
| assert_type(x, MISSING) | ||
| else: | ||
| assert_type(x, int | SPECIAL) | ||
|
|
||
| def func3(x: int | Cls.IN_CLASS = Cls.IN_CLASS) -> None: | ||
| if x is Cls.IN_CLASS: | ||
| assert_type(x, Cls.IN_CLASS) | ||
| else: | ||
| assert_type(x, int) | ||
|
|
||
|
|
||
| func2(1) # ok | ||
| func2(MISSING) # ok | ||
| func2(SPECIAL) # ok | ||
| func2(Cls.IN_CLASS) # E: incompatible argument | ||
|
|
||
| func3(1) # ok | ||
| func3(MISSING) # E: incompatible argument | ||
| func3(Cls.IN_CLASS) # ok | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,6 +53,46 @@ are highly dynamic. | |
| When used in a type hint, the expression ``None`` is considered | ||
| equivalent to ``type(None)``. | ||
|
|
||
| .. _ `sentinels`: | ||
|
|
||
| Sentinels | ||
| --------- | ||
|
|
||
| Sentinel objects may be used in type annotations to represent themselves:: | ||
|
|
||
| MISSING = sentinel('MISSING') | ||
| OTHER = sentinel('OTHER') | ||
|
|
||
| def f(x: int | MISSING = MISSING) -> int: | ||
| if x is MISSING: | ||
| return 0 | ||
| return x | ||
|
|
||
| f(OTHER) # Error, OTHER is not an int or MISSING | ||
| f(MISSING) # OK, MISSING is a valid argument | ||
|
|
||
| Sentinels may be created using the ``sentinel()`` built-in in Python 3.15 | ||
| and higher. ``typing_extensions`` provides a backport of this function. For | ||
| historical reasons the object was first introduced under the name | ||
| ``typing_extensions.Sentinel``, and later ``typing_extensions.sentinel`` was | ||
| added as an alias; type checkers should support both. | ||
|
Comment on lines
+75
to
+78
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure about
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like at the moment That said, if there really aren't any uses of
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's already being used by Pydantic: https://github.com/pydantic/pydantic/blob/597fa692eaaa90132f683e88ca994f28e185463b/pydantic-core/python/pydantic_core/__init__.py#L148
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Is that really an issue though if almost no type checker supports it? To be clear I'm not talking about removing it from
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to support both since it doesn't seem like it would add a significant burden to type checkers.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sure, once support for PEP 661 is implemented, supporting Mypy still has code for
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the Pydantic side, we explicitly document the feature using sentinels as experimental, meaning (at least for us) users should be prepared for things to break, so our end we are fine not supporting |
||
|
|
||
| Sentinel objects may be used in type annotations if they are defined using | ||
| a simple assignment of the form ``NAME = sentinel('NAME')`` in the | ||
| global scope or in a class body that is not within a function. The name of the | ||
| variable need not match the string argument passed to ``sentinel()`` but it is | ||
| conventional to do so for names in the global scope. | ||
|
|
||
| Type checkers must support narrowing union types involving sentinels using the | ||
| ``is`` and ``is not`` operators:: | ||
|
|
||
| def g(x: int | MISSING) -> None: | ||
| if x is MISSING: | ||
| assert_type(x, MISSING) | ||
| else: | ||
| assert_type(x, int) | ||
|
|
||
|
|
||
| .. _`noreturn`: | ||
|
|
||
| ``NoReturn`` | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.