Skip to content

Commit 14b3623

Browse files
1st1msullivan
andauthored
First batch of edits (#106)
Co-authored-by: Michael J. Sullivan <sully@msully.net>
1 parent 17155c4 commit 14b3623

1 file changed

Lines changed: 79 additions & 83 deletions

File tree

pep.rst

Lines changed: 79 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,17 @@ system, some libraries come with custom mypy plugins (though then
3333
other typecheckers suffer). The case of dataclass-like transformations
3434
was considered common enough that a special-case
3535
``@dataclass_transform`` decorator was added specifically to cover
36-
that case (:pep:`681`).
36+
that case (:pep:`681`). The problem with this approach is that many
37+
typecheckers do not (and will not) have a plugin API, so having
38+
consistent typechecking across IDEs, CI, and tooling is not
39+
achievable.
3740

38-
We are proposing to add type manipulation facilities to the type
39-
system that are more capable of keeping up with dynamic Python
40-
code.
41+
Given the significant mismatch between the expressiveness of
42+
the Python language and its type system, we propose to bridge
43+
this gap by adding type manipulation facilities that
44+
are better able to keep up with dynamic Python code.
4145

42-
There does seem to be demand for this. In the analysis of the
46+
There is demand for this. In the analysis of the
4347
responses to Meta's 2025 Typed Python Survey [#survey]_, the first
4448
entry on the list of "Most Requested Features" was:
4549

@@ -50,13 +54,15 @@ entry on the list of "Most Requested Features" was:
5054
dictionaries/dicts (e.g., more flexible TypedDict or anonymous types).
5155

5256
We will present a few examples of problems that could be solved with
53-
more powerful type manipulation.
57+
more powerful type manipulation, but the proposal is generic and will
58+
unlock many more use cases.
5459

5560
Prisma-style ORMs
5661
-----------------
5762

5863
`Prisma <#prisma_>`_, a popular ORM for TypeScript, allows writing
59-
queries like (adapted from `this example <#prisma-example_>`_)::
64+
database queries in TypeScript like
65+
(adapted from `this example <#prisma-example_>`_)::
6066

6167
const user = await prisma.user.findMany({
6268
select: {
@@ -66,7 +72,7 @@ queries like (adapted from `this example <#prisma-example_>`_)::
6672
},
6773
});
6874

69-
for which the inferred type will be something like::
75+
for which the inferred type of ``user`` will be something like::
7076

7177
{
7278
email: string;
@@ -79,15 +85,16 @@ for which the inferred type will be something like::
7985
}[];
8086
}[]
8187

82-
Here, the output type is a combination of both existing information
83-
about the type of ``prisma.user`` and the type of the argument to
84-
``findMany``. It returns an array of objects containing the properties
85-
of ``user`` that were requested; one of the requested elements,
86-
``posts``, is a "relation" referencing another model; it has *all* of
87-
its properties fetched but not its relations.
88+
Here, the output type is an intersection of the existing information
89+
about the type of ``prisma.user`` (a TypeScript type reflected from
90+
the database ``user`` table) and the type of the argument to
91+
the ``findMany`` method. It returns an array of objects containing
92+
the properties of ``user`` that were explicitly requested;
93+
where ``posts`` is a "relation" referencing another type.
8894

89-
We would like to be able to do something similar in Python, perhaps
90-
with a schema defined like::
95+
We would like to be able to do something similar in Python. Suppose
96+
our database schema is defined in Python (or code-generated from
97+
the database) like::
9198

9299
class Comment:
93100
id: Property[int]
@@ -112,11 +119,7 @@ with a schema defined like::
112119
email: Property[str]
113120
posts: Link[Post]
114121

115-
(In Prisma, a code generator generates type definitions based on a
116-
prisma schema in its own custom format; you could imagine something
117-
similar here, or that the definitions were hand-written)
118-
119-
and a call like::
122+
So, in Python code, a call like::
120123

121124
db.select(
122125
User,
@@ -125,7 +128,7 @@ and a call like::
125128
posts=True,
126129
)
127130

