-
Notifications
You must be signed in to change notification settings - Fork 305
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
Changes from all commits
d9961b2
de6d9ea
19e4152
e6dd6e3
84d6fd8
36a2158
9e7b570
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,9 @@ | ||
| conformant = "Pass" | ||
| conformance_automated = "Pass" | ||
| errors_diff = """ | ||
| """ | ||
| output = """ | ||
| ERROR specialtypes_sentinels.py:14:20-27: Default `<MISSING>` is not assignable to parameter `x` with type `int` [bad-function-definition] | ||
| ERROR specialtypes_sentinels.py:36:7-19: Argument `Cls.Cls.IN_CLASS` is not assignable to parameter `x` with type `int | <MISSING> | SPECIAL` in function `func2` [bad-argument-type] | ||
| ERROR specialtypes_sentinels.py:39:7-14: Argument `<MISSING>` is not assignable to parameter `x` with type `int | Cls.Cls.IN_CLASS` in function `func3` [bad-argument-type] | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| conformant = "Partial" | ||
| notes = """ | ||
| Requires the string passed to `Sentinel` to match the assigned variable name. | ||
| This causes valid sentinel types and narrowing operations to be rejected. | ||
| """ | ||
| conformance_automated = "Fail" | ||
| errors_diff = """ | ||
| Line 14: Expected 1 errors | ||
| Line 36: Expected 1 errors | ||
| Line 39: Expected 1 errors | ||
| Line 7: Unexpected errors ['specialtypes_sentinels.py:7:1 - error: Sentinel must be assigned to a variable with the same name (reportGeneralTypeIssues)'] | ||
| Line 11: Unexpected errors ['specialtypes_sentinels.py:11:5 - error: Sentinel must be assigned to a variable with the same name (reportGeneralTypeIssues)'] | ||
| Line 20: Unexpected errors ['specialtypes_sentinels.py:20:20 - 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 | SPECIAL" (reportAssertTypeFailure)', 'specialtypes_sentinels.py:22:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)'] | ||
| Line 24: Unexpected errors ['specialtypes_sentinels.py:24:21 - error: "assert_type" mismatch: expected "int | SPECIAL" but received "int | Unknown | SPECIAL" (reportAssertTypeFailure)'] | ||
| 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:7:1 - error: Sentinel must be assigned to a variable with the same name (reportGeneralTypeIssues) | ||
| specialtypes_sentinels.py:11:5 - error: Sentinel must be assigned to a variable with the same name (reportGeneralTypeIssues) | ||
| specialtypes_sentinels.py:20:20 - error: Variable not allowed in type expression (reportInvalidTypeForm) | ||
| specialtypes_sentinels.py:22:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown | SPECIAL" (reportAssertTypeFailure) | ||
| specialtypes_sentinels.py:22:24 - error: Variable not allowed in type expression (reportInvalidTypeForm) | ||
| specialtypes_sentinels.py:24:21 - error: "assert_type" mismatch: expected "int | SPECIAL" but received "int | Unknown | SPECIAL" (reportAssertTypeFailure) | ||
| 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) | ||
| """ |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| conformant = "Pass" | ||
| conformance_automated = "Pass" | ||
| errors_diff = """ | ||
| """ | ||
| output = """ | ||
| specialtypes_sentinels.py:14:11: error[invalid-parameter-default] Default value of type `MISSING` is not assignable to annotated parameter type `int` | ||
| specialtypes_sentinels.py:36:7: error[invalid-argument-type] Argument to function `func2` is incorrect: Expected `int | MISSING | SPECIAL`, found `IN_CLASS` | ||
| specialtypes_sentinels.py:39:7: error[invalid-argument-type] Argument to function `func3` is incorrect: Expected `int | IN_CLASS`, found `MISSING` | ||
| """ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| conformant = "Pass" | ||
| conformance_automated = "Pass" | ||
| errors_diff = """ | ||
| """ | ||
| output = """ | ||
| specialtypes_sentinels.py:14: error: Incompatible default for parameter "x" (default has type "Sentinel('<MISSING>')", parameter has type "int") [assignment] | ||
| specialtypes_sentinels.py:36: error: Argument 1 to "func2" has incompatible type "Sentinel('Cls.IN_CLASS')"; expected "int | Sentinel('<MISSING>') | Sentinel('SPECIAL')" [arg-type] | ||
| specialtypes_sentinels.py:39: error: Argument 1 to "func3" has incompatible type "Sentinel('<MISSING>')"; expected "int | Sentinel('Cls.IN_CLASS')" [arg-type] | ||
| """ |
| 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.