Skip to content

Commit d9961b2

Browse files
committed
Add sentinels (PEP 661) to the spec
1 parent 6483940 commit d9961b2

9 files changed

Lines changed: 201 additions & 1 deletion

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
conformant = "Unsupported"
2+
conformance_automated = "Fail"
3+
errors_diff = """
4+
Line 33: Expected 1 errors
5+
Line 36: Expected 1 errors
6+
Line 18: Unexpected errors ['specialtypes_sentinels.py:18: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type]']
7+
Line 20: Unexpected errors ['specialtypes_sentinels.py:20: error: Expression is of type "Any", not MISSING? [assert-type]', 'specialtypes_sentinels.py:20: 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.Cls.IN_CLASS" is not valid as a type [valid-type]']
9+
Line 26: Unexpected errors ['specialtypes_sentinels.py:26: error: Expression is of type "Any", not Cls.IN_CLASS? [assert-type]', 'specialtypes_sentinels.py:26: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type]']
10+
"""
11+
output = """
12+
specialtypes_sentinels.py:15: error: Incompatible default for parameter "x" (default has type "Sentinel", parameter has type "int") [assignment]
13+
specialtypes_sentinels.py:18: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type]
14+
specialtypes_sentinels.py:18: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
15+
specialtypes_sentinels.py:20: error: Expression is of type "Any", not MISSING? [assert-type]
16+
specialtypes_sentinels.py:20: error: Variable "specialtypes_sentinels.MISSING" is not valid as a type [valid-type]
17+
specialtypes_sentinels.py:20: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
18+
specialtypes_sentinels.py:24: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type]
19+
specialtypes_sentinels.py:24: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
20+
specialtypes_sentinels.py:26: error: Expression is of type "Any", not Cls.IN_CLASS? [assert-type]
21+
specialtypes_sentinels.py:26: error: Variable "specialtypes_sentinels.Cls.IN_CLASS" is not valid as a type [valid-type]
22+
specialtypes_sentinels.py:26: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
23+
"""
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
conformant = "Unsupported"
2+
conformance_automated = "Fail"
3+
errors_diff = """
4+
Line 33: Expected 1 errors
5+
Line 36: Expected 1 errors
6+
Line 18: Unexpected errors ['Expected a type form, got instance of `type[int] | UnionType` [not-a-type]']
7+
Line 20: Unexpected errors ['assert_type(Sentinel, Unknown) failed [assert-type]', 'Expected a type form, got instance of `Sentinel` [not-a-type]']
8+
Line 22: Unexpected errors ['assert_type(Unknown, int) failed [assert-type]']
9+
Line 24: Unexpected errors ['Expected a type form, got instance of `type[int] | UnionType` [not-a-type]']
10+
Line 26: Unexpected errors ['assert_type(Sentinel, Unknown) failed [assert-type]', 'Expected a type form, got instance of `Sentinel` [not-a-type]']
11+
Line 28: Unexpected errors ['assert_type(Unknown, int) failed [assert-type]']
12+
"""
13+
output = """
14+
ERROR specialtypes_sentinels.py:15:20-27: Default `Sentinel` is not assignable to parameter `x` with type `int` [bad-function-definition]
15+
ERROR specialtypes_sentinels.py:18:14-27: Expected a type form, got instance of `type[int] | UnionType` [not-a-type]
16+
ERROR specialtypes_sentinels.py:20:20-32: assert_type(Sentinel, Unknown) failed [assert-type]
17+
ERROR specialtypes_sentinels.py:20:24-31: Expected a type form, got instance of `Sentinel` [not-a-type]
18+
ERROR specialtypes_sentinels.py:22:20-28: assert_type(Unknown, int) failed [assert-type]
19+
ERROR specialtypes_sentinels.py:24:14-32: Expected a type form, got instance of `type[int] | UnionType` [not-a-type]
20+
ERROR specialtypes_sentinels.py:26:20-37: assert_type(Sentinel, Unknown) failed [assert-type]
21+
ERROR specialtypes_sentinels.py:26:24-36: Expected a type form, got instance of `Sentinel` [not-a-type]
22+
ERROR specialtypes_sentinels.py:28:20-28: assert_type(Unknown, int) failed [assert-type]
23+
"""
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
conformant = "Unsupported"
2+
conformance_automated = "Fail"
3+
errors_diff = """
4+
Line 33: Expected 1 errors
5+
Line 36: Expected 1 errors
6+
Line 18: Unexpected errors ['specialtypes_sentinels.py:18:20 - error: Variable not allowed in type expression (reportInvalidTypeForm)']
7+
Line 20: Unexpected errors ['specialtypes_sentinels.py:20:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure)', 'specialtypes_sentinels.py:20:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)']
8+
Line 22: Unexpected errors ['specialtypes_sentinels.py:22:21 - error: "assert_type" mismatch: expected "int" but received "int | Unknown" (reportAssertTypeFailure)']
9+
Line 24: Unexpected errors ['specialtypes_sentinels.py:24:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)']
10+
Line 26: Unexpected errors ['specialtypes_sentinels.py:26:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure)', 'specialtypes_sentinels.py:26:28 - error: Variable not allowed in type expression (reportInvalidTypeForm)']
11+
Line 28: Unexpected errors ['specialtypes_sentinels.py:28:21 - error: "assert_type" mismatch: expected "int" but received "int | Unknown" (reportAssertTypeFailure)']
12+
"""
13+
output = """
14+
specialtypes_sentinels.py:15:20 - error: Expression of type "Sentinel" cannot be assigned to parameter of type "int"
15+
  "Sentinel" is not assignable to "int" (reportArgumentType)
16+
specialtypes_sentinels.py:18:20 - error: Variable not allowed in type expression (reportInvalidTypeForm)
17+
specialtypes_sentinels.py:20:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure)
18+
specialtypes_sentinels.py:20:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)
19+
specialtypes_sentinels.py:22:21 - error: "assert_type" mismatch: expected "int" but received "int | Unknown" (reportAssertTypeFailure)
20+
specialtypes_sentinels.py:24:24 - error: Variable not allowed in type expression (reportInvalidTypeForm)
21+
specialtypes_sentinels.py:26:21 - error: "assert_type" mismatch: expected "Unknown" but received "int | Unknown" (reportAssertTypeFailure)
22+
specialtypes_sentinels.py:26:28 - error: Variable not allowed in type expression (reportInvalidTypeForm)
23+
specialtypes_sentinels.py:28:21 - error: "assert_type" mismatch: expected "int" but received "int | Unknown" (reportAssertTypeFailure)
24+
"""