128-
which would have return type ``list[<User>]`` where::
131+
would have a dynamically computed return type ``list[<User>]`` where::
129132

130133
class <User>:
131134
name: str
@@ -137,6 +140,8 @@ which would have return type ``list[<User>]`` where::
137140
title: str
138141
content: str
139142

143+
Even further, an IDE could offer code completion for
144+
all arguments of the ``db.select()`` call, recursively.
140145

141146
(Example code for implementing this :ref:`below <qb-impl>`.)
142147

@@ -182,13 +187,14 @@ the types, using `Pydantic <#pydantic_>`_).
182187
Despite the multiple types and duplication here, mechanical rules
183188
could be written for deriving these types:
184189

185-
* Public should include all non-"hidden" fields, and the primary key
190+
* The "Public" version should include all non-"hidden" fields, and the primary key
186191
should be made non-optional
187-
* Create should include all fields except the primary key
188-
* Update should include all fields except the primary key, but they
192+
* "Create" should include all fields except the primary key
193+
* "Update" should include all fields except the primary key, but they
189194
should all be made optional and given a default value
190195

191-
With the definition of appropriate helpers, this proposal would allow writing::
196+
With the definition of appropriate helpers inside FastAPI framework,
197+
this proposal would allow its users to write::
192198

193199
class Hero(NewSQLModel, table=True):
194200
id: int | None = Field(default=None, primary_key=True)
@@ -222,13 +228,12 @@ Those types, evaluated, would look something like::
222228
secret_name: str | None = None
223229

224230

231+
While the implementation of ``Public[]``, ``Create[]``, and ``Update[]``
232+
computed types is relatively complex, they perform quite mechanical
233+
operations and if included in the framework library they would significantly
234+
reduce the boilerplate the users of FastAPI have to maintain.
225235

226-
While the implementation of ``Public``, ``Create``, and ``Update`` are
227-
certainly more complex than duplicating code would be, they perform
228-
quite mechanical operations and could be included in the framework
229-
library.
230-
231-
A notable feature of this use case is that it **depends on performing
236+
A notable feature of this use case is that it **requires performing
232237
runtime evaluation of the type annotations**. FastAPI uses the
233238
Pydantic models to validate and convert to/from JSON for both input
234239
and output from endpoints.
@@ -279,8 +284,6 @@ This proposal will cover those cases.
279284
Specification of Some Prerequisites
280285
===================================
281286

282-
(Some content is still in `spec-draft.rst <spec-draft.rst>`_).
283-
284287
We have two subproposals that are necessary to get mileage out of the
285288
main part of this proposal.
286289

@@ -337,10 +340,9 @@ read-only items are invariant.)
337340
This is potentially moderately useful on its own but is being done to
338341
support processing ``**kwargs`` with type level computation.
339342

340-
---
341343

342-
Extended Callables, take 2
343-
--------------------------
344+
Extended Callables
345+
------------------
344346

345347
We introduce a new extended callable proposal for expressing arbitrarily
346348
complex callable types. The goal here is not really to produce a new
@@ -411,9 +413,9 @@ or, using the type abbreviations we provide::
411413

412414
(Rationale discussed :ref:`below <callable-rationale>`.)
413415

414-
TODO: Should the extended argument list be wrapped in a
415-
``typing.Parameters[*Params]`` type (that will also kind of serve as a
416-
bound for ``ParamSpec``)?
416+
.. TODO: Should the extended argument list be wrapped in a
417+
.. ``typing.Parameters[*Params]`` type (that will also kind of serve as a
418+
.. bound for ``ParamSpec``)?
417419
418420
419421
Specification
@@ -427,14 +429,9 @@ forms of valid types, but much of the power comes from type level
427429
Grammar specification of the extensions to the type language
428430
------------------------------------------------------------
429431

430-
Note first that no changes to the **Python** grammar are being
431-
proposed, only to the grammar of what Python expressions are
432-
considered as valid types.
432+
No changes to the **Python** grammar are being proposed, only
433+
to the grammar of what Python expressions are considered as valid types.
433434

