From 0cabfec14e3911bc3860fe4df26d2c88364684f3 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 26 Jan 2026 15:48:19 -0800 Subject: [PATCH 01/14] Fix rst_replace_section to not blow away labels --- pre-pep.rst | 2 ++ scripts/rst_replace_section.py | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index c2e00a9..73be7f8 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -242,6 +242,8 @@ contains all the ``Property`` attributes of ``T``. The full test is `in our test suite <#qb-test_>`_. +.. _fastapi-impl: + Automatically deriving FastAPI CRUD models ------------------------------------------ diff --git a/scripts/rst_replace_section.py b/scripts/rst_replace_section.py index e35ebf7..0d43e7a 100755 --- a/scripts/rst_replace_section.py +++ b/scripts/rst_replace_section.py @@ -75,17 +75,33 @@ def replace_section(rst_content: str, label: str, new_content: str) -> str: section_level = get_section_level(lines[i + 1]) break - if section_title_idx is None: + if section_title_idx is None or section_underline_idx is None or section_level is None: raise ValueError(f"No section heading found after label '{label}'") # Find where the section ends (next section at same or higher level) section_end_idx = len(lines) + # Pattern to match RST labels like ".. _label-name:" or ".. _#label-name:" + label_pattern = re.compile(r'^\.\.\s+_#?[\w-]+:\s*$') + for i in range(section_underline_idx + 1, len(lines) - 1): if is_section_underline(lines[i + 1], lines[i]): next_level = get_section_level(lines[i + 1]) if next_level is not None and next_level <= section_level: + # Walk backward past blank lines and labels to find all labels + # that belong to this next section section_end_idx = i + + idx = i + while idx > section_underline_idx + 1: + prev_line = lines[idx - 1] + if label_pattern.match(prev_line): + idx -= 1 + section_end_idx = idx + elif not prev_line.strip(): + idx -= 1 + else: + break break # Build the result @@ -102,7 +118,7 @@ def replace_section(rst_content: str, label: str, new_content: str) -> str: # Add the new content result_lines.extend(new_content_lines) - # Add blank line before next section if needed + # Add blank line before next section if needed (when content doesn't end with one) if section_end_idx < len(lines): if result_lines and result_lines[-1].strip(): result_lines.append('') From f020f74ef8bff627c710ff05530610976611a63a Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 26 Jan 2026 15:26:14 -0800 Subject: [PATCH 02/14] Unevaluated types --- pre-pep.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index 73be7f8..1bca756 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -1013,8 +1013,18 @@ Open Issues * What invalid operations should be errors and what should return ``Never``? - -[Any points that are still being decided/discussed.] +What exactly are the subtyping (etc) rules for unevaluated types +---------------------------------------------------------------- + +Because of generic functions, there will be plenty of cases where we +can't evaluate a type operator (because it's applied to an unresolved +type variable), and exactly what the type evaluation rules should be +in those cases is somewhat unclear. + +Currently, in the proof of concept implementation in mypy, stuck type +evaluations implement subtype checking fully invariantly: we check +that the operators match and that every operand matches in both +arguments invariantly. Acknowledgements From 739a7b18ee2f56e3c0b366deb447b4b6cfd1e7f8 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 26 Jan 2026 15:32:23 -0800 Subject: [PATCH 03/14] Move all the example code into their own section --- pre-pep.rst | 387 +++++++++++++++++++++++++++------------------------- 1 file changed, 200 insertions(+), 187 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index 1bca756..e5154f5 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -124,123 +124,9 @@ which would have return type ``list[]`` where:: content: str -Unlike the FastAPI-style example above, we probably don't have too -much need for runtime introspection of the types here, which is good: -inferring the type of a function is much less likely to be feasible. +(Example code for implementing this :ref:`below `.) -.. _qb-impl: - -Implementation -'''''''''''''' - -This will take something of a tutorial approach in discussing the -implementation, and explain the features being used as we use -them. More details were appear in the specification section. - -First, to support the annotations we saw above, we have a collection -of dummy classes with generic types. - -:: - - class Pointer[T]: - pass - - class Property[T](Pointer[T]): - pass - - class Link[T](Pointer[T]): - pass - - class SingleLink[T](Link[T]): - pass - - class MultiLink[T](Link[T]): - pass - -The ``select`` method is where we start seeing new things. - -The ``**kwargs: Unpack[K]`` is part of this proposal, and allows -*inferring* a TypedDict from keyword args. - -``Attrs[K]`` extracts ``Member`` types corresponding to every -type-annotated attribute of ``K``, while calling ``NewProtocol`` with -``Member`` arguments constructs a new structural type. - -``GetName`` is a getter operator that fetches the name of a ``Member`` -as a literal type--all of these mechanisms lean very heavily on literal types. -``GetAttr`` gets the type of an attribute from a class. - -:: - - def select[ModelT, K: BaseTypedDict]( - typ: type[ModelT], - /, - **kwargs: Unpack[K], - ) -> list[ - NewProtocol[ - *[ - Member[ - GetName[c], - ConvertField[GetAttr[ModelT, GetName[c]]], - ] - for c in Iter[Attrs[K]] - ] - ] - ]: ... - -ConvertField is our first type helper, and it is a conditional type -alias, which decides between two types based on a (limited) -subtype-ish check. - -In ``ConvertField``, we wish to drop the ``Property`` or ``Link`` -annotation and produce the underlying type, as well as, for links, -producing a new target type containing only properties and wrapping -``MultiLink`` in a list. - -:: - - type ConvertField[T] = ( - AdjustLink[PropsOnly[PointerArg[T]], T] if IsSub[T, Link] else PointerArg[T] - ) - -``PointerArg`` gets the type argument to ``Pointer`` or a subclass. - -``GetArg[T, Base, I]`` is one of the core primitives; it fetches the -index ``I`` type argument to ``Base`` from a type ``T``, if ``T`` -inherits from ``Base``. - -(The subtleties of this will be discussed later; in this case, it just -grabs the argument to a ``Pointer``). - -:: - - type PointerArg[T: Pointer] = GetArg[T, Pointer, Literal[0]] - -``AdjustLink`` sticks a ``list`` around ``MultiLink``, using features -we've discussed already. - -:: - - type AdjustLink[Tgt, LinkTy] = list[Tgt] if IsSub[LinkTy, MultiLink] else Tgt - -And the final helper, ``PropsOnly[T]``, generates a new type that -contains all the ``Property`` attributes of ``T``. - -:: - - type PropsOnly[T] = list[ - NewProtocol[ - *[ - Member[GetName[p], PointerArg[GetType[p]]] - for p in Iter[Attrs[T]] - if IsSub[GetType[p], Property] - ] - ] - ] - -The full test is `in our test suite <#qb-test_>`_. - .. _fastapi-impl: @@ -336,41 +222,7 @@ runtime evaluation of the type annotations**. FastAPI uses the Pydantic models to validate and convert to/from JSON for both input and output from endpoints. - -Implementation -'''''''''''''' - -We have a more `fully-worked example <#fastapi-test_>`_ in our test -suite, but here is a possible implementation of just ``Public``:: - - # Extract the default type from an Init field. - # If it is a Field, then we try pulling out the "default" field, - # otherwise we return the type itself. - type GetDefault[Init] = ( - GetFieldItem[Init, Literal["default"]] if IsSub[Init, Field] else Init - ) - - # Create takes everything but the primary key and preserves defaults - type Create[T] = NewProtocol[ - *[ - Member[GetName[p], GetType[p], GetQuals[p], GetDefault[GetInit[p]]] - for p in Iter[Attrs[T]] - if not IsSub[ - Literal[True], GetFieldItem[GetInit[p], Literal["primary_key"]] - ] - ] - ] - -The ``Create`` type alias creates a new type (via ``NewProtocol``) by -iterating over the attributes of the original type. It has access to -names, types, qualifiers, and the literal types of initializers (in -part through new facilities to handle the extremely common -``= Field(...)`` like pattern used here. - -Here, we filter out attributes that have ``primary_key=True`` in their -``Field`` as well as extracting default arguments (which may be either -from a ``default`` argument to a field or specified directly as an -initializer). +(Example code for implementing this :ref:`below `.) dataclasses-style method generation @@ -386,43 +238,11 @@ This kind of pattern is widespread enough that :pep:`PEP 681 <681>` was created to represent a lowest-common denominator subset of what existing libraries do. -.. _init-impl: +Make it possible for libraries to implement more of these patterns +directly in the type system will give better typing without needing +futher special casing, typechecker plugins, hardcoded support, etc. -Implementation -'''''''''''''' - -:: - - # Generate the Member field for __init__ for a class - type InitFnType[T] = Member[ - Literal["__init__"], - Callable[ - [ - Param[Literal["self"], Self], - *[ - Param[ - GetName[p], - GetType[p], - # All arguments are keyword-only - # It takes a default if a default is specified in the class - Literal["keyword"] - if IsSub[ - GetDefault[GetInit[p]], - Never, - ] - else Literal["keyword", "default"], - ] - for p in Iter[Attrs[T]] - ], - ], - None, - ], - Literal["ClassVar"], - ] - type AddInit[T] = NewProtocol[ - InitFnType[T], - *[x for x in Iter[Members[T]]], - ] +(Example code for implementing this :ref:`below `.) Specification of Needed Preliminaries @@ -866,10 +686,203 @@ TODO: EXPLAIN .. _rt-support: - Runtime evaluation support -------------------------- + +Examples / Tutorial +=================== + +Here we will take something of a tutorial approach in discussing how +to achieve the goals in the examples in the motivation section, +explain the features being used as we use them. + +.. _qb-impl: + +Prisma-style ORMs +----------------- + +More details were appear in the specification section. + +First, to support the annotations we saw above, we have a collection +of dummy classes with generic types. + +:: + + class Pointer[T]: + pass + + class Property[T](Pointer[T]): + pass + + class Link[T](Pointer[T]): + pass + + class SingleLink[T](Link[T]): + pass + + class MultiLink[T](Link[T]): + pass + +The ``select`` method is where we start seeing new things. + +The ``**kwargs: Unpack[K]`` is part of this proposal, and allows +*inferring* a TypedDict from keyword args. + +``Attrs[K]`` extracts ``Member`` types corresponding to every +type-annotated attribute of ``K``, while calling ``NewProtocol`` with +``Member`` arguments constructs a new structural type. + +``GetName`` is a getter operator that fetches the name of a ``Member`` +as a literal type--all of these mechanisms lean very heavily on literal types. +``GetAttr`` gets the type of an attribute from a class. + +:: + + def select[ModelT, K: BaseTypedDict]( + typ: type[ModelT], + /, + **kwargs: Unpack[K], + ) -> list[ + NewProtocol[ + *[ + Member[ + GetName[c], + ConvertField[GetAttr[ModelT, GetName[c]]], + ] + for c in Iter[Attrs[K]] + ] + ] + ]: ... + +ConvertField is our first type helper, and it is a conditional type +alias, which decides between two types based on a (limited) +subtype-ish check. + +In ``ConvertField``, we wish to drop the ``Property`` or ``Link`` +annotation and produce the underlying type, as well as, for links, +producing a new target type containing only properties and wrapping +``MultiLink`` in a list. + +:: + + type ConvertField[T] = ( + AdjustLink[PropsOnly[PointerArg[T]], T] if IsSub[T, Link] else PointerArg[T] + ) + +``PointerArg`` gets the type argument to ``Pointer`` or a subclass. + +``GetArg[T, Base, I]`` is one of the core primitives; it fetches the +index ``I`` type argument to ``Base`` from a type ``T``, if ``T`` +inherits from ``Base``. + +(The subtleties of this will be discussed later; in this case, it just +grabs the argument to a ``Pointer``). + +:: + + type PointerArg[T: Pointer] = GetArg[T, Pointer, Literal[0]] + +``AdjustLink`` sticks a ``list`` around ``MultiLink``, using features +we've discussed already. + +:: + + type AdjustLink[Tgt, LinkTy] = list[Tgt] if IsSub[LinkTy, MultiLink] else Tgt + +And the final helper, ``PropsOnly[T]``, generates a new type that +contains all the ``Property`` attributes of ``T``. + +:: + + type PropsOnly[T] = list[ + NewProtocol[ + *[ + Member[GetName[p], PointerArg[GetType[p]]] + for p in Iter[Attrs[T]] + if IsSub[GetType[p], Property] + ] + ] + ] + +The full test is `in our test suite <#qb-test_>`_. + + +Automatically deriving FastAPI CRUD models +------------------------------------------ + +We have a more `fully-worked example <#fastapi-test_>`_ in our test +suite, but here is a possible implementation of just ``Public``:: + + # Extract the default type from an Init field. + # If it is a Field, then we try pulling out the "default" field, + # otherwise we return the type itself. + type GetDefault[Init] = ( + GetFieldItem[Init, Literal["default"]] if IsSub[Init, Field] else Init + ) + + # Create takes everything but the primary key and preserves defaults + type Create[T] = NewProtocol[ + *[ + Member[GetName[p], GetType[p], GetQuals[p], GetDefault[GetInit[p]]] + for p in Iter[Attrs[T]] + if not IsSub[ + Literal[True], GetFieldItem[GetInit[p], Literal["primary_key"]] + ] + ] + ] + +The ``Create`` type alias creates a new type (via ``NewProtocol``) by +iterating over the attributes of the original type. It has access to +names, types, qualifiers, and the literal types of initializers (in +part through new facilities to handle the extremely common +``= Field(...)`` like pattern used here. + +Here, we filter out attributes that have ``primary_key=True`` in their +``Field`` as well as extracting default arguments (which may be either +from a ``default`` argument to a field or specified directly as an +initializer). + + +.. _init-impl: + +dataclasses-style method generation +----------------------------------- + +:: + + # Generate the Member field for __init__ for a class + type InitFnType[T] = Member[ + Literal["__init__"], + Callable[ + [ + Param[Literal["self"], Self], + *[ + Param[ + GetName[p], + GetType[p], + # All arguments are keyword-only + # It takes a default if a default is specified in the class + Literal["keyword"] + if IsSub[ + GetDefault[GetInit[p]], + Never, + ] + else Literal["keyword", "default"], + ] + for p in Iter[Attrs[T]] + ], + ], + None, + ], + Literal["ClassVar"], + ] + type AddInit[T] = NewProtocol[ + InitFnType[T], + *[x for x in Iter[Members[T]]], + ] + + Rationale ========= From b5dbe9eaf44036e5c8a0ae3a34cf2f05df391f39 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 26 Jan 2026 16:06:54 -0800 Subject: [PATCH 04/14] Adjust the motivation --- pre-pep.rst | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index e5154f5..6f1b8a4 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -24,19 +24,26 @@ different conditions of Python typing. Motivation ========== -Python has a gradual type system, but at the heart of it is a fairly -conventional and tame static type system (apart from untagged union -types and type narrowing, which are common in gradual type systems but -not in traditional static ones). In Python as a language, on the -other hand, it is not unusual to perform complex metaprogramming, -especially at the library layer. - -Typically, type safety is lost when doing these sorts of things. Some -libraries come with custom mypy plugins, and a special-case -``@dataclass_transform`` decorator was added specifically to cover the -case of dataclass-like transformations (:pep:`PEP 681 <681>`). - -Examples: pydantic/fastapi, dataclasses, sqlalchemy +Python has a gradual type system, but at the heart of it is a *fairly* +conventional static type system. + +In Python as a language, on the other hand, it is not unusual to +perform complex metaprogramming, especially in libraries and +frameworks. The type system typically cannot model metaprogramming. + +To bridge the gap between metaprogramming and the type +system, some libraries come with custom mypy plugins (though then +other typechecker suffer). The case of dataclass-like transformations +was considered common enough that a special-case +``@dataclass_transform`` decorator was added specifically to cover +that case (:pep:`PEP 681 <681>`). + +We are proposing to add to the type system type manipulation +facilities that are more capable of keeping up with dynamic Python +code. + +We will present a few examples of problems that could be solved with +more powerful type manipulation. Prisma-style ORMs ----------------- From 3c4f256b7b3ec4b604766e7ec4c72538593c222c Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 26 Jan 2026 16:22:10 -0800 Subject: [PATCH 05/14] Add a note about runtime fastapi generation --- pre-pep.rst | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index 6f1b8a4..19c9ac4 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -219,16 +219,21 @@ Those types, evaluated, would look something like:: -While the implementation of ``Public``, ``Create``, and ``Update`` -(presented in the next subsection) are certainly more complex than -duplicating code would be, they perform quite mechanical operations -and could be included in the framework library. +While the implementation of ``Public``, ``Create``, and ``Update`` are +certainly more complex than duplicating code would be, they perform +quite mechanical operations and could be included in the framework +library. A notable feature of this use case is that it **depends on performing runtime evaluation of the type annotations**. FastAPI uses the Pydantic models to validate and convert to/from JSON for both input and output from endpoints. +Currently it is possible to do the runtime half of this: we could write +functions that generate Pydantic models at runtime based on whatever +rules we wished. But this is unsatisfying, because we would not be +able to properly statically typecheck the functions. + (Example code for implementing this :ref:`below `.) From cf36f073b4e9196ba97829f2337bf6b00ce8500d Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 26 Jan 2026 17:06:05 -0800 Subject: [PATCH 06/14] Fix label --- pre-pep.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index 19c9ac4..a4524d0 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -134,9 +134,6 @@ which would have return type ``list[]`` where:: (Example code for implementing this :ref:`below `.) - -.. _fastapi-impl: - Automatically deriving FastAPI CRUD models ------------------------------------------ @@ -820,6 +817,8 @@ contains all the ``Property`` attributes of ``T``. The full test is `in our test suite <#qb-test_>`_. +.. _fastapi-impl: + Automatically deriving FastAPI CRUD models ------------------------------------------ From 2becd7b83d3ac5cc280c7c3c85c9b78825c1a155 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Mon, 26 Jan 2026 17:07:15 -0800 Subject: [PATCH 07/14] Tweak --- pre-pep.rst | 2 -- tests/test_qblike_2.py | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index a4524d0..9df6a91 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -711,8 +711,6 @@ explain the features being used as we use them. Prisma-style ORMs ----------------- -More details were appear in the specification section. - First, to support the annotations we saw above, we have a collection of dummy classes with generic types. diff --git a/tests/test_qblike_2.py b/tests/test_qblike_2.py index e8f06f1..e75612f 100644 --- a/tests/test_qblike_2.py +++ b/tests/test_qblike_2.py @@ -21,11 +21,7 @@ # Begin PEP section: Prisma-style ORMs -"""This will take something of a tutorial approach in discussing the -implementation, and explain the features being used as we use -them. More details were appear in the specification section. - -First, to support the annotations we saw above, we have a collection +"""First, to support the annotations we saw above, we have a collection of dummy classes with generic types. """ From 106f49b362d320372fe03c5a6fd946f0ede23f37 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 27 Jan 2026 16:14:01 -0800 Subject: [PATCH 08/14] Some notes about typeddicts --- pre-pep.rst | 11 ++++++----- typemap/typing.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index 9df6a91..28684ef 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -476,7 +476,7 @@ Object inspection ''''''''''''''''' * ``Members[T]``: produces a ``tuple`` of ``Member`` types describing - the members (attributes and methods) of class ``T``. + the members (attributes and methods) of class or typed dict ``T``. In order to allow typechecking time and runtime evaluation coincide more closely, **only members with explicit type annotations are included**. @@ -495,11 +495,12 @@ Object inspection * ``Init`` is the literal type of the attribute initializer in the class (see :ref:`InitField `) * ``D`` is the defining class of the member. (That is, which class - the member is inherited from.) + the member is inherited from. Always ``Never``, for a ``TypedDict``) -* ``MemberQuals = Literal['ClassVar', 'Final']`` - ``MemberQuals`` is - the type of "qualifiers" that can apply to a member; currently - ``ClassVar`` and ``Final`` +* ``MemberQuals = Literal['ClassVar', 'Final', 'NotRequired, 'ReadOnly']`` - + ``MemberQuals`` is the type of "qualifiers" that can apply to a + member; currently ``ClassVar`` and ``Final`` apply to classes and + ``NotRequired``, and ``ReadOnly`` to typed dicts Methods are returned as callables using the new ``Param`` based diff --git a/typemap/typing.py b/typemap/typing.py index 021483d..689d08e 100644 --- a/typemap/typing.py +++ b/typemap/typing.py @@ -75,7 +75,7 @@ class DropAnnotations[T]: ### -MemberQuals = Literal["ClassVar", "Final"] +MemberQuals = Literal["ClassVar", "Final", "NotRequired", "ReadOnly"] class Member[ From a076ec185d3dd89703cbe8e00566d807fa5f56c1 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Tue, 27 Jan 2026 17:27:08 -0800 Subject: [PATCH 09/14] Document all the boolean/for stuff --- pre-pep.rst | 72 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 14 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index 28684ef..064490b 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -371,10 +371,10 @@ Note first that no changes to the **Python** grammar are being proposed, only to the grammar of what Python expressions are considered as valid types. -(It's also slightly imprecise to call this a grammar: where operator -names are mentioned directly, like ``IsSub``, they require that name -to be imported, and it could also be used qualified as -``typing.IsSub`` or imported as a different name.) +(It's also slightly imprecise to call this a grammar: +```` refers to any of the names defined in the +:ref:`Boolean Operators ` section, which might be +imported qualified or with some other name) :: @@ -390,18 +390,13 @@ to be imported, and it could also be used qualified as | [ +] # Type conditional checks are boolean compositions of - # "subtype checking" and boolean Literal type checking. + # boolean type operators = - IsSub[, ] - | Bool[] + [ +] | not | and | or - # Do we want these next two? Maybe not. - | Any[ +] - | All[ +] - = , | * , @@ -415,8 +410,46 @@ to be imported, and it could also be used qualified as if -TODO: explain conditional types and iteration +There are three core syntactic features introduced: type booleans, +conditional types and unpacked comprehension types. + +Type booleans +''''''''''''' + +Type booleans are a special subset of the type language that can be +used in the body of conditionals. They consist of the +:ref:`Boolean Operators `, defined below, +potentially combined with ``and``, ``or``, and ``not``. + +When evaluated, they will evaluate to ``Literal[True]`` or +``Literal[False]]``. + +(We want to restrict what operators may be used in a conditional +so that at runtime, we can have those operators produce "type" values +with appropriate behavior, without needing to change the behavior of +existing ``Literal[False]`` values and the like.) + + +Conditional types +''''''''''''''''' + +The type ``true_typ if bool_typ else false_typ`` is a conditional +type, which resolves to ``true_typ`` if ``bool_typ`` is equivalent to +``Literal[True]`` and to ``true_typ`` otherwise. + +``bool_typ`` is a type, but it needs syntactically be a type boolean, +defined above. + +Unpacked comprehension +'''''''''''''''''''''' +An unpacked comprehension, ``*[ty for t in Iter[iter_ty]]`` may appear +anywhere in a type that ``Unpack[...]`` is currently allowed, and it +evaluates essentially to an ``Unpack`` of a tuple produced by a list +comprehension iterating over the arguments of tuple type ``iter_ty``. + +The comprehension may also have ``if`` clauses, which filter in the +usual way. Type operators -------------- @@ -425,13 +458,24 @@ In some sections below we write things like ``Literal[int]]`` to mean "a literal that is of type ``int``". I don't think I'm really proposing to add that as a notion, but we could. -Boolean types -''''''''''''' +.. _boolean-ops: + +Boolean operators +''''''''''''''''' * ``IsSub[T, S]``: What we would **want** is that it returns a boolean literal type indicating whether ``T`` is a subtype of ``S``. To support runtime checking, we probably need something weaker. +* ``Bool[T]``: Returns ``Literal[True]`` if ``T`` is also + ``Literal[True]`` or a union containing it. + Equivalent to ``IsSub[T, Literal[True]] and not IsSub[T, Never]``. + +* ``Any[*Ts]``: Returns ``Literal[True]`` if any of ``Ts`` are true + (by the rule given in ``Bool``) and ``Literal[False]`` otherwise. + +* ``All[*Ts]``: Returns ``Literal[True]`` if all of ``Ts`` are true + (by the rule given in ``Bool``) and ``Literal[False]`` otherwise. Basic operators ''''''''''''''' From ea17616a3081469d377770f81b585183cadeeb6f Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 28 Jan 2026 10:42:46 -0800 Subject: [PATCH 10/14] Needs to be AnyOf and AllOf, add Matches --- pre-pep.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index 064490b..f8b2895 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -467,14 +467,19 @@ Boolean operators literal type indicating whether ``T`` is a subtype of ``S``. To support runtime checking, we probably need something weaker. + TODO: Discuss this in detail. + +* ``Matches[T, S]``: + Equivalent to ``IsSub[T, S] and IsSub[S, T]``. + * ``Bool[T]``: Returns ``Literal[True]`` if ``T`` is also ``Literal[True]`` or a union containing it. Equivalent to ``IsSub[T, Literal[True]] and not IsSub[T, Never]``. -* ``Any[*Ts]``: Returns ``Literal[True]`` if any of ``Ts`` are true +* ``AnyOf[*Ts]``: Returns ``Literal[True]`` if any of ``Ts`` are true (by the rule given in ``Bool``) and ``Literal[False]`` otherwise. -* ``All[*Ts]``: Returns ``Literal[True]`` if all of ``Ts`` are true +* ``AllOf[*Ts]``: Returns ``Literal[True]`` if all of ``Ts`` are true (by the rule given in ``Bool``) and ``Literal[False]`` otherwise. Basic operators From 9d8961099807c387c9ec8e80bdfce9946b549303 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 28 Jan 2026 14:05:45 -0800 Subject: [PATCH 11/14] Tweak how for is written in grammar a bit --- pre-pep.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index f8b2895..d41db97 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -399,10 +399,10 @@ imported qualified or with some other name) = , - | * , + | * [ ] , - = [ + * ] + = + * = # Iterate over a tuple type for in Iter[] From 92dff9c8ae45f958a1bdd25bf7a8386e13ac45dd Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 28 Jan 2026 14:11:26 -0800 Subject: [PATCH 12/14] Go back to allowing any/all --- pre-pep.rst | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index d41db97..854dc5f 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -396,6 +396,8 @@ imported qualified or with some other name) | not | and | or + | any() + | all() = , @@ -410,6 +412,9 @@ imported qualified or with some other name) if +(```` is identical to ```` except that the +result type is a ```` instead of a ````.) + There are three core syntactic features introduced: type booleans, conditional types and unpacked comprehension types. @@ -417,9 +422,11 @@ Type booleans ''''''''''''' Type booleans are a special subset of the type language that can be -used in the body of conditionals. They consist of the -:ref:`Boolean Operators `, defined below, -potentially combined with ``and``, ``or``, and ``not``. +used in the body of conditionals. They consist of the :ref:`Boolean +Operators `, defined below, potentially combined with +``and``, ``or``, ``not``, ``all``, and ``any``. For ``all`` and +``any``, the argument is a comprehension of type booleans, evaluated +in the same was as the :ref:`unpacked comprehensions `. When evaluated, they will evaluate to ``Literal[True]`` or ``Literal[False]]``. @@ -440,6 +447,8 @@ type, which resolves to ``true_typ`` if ``bool_typ`` is equivalent to ``bool_typ`` is a type, but it needs syntactically be a type boolean, defined above. +.. _unpacked: + Unpacked comprehension '''''''''''''''''''''' @@ -476,11 +485,8 @@ Boolean operators ``Literal[True]`` or a union containing it. Equivalent to ``IsSub[T, Literal[True]] and not IsSub[T, Never]``. -* ``AnyOf[*Ts]``: Returns ``Literal[True]`` if any of ``Ts`` are true - (by the rule given in ``Bool``) and ``Literal[False]`` otherwise. - -* ``AllOf[*Ts]``: Returns ``Literal[True]`` if all of ``Ts`` are true - (by the rule given in ``Bool``) and ``Literal[False]`` otherwise. + This is useful for invoking "helper aliases" that return a boolean + literal type. Basic operators ''''''''''''''' From c22722df5f5d051213311cd4d870b3eccaf90c0d Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 28 Jan 2026 14:24:39 -0800 Subject: [PATCH 13/14] Document lifting over unions --- pre-pep.rst | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/pre-pep.rst b/pre-pep.rst index 854dc5f..86ef04b 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -517,7 +517,8 @@ Basic operators (or ``Literal[None]`` if it is unbounded) -All of the operators in this section are "lifted" over union types. +All of the operators in this section are :ref:`lifted over union types +`. Union processing '''''''''''''''' @@ -579,7 +580,8 @@ with ``Param``, discussed below.) * ``GetInit[T: Member]`` * ``GetDefiner[T: Member]`` - +All of the operators in this section are :ref:`lifted over union types +`. (BUT TODO: should they be?) Object creation ''''''''''''''' @@ -593,7 +595,6 @@ Object creation similarly to ``NewProtocol`` but has different flags - .. _init-field: InitField @@ -701,7 +702,8 @@ for all unicode!). * ``Capitalize[S: Literal[str]]``: capitalize a string literal * ``Uncapitalize[S: Literal[str]]``: uncapitalize a string literal -All of the operators in this section are "lifted" over union types. +All of the operators in this section are :ref:`lifted over union types +`. Raise error ''''''''''' @@ -746,7 +748,20 @@ For example:: ) -TODO: EXPLAIN +When an operation is lifted over union types, we take the cross +product of the union elements for each argument position, evaluate the +operator for each tuple in the cross product, and then union all of +the results together. In Python, the logic looks like:: + + args_union_els = [get_union_elems(arg) for arg in args] + results = [ + eval_operator(*xs) + for xs in itertools.product(*args_union_els) + ] + if results: + return Union[*results] + else: + return Never .. _rt-support: From c7a30f88b5384fd12dd7fe540e55637bc73dcab1 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 28 Jan 2026 14:39:46 -0800 Subject: [PATCH 14/14] Document the runtime evaluation hook --- pre-pep.rst | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pre-pep.rst b/pre-pep.rst index 86ef04b..433d912 100644 --- a/pre-pep.rst +++ b/pre-pep.rst @@ -769,6 +769,30 @@ the results together. In Python, the logic looks like:: Runtime evaluation support -------------------------- +An important goal is supporting runtime evaluation of these computed +types. We do not propose to add an official evaluator to the standard +library, but intend to release a third-party evaluator library. + +While most of the extensions to the type system are "inert" type +operator applications, the syntax also includes list iteration and +conditionals, which will be automatically evaluated when the +``__annotate__`` method of a class, alias, or function is called. + +In order to allow an evaluator library to trigger type evaluation in +those cases, we add a new hook to ``typing``: + +* ``special_form_evaluator``: This is a ``ContextVar`` that holds a + callable that will be invoked with a ``typing._GenericAlias`` + argument when ``__bool__`` is called on a + :ref:`Boolean Operator ` or ``__iter__`` is called + on ``typing.Iter``. + The returned value will then have ``bool`` or ``iter`` called upon + it before being returned. + + If set to ``None`` (the default), the boolean operators will return + ``False`` while ``Iter`` will evaluate to + ``iter(typing.TypeVarTuple("_IterDummy"))``. + (TODO: Or should it be to ``iter([])``?) Examples / Tutorial ===================