conformance/results/results.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,13 @@ <h3>Python Type System Conformance Test Results</h3>
262262
<th class="column col2 conformant">Pass</th>
263263
<th class="column col2 conformant">Pass</th>
264264
</tr>
265+
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;specialtypes_sentinels</th>
266+
<th class="column col2 not-conformant">Unsupported</th>
267+
<th class="column col2 not-conformant">Unsupported</th>
268+
<th class="column col2 not-conformant">Unsupported</th>
269+
<th class="column col2 not-conformant">Unsupported</th>
270+
<th class="column col2 not-conformant">Unsupported</th>
271+
</tr>
265272
<tr><th class="column col1">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;specialtypes_type</th>
266273
<th class="column col2 partially-conformant"><div class="hover-text">Partial<span class="tooltip-text" id="bottom"><p>Does not treat `type` same as `type[Any]` for assert_type.</p><p>Does not allow access to unknown attributes from object of type `type[Any]`.</p></span></div></th>
267274
<th class="column col2 conformant">Pass</th>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
conformant = "Unsupported"
2+
conformance_automated = "Fail"
3+
errors_diff = """
4+
Line 33: Expected 1 errors
5+
Line 36: Expected 1 errors
6+
Line 18: Unexpected errors ['specialtypes_sentinels.py:18:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation']
7+
Line 20: Unexpected errors ['specialtypes_sentinels.py:20:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo`']
8+
Line 22: Unexpected errors ['specialtypes_sentinels.py:22:9: error[type-assertion-failure] Type `int | Unknown` does not match asserted type `int`']
9+
Line 24: Unexpected errors ['specialtypes_sentinels.py:24:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation']
10+
Line 26: Unexpected errors ['specialtypes_sentinels.py:26:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo`']
11+
Line 28: Unexpected errors ['specialtypes_sentinels.py:28:9: error[type-assertion-failure] Type `int | Unknown` does not match asserted type `int`']
12+
"""
13+
output = """
14+
specialtypes_sentinels.py:15:11: error[invalid-parameter-default] Default value of type `Sentinel` is not assignable to annotated parameter type `int`
15+
specialtypes_sentinels.py:18:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation
16+
specialtypes_sentinels.py:20:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo`
17+
specialtypes_sentinels.py:22:9: error[type-assertion-failure] Type `int | Unknown` does not match asserted type `int`
18+
specialtypes_sentinels.py:24:20: error[invalid-type-form] Variable of type `Sentinel` is not allowed in a parameter annotation
19+
specialtypes_sentinels.py:26:9: error[type-assertion-failure] Type `(int & Sentinel) | (Unknown & Sentinel)` does not match asserted type `@Todo`
20+
specialtypes_sentinels.py:28:9: error[type-assertion-failure] Type `int | Unknown` does not match asserted type `int`
21+
"""
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
conformant = "Unsupported"
2+
conformance_automated = "Fail"
3+
errors_diff = """
4+
Line 33: Expected 1 errors
5+
Line 36: Expected 1 errors
6+
Line 18: Unexpected errors ['specialtypes_sentinels.py:18: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type]']
7+
Line 20: Unexpected errors ['specialtypes_sentinels.py:20: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type]']
8+
Line 22: Unexpected errors ['specialtypes_sentinels.py:22: error: Expression is of type "int | Any", not "int" [misc]']
9+
Line 24: Unexpected errors ['specialtypes_sentinels.py:24: error: Variable "tests.specialtypes_sentinels.IN_CLASS" is not valid as a type [valid-type]']
10+
Line 26: Unexpected errors ['specialtypes_sentinels.py:26: error: Variable "tests.specialtypes_sentinels.IN_CLASS" is not valid as a type [valid-type]']
11+
Line 28: Unexpected errors ['specialtypes_sentinels.py:28: error: Expression is of type "int | Any", not "int" [misc]']
12+
"""
13+
output = """
14+
specialtypes_sentinels.py:15: error: Incompatible default for argument "x" (default has type "Sentinel", argument has type "int") [assignment]
15+
specialtypes_sentinels.py:18: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type]
16+
specialtypes_sentinels.py:18: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
17+
specialtypes_sentinels.py:20: error: Variable "tests.specialtypes_sentinels.MISSING" is not valid as a type [valid-type]
18+
specialtypes_sentinels.py:20: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#variables-vs-type-aliases
19+
specialtypes_sentinels.py:22: error: Expression is of type "int | Any", not "int" [misc]
20+
specialtypes_sentinels.py:24: error: Variable "tests.specialtypes_sentinels.IN_CLASS" 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 "tests.specialtypes_sentinels.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 "int | Any", not "int" [misc]
25+
"""
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from unittest.mock import sentinel
2+
3+
from typing_extensions import Sentinel, assert_type
4+
5+
# > Sentinel objects may be used in type annotations if they are defined using
6+
# > a simple assignment of the form ``NAME = sentinel('NAME')`` in the
7+
# > global scope or in a class body that is not within a function.
8+
9+
MISSING = Sentinel("MISSING")
10+
11+
class Cls:
12+
IN_CLASS = Sentinel("Cls.IN_CLASS")
13+
14+
15+
def func1(x: int = MISSING) -> None: # E: incompatible default
16+
pass
17+
18+
def func2(x: int | MISSING = MISSING) -> None:
19+
if x is MISSING:
20+
assert_type(x, MISSING)
21+
else:
22+
assert_type(x, int)
23+
24+
def func3(x: int | Cls.IN_CLASS = Cls.IN_CLASS) -> None:
25+
if x is Cls.IN_CLASS:
26+
assert_type(x, Cls.IN_CLASS)
27+
else:
28+
assert_type(x, int)
29+
30+
31+
func2(1) # ok
32+
func2(MISSING) # ok
33+
func2(Cls.IN_CLASS) # E: incompatible argument
34+
35+
func3(1) # ok
36+
func3(MISSING) # E: incompatible argument
37+
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`` or ``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)