434-
(It's also slightly imprecise to call this a grammar:
435-
``<bool-operator>`` refers to any of the names defined in the
436-
:ref:`Boolean Operators <boolean-ops>` section, which might be
437-
imported qualified or with some other name)
438435

439436
::
440437

@@ -476,9 +473,14 @@ imported qualified or with some other name)
476473
<type-for-if> =
477474
if <type-bool>
478475

476+
Where:
479477

480-
(``<type-bool-for>`` is identical to ``<type-for>`` except that the
481-
result type is a ``<type-bool>`` instead of a ``<type>``.)
478+
* ``<bool-operator>`` refers to any of the names defined in the
479+
:ref:`Boolean Operators <boolean-ops>` section, whether used directly,
480+
qualified, or under another name.
481+
482+
* ``<type-bool-for>`` is identical to ``<type-for>`` except that the
483+
result type is a ``<type-bool>`` instead of a ``<type>``.
482484

483485
There are three and a half core syntactic features introduced: type booleans,
484486
conditional types, unpacked comprehension types, and type member access.
@@ -496,13 +498,13 @@ Operators <boolean-ops>`, defined below, potentially combined with
496498
``any``, the argument is a comprehension of type booleans, evaluated
497499
in the same way as the :ref:`unpacked comprehensions <unpacked>`.
498500

499-
When evaluated, they will evaluate to ``Literal[True]`` or
500-
``Literal[False]``.
501+
When evaluated in type annotation context, they will evaluate to
502+
``Literal[True]`` or ``Literal[False]``.
501503

502-
(We want to restrict what operators may be used in a conditional
504+
We restrict what operators may be used in a conditional
503505
so that at runtime, we can have those operators produce "type" values
504506
with appropriate behavior, without needing to change the behavior of
505-
existing ``Literal[False]`` values and the like.)
507+
existing ``Literal[False]`` values and the like.
506508

507509

508510
Conditional types
@@ -578,14 +580,12 @@ Basic operators
578580

579581
Negative indexes work in the usual way.
580582

581-
N.B: Runtime evaluation will only be able to support proper classes
583+
Note that runtime evaluation will only be able to support proper classes
582584
as ``Base``, *not* protocols. So, for example, ``GetArg[Ty,
583-
Iterable, 0]`` to get the type of something iterable will need to
584-
fail in a runtime evaluator. We should be able to allow it
585-
statically though.
585+
Iterable, 0]`` to get the type of something iterable will
586+
fail in the runtime evaluator.
586587

587-
Special forms unfortunately
588-
require some special handling: the arguments list of a ``Callable``
588+
Special forms require special handling: the arguments list of a ``Callable``
589589
will be packed in a tuple, and a ``...`` will become
590590
``SpecialFormEllipsis``.
591591

@@ -681,11 +681,11 @@ Object creation
681681
something of a new concept.)
682682

683683
* ``NewTypedDict[*Ps: Member]`` - Creates a new ``TypedDict`` with
684-
items specified by the ``Member`` arguments. TODO: Do we want a way
685-
to specify ``extra_items``?
686-
684+
items specified by the ``Member`` arguments.
687685

688-
N.B: Currently we aren't proposing any way to create nominal classes
686+
.. TODO: Do we want a way to specify ``extra_items``?
687+
688+
Note that we are not currently proposing any way to create *nominal* classes
689689
or any way to make new *generic* types.
690690

691691

@@ -696,7 +696,7 @@ InitField
696696

697697
We want to be able to support transforming types based on
698698
dataclasses/attrs/pydantic style field descriptors. In order to do
699-
that, we need to be able to consume things like calls to ``Field``.
699+
that, we need to be able to consume operations like calls to ``Field``.
700700

701701
Our strategy for this is to introduce a new type
702702
``InitField[KwargDict]`` that collects arguments defined by a
@@ -728,14 +728,14 @@ that would be made available as the ``Init`` field of the ``Member``.
728728
Annotated
729729
'''''''''
730730

731-
TODO: This could maybe be dropped if it doesn't seem implementable?
731+
.. TODO: This could maybe be dropped if it doesn't seem implementable?
732732
733733
Libraries like FastAPI use annotations heavily, and we would like to
734734
be able to use annotations to drive type-level computation decision
735735
making.
736736

737-
We understand that this may be controversial, as currently ``Annotated``
738-
may be fully ignored by typecheckers. The operations proposed are:
737+
Note that currently ``Annotated`` may be fully ignored by typecheckers.
738+
The operations proposed are:
739739

740740
* ``GetAnnotations[T]`` - Fetch the annotations of a potentially
741741
Annotated type, as Literals. Examples::
@@ -762,8 +762,7 @@ Callable format discussed above.
762762
The names, type, and qualifiers share associated type names with
763763
``Member`` (``.name``, ``.type``, and ``.quals``).
764764

765-
TODO: Should we make ``.init`` be literal types of default parameter
766-
values too?
765+
.. TODO: Should we make ``.init`` be literal types of default parameter values too?
767766
768767
.. _generic-callable:
769768

@@ -776,16 +775,13 @@ Generic Callable
776775
variables in ``Vs`` via the bound variables in ``<vs>``.
777776

778777
For now, we restrict the use of ``GenericCallable`` to
779-
the type argument of ``Member`` (that is, to disallow its use for
778+
the type argument of ``Member``, to disallow its use for
780779
locals, parameter types, return types, nested inside other types,
781-
etc).
780+
etc. Rationale discussed :ref:`below <generic-callable-rationale>`.
782781

783-
(This is a little unsatisfying. Rationale discussed :ref:`below
784-
<generic-callable-rationale>`.)
785-
786-
TODO: Decide if we have any mechanisms to inspect/destruct
787-
``GenericCallable``. Maybe can fetch the variable information and
788-
maybe can apply it to concrete types?
782+
.. TODO: Decide if we have any mechanisms to inspect/destruct
783+
.. ``GenericCallable``. Maybe can fetch the variable information and
784+
.. maybe can apply it to concrete types?
789785
790786
Overloaded function types
791787
'''''''''''''''''''''''''
@@ -797,8 +793,8 @@ String manipulation
797793
'''''''''''''''''''
798794

799795
String manipulation operations for string ``Literal`` types.
800-
We can put more in, but this is what typescript has.
801-
``Slice`` and ``Concat`` are a poor man's literal template.
796+
797+
``Slice`` and ``Concat`` allow for basic literal template-like manipulation.
802798
We can actually implement the case functions in terms of them and a
803799
bunch of conditionals, but shouldn't (especially if we want it to work
804800
for all unicode!).
@@ -1151,9 +1147,9 @@ I am proposing a fully new extended callable syntax because:
11511147
2. They use parentheses and not brackets, which really goes against
11521148
the philosophy here.
11531149
3. We can make an API that more nicely matches what we are going to
1154-
do for inspecting members (We could introduce extended callables that
1150+
do for inspecting members (we could introduce extended callables that
11551151
closely mimic the ``mypy_extensions`` version though, if something new
1156-
is a non starter)
1152+
is a non-starter)
11571153

11581154
TODO: Currently I made the qualifiers be short strings, for code brevity
11591155
when using them, but an alternate approach would be to mirror
@@ -1600,10 +1596,10 @@ situation at lower cost.
16001596
Make the type-level operations more "strictly-typed"
16011597
----------------------------------------------------
16021598

1603-
This proposal is less "strictly-typed" than typescript
1599+
This proposal is less "strictly-typed" than TypeScript
16041600
(strictly-kinded, maybe?).
16051601

1606-
Typescript has better typechecking at the alias definition site:
1602+
TypeScript has better typechecking at the alias definition site:
16071603
For ``P[K]``, ``K`` needs to have ``keyof P``...
16081604

16091605
We could do potentially better but it would require more machinery.

0 commit comments

Comments
 (0)