Skip to content

Commit 513ebea

Browse files
Add sentinels (PEP 661) to the spec (#2277)
1 parent 4fb3203 commit 513ebea

10 files changed

Lines changed: 201 additions & 13 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
conformant = "Unsupported"
2+
conformance_automated = "Fail"
3+
errors_diff = """
4+
Line 36: Expected 1 errors
5+
Line 39: Expected 1 errors
6+
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]']
7+
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]']
8+
Line 24: Unexpected errors ['specialtypes_sentinels.py:24: error: Variable "specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type]']
9+
Line 26: Unexpected errors ['specialtypes_sentinels.py:26: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type]']
10+
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]']
11+
"""
12+
output = """
13+
specialtypes_sentinels.py:14: error: Incompatible default for parameter "x" (default has type "Sentinel", parameter has type "int") [assignment]
14+
specialtypes_sentinels.py:20: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type]
15+
specialtypes_sentinels.py:20: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
16+
specialtypes_sentinels.py:20: error: Variable "specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type]
17+
specialtypes_sentinels.py:22: error: Expression is of type "Any", not MISSING? [assert-type]
18+
specialtypes_sentinels.py:22: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type]
19+
specialtypes_sentinels.py:22: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
20+
specialtypes_sentinels.py:24: error: Variable "specialtypes_sentinels.SPECIAL" is not valid as a type [valid-type]
21+
specialtypes_sentinels.py:24: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
22+
specialtypes_sentinels.py:26: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type]
23+
specialtypes_sentinels.py:26: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
24+
specialtypes_sentinels.py:28: error: Expression is of type "Any", not Cls.IN_CLASS? [assert-type]
25+
specialtypes_sentinels.py:28: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type]
26+
specialtypes_sentinels.py:28: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
27+
"""
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
conformance_automated = "Pass"
2+
errors_diff = """
3+
"""
4+
output = """
5+
./specialtypes_sentinels.py:14:10: Default value for argument x incompatible with declared type int [incompatible_default]
6+
./specialtypes_sentinels.py:36:6: Incompatible argument type for x: expected int | Literal[<<MISSING>>, <SPECIAL>] but got <Cls.IN_CLASS> [incompatible_argument]
7+
./specialtypes_sentinels.py:39:6: Incompatible argument type for x: expected int | Literal[<Cls.IN_CLASS>] but got <<MISSING>> [incompatible_argument]
8+
"""
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
conformant = "Pass"
2+
conformance_automated = "Pass"
3+
errors_diff = """
4+
"""
5+
output = """
6+
ERROR specialtypes_sentinels.py:14:20-27: Default `<MISSING>` is not assignable to parameter `x` with type `int` [bad-function-definition]
7+
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]
8+
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]
9+
"""
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
conformant = "Partial"
2+
notes = """
3+
Requires the string passed to `Sentinel` to match the assigned variable name.
4+
This causes valid sentinel types and narrowing operations to be rejected.
5+
"""
6+
conformance_automated = "Fail"
7+
errors_diff = """
8+
Line 14: Expected 1 errors
9+
Line 36: Expected 1 errors
10+
Line 39: Expected 1 errors
11+
Line 7: Unexpected errors ['specialtypes_sentinels.py:7:1 - error: Sentinel must be assigned to a variable with the same name (reportGeneralTypeIssues)']
12+
Line 11: Unexpected errors ['specialtypes_sentinels.py:11:5 - error: Sentinel must be assigned to a variable with the same name (reportGeneralTypeIssues)']
13+
Line 20: Unexpected errors ['specialtypes_sentinels.py:20:20 - error: Variable not allowed in type expression (reportInvalidTypeForm)']
14+
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)']
15+
Line 24: Unexpected errors ['specialtypes_sentinels.py:24:21 - error: "assert_type" mismatch: expected "int | SPECIAL" but received "int | Unknown | SPECIAL" (reportAssertTypeFailure)']
16+
Line 26: Unexpected errors ['specialtypes_sentinels.py:26:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)']
17+
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)']
18+
Line 30: Unexpected errors ['specialtypes_sentinels.py:30:21 - error: "assert_type" mismatch: expected "int" but received "int | Unknown" (reportAssertTypeFailure)']
19+
"""
20+
output = """
21+
specialtypes_sentinels.py:7:1 - error: Sentinel must be assigned to a variable with the same name (reportGeneralTypeIssues)
22+
specialtypes_sentinels.py:11:5 - error: Sentinel must be assigned to a variable with the same name (reportGeneralTypeIssues)
23+
specialtypes_sentinels.py:20:20 - error: Variable not allowed in type expression (reportInvalidTypeForm)
24+
specialtypes_sentinels.py:22:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown | SPECIAL" (reportAssertTypeFailure)
25+
specialtypes_sentinels.py:22:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)
26+
specialtypes_sentinels.py:24:21 - error: "assert_type" mismatch: expected "int | SPECIAL" but received "int | Unknown | SPECIAL" (reportAssertTypeFailure)
27+
specialtypes_sentinels.py:26:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)
28+
specialtypes_sentinels.py:28:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure)
29+
specialtypes_sentinels.py:28:28 - error: Variable not allowed in type expression (reportInvalidTypeForm)
30+
specialtypes_sentinels.py:30:21 - error: "assert_type" mismatch: expected "int" but received "int | Unknown" (reportAssertTypeFailure)
31+
"""

conformance/results/results.html

Lines changed: 27 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
conformant = "Pass"
2+
conformance_automated = "Pass"
3+
errors_diff = """
4+
"""
5+
output = """
6+
specialtypes_sentinels.py:14:11: error[invalid-parameter-default] Default value of type `MISSING` is not assignable to annotated parameter type `int`
7+
specialtypes_sentinels.py:36:7: error[invalid-argument-type] Argument to function `func2` is incorrect: Expected `int | MISSING | SPECIAL`, found `IN_CLASS`
8+
specialtypes_sentinels.py:39:7: error[invalid-argument-type] Argument to function `func3` is incorrect: Expected `int | IN_CLASS`, found `MISSING`
9+
"""
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
conformant = "Pass"
2+
conformance_automated = "Pass"
3+
errors_diff = """
4+
"""
5+
output = """
6+
specialtypes_sentinels.py:14: error: Incompatible default for parameter "x" (default has type "Sentinel('<MISSING>')", parameter has type "int") [assignment]
7+
specialtypes_sentinels.py:36: error: Argument 1 to "func2" has incompatible type "Sentinel('Cls.IN_CLASS')"; expected "int | Sentinel('<MISSING>') | Sentinel('SPECIAL')" [arg-type]
8+
specialtypes_sentinels.py:39: error: Argument 1 to "func3" has incompatible type "Sentinel('<MISSING>')"; expected "int | Sentinel('Cls.IN_CLASS')" [arg-type]
9+
"""
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from typing_extensions import Sentinel, assert_type
2+
3+
# > Sentinel objects may be used in type annotations if they are defined using
4+
# > a simple assignment of the form ``NAME = sentinel('NAME')`` in the
5+
# > global scope or in a class body that is not within a function.
6+
7+
MISSING = Sentinel("<MISSING>") # name is not required to match the variable name
8+
SPECIAL = Sentinel("SPECIAL")
9+
10+
class Cls:
11+
IN_CLASS = Sentinel("Cls.IN_CLASS")
12+
13+
14+
def func1(x: int = MISSING) -> None: # E: incompatible default
15+
pass
16+
17+
# > Type checkers must support narrowing union types involving sentinels using the
18+
# > ``is`` and ``is not`` operators
19+
20+
def func2(x: int | MISSING | SPECIAL = MISSING) -> None:
21+
if x is MISSING:
22+
assert_type(x, MISSING)
23+
else:
24+
assert_type(x, int | SPECIAL)
25+
26+
def func3(x: int | Cls.IN_CLASS = Cls.IN_CLASS) -> None:
27+
if x is Cls.IN_CLASS:
28+
assert_type(x, Cls.IN_CLASS)
29+
else:
30+
assert_type(x, int)
31+
32+
33+
func2(1) # ok
34+
func2(MISSING) # ok
35+
func2(SPECIAL) # ok
36+
func2(Cls.IN_CLASS) # E: incompatible argument
37+
38+
func3(1) # ok
39+
func3(MISSING) # E: incompatible argument
40+
func3(Cls.IN_CLASS) # ok

docs/spec/annotations.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ The following grammar describes the allowed elements of type and annotation expr
139139
: | <None>
140140
: | name
141141
: (where name must refer to a valid in-scope class,
142-
: type alias, or TypeVar)
142+
: type alias, TypeVar, or sentinel object)
143143
: | name '[' (`maybe_unpacked` | `type_expression_list`)
144144
: (',' (`maybe_unpacked` | `type_expression_list`))* ']'
145145
: (the `type_expression_list` form is valid only when

docs/spec/special-types.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,46 @@ are highly dynamic.
5353
When used in a type hint, the expression ``None`` is considered
5454
equivalent to ``type(None)``.
5555

56+
.. _ `sentinels`:
57+
58+
Sentinels
59+
---------
60+
61+
Sentinel objects may be used in type annotations to represent themselves::
62+
63+
MISSING = sentinel('MISSING')
64+
OTHER = sentinel('OTHER')
65+
66+
def f(x: int | MISSING = MISSING) -> int:
67+
if x is MISSING:
68+
return 0
69+
return x
70+
71+
f(OTHER) # Error, OTHER is not an int or MISSING
72+
f(MISSING) # OK, MISSING is a valid argument
73+
74+
Sentinels may be created using the ``sentinel()`` built-in in Python 3.15
75+
and higher. ``typing_extensions`` provides a backport of this function. For
76+
historical reasons the object was first introduced under the name
77+
``typing_extensions.Sentinel``, and later ``typing_extensions.sentinel`` was
78+
added as an alias; type checkers should support both.
79+
80+
Sentinel objects may be used in type annotations if they are defined using
81+
a simple assignment of the form ``NAME = sentinel('NAME')`` in the
82+
global scope or in a class body that is not within a function. The name of the
83+
variable need not match the string argument passed to ``sentinel()`` but it is
84+
conventional to do so for names in the global scope.
85+
86+
Type checkers must support narrowing union types involving sentinels using the
87+
``is`` and ``is not`` operators::
88+
89+
def g(x: int | MISSING) -> None:
90+
if x is MISSING:
91+
assert_type(x, MISSING)
92+
else:
93+
assert_type(x, int)
94+
95+
5696
.. _`noreturn`:
5797

5898
``NoReturn``

0 commit comments

Comments
 (0)