diff --git a/conformance/results/mypy/specialtypes_sentinels.toml b/conformance/results/mypy/specialtypes_sentinels.toml new file mode 100644 index 00000000..1ff31a1c --- /dev/null +++ b/conformance/results/mypy/specialtypes_sentinels.toml @@ -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 +""" diff --git a/conformance/results/pycroscope/specialtypes_sentinels.toml b/conformance/results/pycroscope/specialtypes_sentinels.toml new file mode 100644 index 00000000..4c242538 --- /dev/null +++ b/conformance/results/pycroscope/specialtypes_sentinels.toml @@ -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[<>, ] but got [incompatible_argument] +./specialtypes_sentinels.py:39:6: Incompatible argument type for x: expected int | Literal[] but got <> [incompatible_argument] +""" diff --git a/conformance/results/pyrefly/specialtypes_sentinels.toml b/conformance/results/pyrefly/specialtypes_sentinels.toml new file mode 100644 index 00000000..0366c578 --- /dev/null +++ b/conformance/results/pyrefly/specialtypes_sentinels.toml @@ -0,0 +1,9 @@ +conformant = "Pass" +conformance_automated = "Pass" +errors_diff = """ +""" +output = """ +ERROR specialtypes_sentinels.py:14:20-27: Default `` 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 | | SPECIAL` in function `func2` [bad-argument-type] +ERROR specialtypes_sentinels.py:39:7-14: Argument `` is not assignable to parameter `x` with type `int | Cls.Cls.IN_CLASS` in function `func3` [bad-argument-type] +""" diff --git a/conformance/results/pyright/specialtypes_sentinels.toml b/conformance/results/pyright/specialtypes_sentinels.toml new file mode 100644 index 00000000..a2f0cea7 --- /dev/null +++ b/conformance/results/pyright/specialtypes_sentinels.toml @@ -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) +""" diff --git a/conformance/results/results.html b/conformance/results/results.html index 2c57ab56..44b7924b 100644 --- a/conformance/results/results.html +++ b/conformance/results/results.html @@ -465,6 +465,21 @@

Python Type System Conformance Test Results

Pass Pass + + specialtypes_sentinels + Unsupported + Pass + Pass + + Partial +
    +
  • Requires the string passed to Sentinel to match the assigned variable name.
  • +
  • This causes valid sentinel types and narrowing operations to be rejected.
  • +
+ + Pass + Pass + specialtypes_type @@ -488,12 +503,12 @@

Python Type System Conformance Test Results

- 4.5 / 5 • 90.0% - 4.5 / 5 • 90.0% - 5 / 5 • 100.0% - 5 / 5 • 100.0% - 4.5 / 5 • 90.0% - 5 / 5 • 100.0% + 4.5 / 6 • 75.0% + 5.5 / 6 • 91.7% + 6 / 6 • 100.0% + 5.5 / 6 • 91.7% + 5.5 / 6 • 91.7% + 6 / 6 • 100.0% @@ -2587,12 +2602,12 @@

Python Type System Conformance Test Results

- 109 / 141 • 77.3% - 130 / 141 • 92.2% - 140 / 141 • 99.3% - 136 / 141 • 96.5% - 125 / 141 • 88.7% - 140 / 141 • 99.3% + 109 / 142 • 76.8% + 131 / 142 • 92.3% + 141 / 142 • 99.3% + 136.5 / 142 • 96.1% + 126 / 142 • 88.7% + 141 / 142 • 99.3% diff --git a/conformance/results/ty/specialtypes_sentinels.toml b/conformance/results/ty/specialtypes_sentinels.toml new file mode 100644 index 00000000..0a2125c3 --- /dev/null +++ b/conformance/results/ty/specialtypes_sentinels.toml @@ -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` +""" diff --git a/conformance/results/zuban/specialtypes_sentinels.toml b/conformance/results/zuban/specialtypes_sentinels.toml new file mode 100644 index 00000000..fc3d0bca --- /dev/null +++ b/conformance/results/zuban/specialtypes_sentinels.toml @@ -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('')", parameter has type "int") [assignment] +specialtypes_sentinels.py:36: error: Argument 1 to "func2" has incompatible type "Sentinel('Cls.IN_CLASS')"; expected "int | Sentinel('') | Sentinel('SPECIAL')" [arg-type] +specialtypes_sentinels.py:39: error: Argument 1 to "func3" has incompatible type "Sentinel('')"; expected "int | Sentinel('Cls.IN_CLASS')" [arg-type] +""" diff --git a/conformance/tests/specialtypes_sentinels.py b/conformance/tests/specialtypes_sentinels.py new file mode 100644 index 00000000..73c25701 --- /dev/null +++ b/conformance/tests/specialtypes_sentinels.py @@ -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("") # name is not required to match the variable name +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 diff --git a/docs/spec/annotations.rst b/docs/spec/annotations.rst index 085d9277..558b9b70 100644 --- a/docs/spec/annotations.rst +++ b/docs/spec/annotations.rst @@ -139,7 +139,7 @@ The following grammar describes the allowed elements of type and annotation expr : | : | name : (where name must refer to a valid in-scope class, - : type alias, or TypeVar) + : type alias, TypeVar, or sentinel object) : | name '[' (`maybe_unpacked` | `type_expression_list`) : (',' (`maybe_unpacked` | `type_expression_list`))* ']' : (the `type_expression_list` form is valid only when diff --git a/docs/spec/special-types.rst b/docs/spec/special-types.rst index eab658b0..56e61daa 100644 --- a/docs/spec/special-types.rst +++ b/docs/spec/special-types.rst @@ -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. + +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``