Skip to content

Commit e5ded19

Browse files
authored
Lots more tweaks to the PEP (#104)
1 parent 93f91ab commit e5ded19

2 files changed

Lines changed: 112 additions & 58 deletions

File tree

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1-
# Computed Types in Python
1+
# Type Manipulation in Python
22

3-
See [pep.rst](pep.rst) for the PEP draft.
3+
This is the development repository for a PEP to add
4+
TypeScript-inspired type-level introspection and construction
5+
facilities to the Python type system.
6+
7+
There is [a rendered PEP draft](https://python-typemap.labs.vercel.dev).
8+
9+
See [pep.rst](pep.rst) for the PEP draft source code.
10+
11+
This repository also contains an implementation of the proposed
12+
additions to ``typing`` ([typemap/typing.py](typemap/typing.py)), as well as a
13+
**prototype** runtime evaluator ([typemap/type_eval](typemap/type_eval)).
414

515
## Development
616

pep.rst

Lines changed: 100 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ Extended Callables, take 2
345345
We introduce a new extended callable proposal for expressing arbitrary
346346
complex callable types. The goal here is not really to produce a new
347347
syntax to write in annotations (it seems less pleasant to write than
348-
callback protocols are), but to provide a way of contructing the types
348+
callback protocols are), but to provide a way of constructing the types
349349
that is amenable to creating and introspecting callable types using
350350
the other features of this PEP.
351351

@@ -512,7 +512,7 @@ The type ``true_typ if bool_typ else false_typ`` is a conditional
512512
type, which resolves to ``true_typ`` if ``bool_typ`` is equivalent to
513513
``Literal[True]`` and to ``false_typ`` otherwise.
514514

515-
``bool_typ`` is a type, but it needs syntactically be a type boolean,
515+
``bool_typ`` is a type, but it needs to syntactically be a type boolean,
516516
defined above.
517517

518518
.. _unpacked:
@@ -628,7 +628,7 @@ Object inspection
628628
* ``Members[T]``: produces a ``tuple`` of ``Member`` types describing
629629
the members (attributes and methods) of class or typed dict ``T``.
630630

631-
In order to allow typechecking time and runtime evaluation coincide
631+
In order to allow typechecking time and runtime evaluation to coincide
632632
more closely, **only members with explicit type annotations are included**.
633633

634634
* ``Attrs[T]``: like ``Members[T]`` but only returns attributes (not
@@ -642,14 +642,14 @@ Object inspection
642642
of classes. Its type parameters encode the information about each
643643
member.
644644

645-
* ``N`` is the name, as a literal string type. Accessable with ``.name``.
646-
* ``T`` is the type. Accessable with ``.type``.
647-
* ``Q`` is a union of qualifiers (see ``MemberQuals`` below). Accessable with ``.quals``.
645+
* ``N`` is the name, as a literal string type. Accessible with ``.name``.
646+
* ``T`` is the type. Accessible with ``.type``.
647+
* ``Q`` is a union of qualifiers (see ``MemberQuals`` below). Accessible with ``.quals``.
648648
* ``Init`` is the literal type of the attribute initializer in the
649-
class (see :ref:`InitField <init-field>`). Accessable with ``.init``.
649+
class (see :ref:`InitField <init-field>`). Accessible with ``.init``.
650650
* ``D`` is the defining class of the member. (That is, which class
651651
the member is inherited from. Always ``Never``, for a ``TypedDict``).
652-
Accessable with ``.definer``.
652+
Accessible with ``.definer``.
653653

654654
* ``MemberQuals = Literal['ClassVar', 'Final', 'NotRequired', 'ReadOnly']`` -
655655
``MemberQuals`` is the type of "qualifiers" that can apply to a
@@ -673,7 +673,12 @@ Object creation
673673
specified by ``Member`` arguments
674674

675675
* ``NewProtocolWithBases[Bases: tuple[type], *Ms: Member]`` - A variant that
676-
allows specifying bases too. TODO: Is this something we actually want?
676+
allows specifying bases too. The idea is that a type would satisfy
677+
this protocol if it extends all of the given bases and has the
678+
specified members. (TODO: Is this something we actually
679+
want? It would would be a potentially powerful feature for dealing
680+
with things like Pydantic models, but protocol-with-bases would be
681+
something of a new concept.)
677682

678683
* ``NewTypedDict[*Ps: Member]`` - Creates a new ``TypedDict`` with
679684
items specified by the ``Member`` arguments. TODO: Do we want a way
@@ -837,7 +842,7 @@ base classes and type decorators that do ``dataclass`` like things.
837842
When a class is declared, if one or more of its ancestors have an
838843
``__init_subclass__`` with an ``UpdateClass`` return type, they are
839844
applied in reverse MRO order. N.B: If the ``cls`` param is
840-
parameterized by ``type[T]]``, then the class type should
845+
parameterized by ``type[T]``, then the class type should be
841846
substituted in for ``T``.
842847

843848
One snag here: it introduces type-evaluation-order dependence; if the
@@ -886,13 +891,14 @@ Runtime evaluation support
886891
--------------------------
887892

888893
An important goal is supporting runtime evaluation of these computed
889-
types. We do not propose to add an official evaluator to the standard
894+
types. We **do not** propose to add an official evaluator to the standard
890895
library, but intend to release a third-party evaluator library.
891896

892897
While most of the extensions to the type system are "inert" type
893-
operator applications, the syntax also includes list iteration and
894-
conditionals, which will be automatically evaluated when the
895-
``__annotate__`` method of a class, alias, or function is called.
898+
operator applications, the syntax also includes list iteration,
899+
conditionals, and attribute access, which will be automatically
900+
evaluated when the ``__annotate__`` method of a class, alias, or
901+
function is called.
896902

897903
In order to allow an evaluator library to trigger type evaluation in
898904
those cases, we add a new hook to ``typing``:
@@ -1073,7 +1079,7 @@ The ``Create`` type alias creates a new type (via ``NewProtocol``) by
10731079
iterating over the attributes of the original type. It has access to
10741080
names, types, qualifiers, and the literal types of initializers (in
10751081
part through new facilities to handle the extremely common
1076-
``= Field(...)`` like pattern used here.
1082+
``= Field(...)``-like pattern used here).
10771083

10781084
Here, we filter out attributes that have ``primary_key=True`` in their
10791085
``Field`` as well as extracting default arguments (which may be either
@@ -1149,6 +1155,10 @@ I am proposing a fully new extended callable syntax because:
11491155
closely mimic the ``mypy_extensions`` version though, if something new
11501156
is a non starter)
11511157

1158+
TODO: Currently I made the qualifiers be short strings, for code brevity
1159+
when using them, but an alternate approach would be to mirror
1160+
``inspect.Signature`` more directly, and have an enum with names like
1161+
``ParamKind.POSITIONAL_OR_KEYWORD``.
11521162

11531163
.. _generic-callable-rationale:
11541164

@@ -1162,7 +1172,7 @@ Consider a method with the following signature::
11621172

11631173
The type of the method is generic, and the generic is bound at the
11641174
**method**, not the class. We need a way to represent such a generic
1165-
function both as a programmer might write it for a ``NewProtocol``.
1175+
function as a programmer might write it for a ``NewProtocol``.
11661176

11671177
One option that is somewhat appealing but doesn't work would be to use
11681178
unbound type variables and let them be generalized::
@@ -1226,6 +1236,9 @@ like mapped types are unmentioned in current documentation
12261236
Reference Implementation
12271237
========================
12281238

1239+
There is a demo of a runtime evaluator [#runtime]_, which is
1240+
also where this PEP draft currently lives.
1241+
12291242
There is an in-progress proof-of-concept implementation in mypy [#ref-impl]_.
12301243

12311244
It can type check the ORM and FastAPI-style model derivation
@@ -1234,8 +1247,6 @@ examples.
12341247
It is missing support for callables, ``UpdateClass``, annotation
12351248
processing, and various smaller things.
12361249

1237-
There is a demo of a runtime evaluator as well [#runtime]_.
1238-
12391250
Alternate syntax ideas
12401251
======================
12411252

@@ -1342,7 +1353,7 @@ The main proposal is currently silent about exactly *how* ``Member``
13421353
and ``Param`` will have associated types for ``.name`` and ``.type``.
13431354

13441355
We could just make it work for those particular types, or we could
1345-
introduce a general mechansim that might look something like::
1356+
introduce a general mechanism that might look something like::
13461357

13471358
@typing.has_associated_types
13481359
class Member[
@@ -1374,15 +1385,48 @@ Rejected Ideas
13741385
Renounce all cares of runtime evaluation
13751386
----------------------------------------
13761387

1377-
This would have a lot of simplifying features.
1388+
This would give us more flexibility to experiment with syntactic
1389+
forms, and would allow us to dispense with some ugliness such as
1390+
requiring ``typing.Iter`` in unpacked comprehension types and having a
1391+
limited set of ``<type-bool>`` expressions that can appear in
1392+
conditional types.
13781393

1379-
TODO: Expand
1394+
For better or worse, though, runtime use of type annotations is
1395+
widespread, and one of our motivating examples (automatically deriving
1396+
FastAPI CRUD models) depends on it.
13801397

13811398
Support TypeScript style pattern matching in subtype checking
13821399
-------------------------------------------------------------
13831400

1384-
This would almost certainly only be possible if we also decide not to
1385-
care about runtime evaluation, as above.
1401+
In TypeScript, conditional types are formed like::
1402+
1403+
SomeType extends OtherType ? TrueType : FalseType
1404+
1405+
What's more, the right hand side of the check allows binding type
1406+
variables based on pattern matching, using the ``infer`` keyword, like
1407+
this example that extracts the element type of an array::
1408+
1409+
type ArrayArg<T> = T extends [infer El] ? El : never;
1410+
1411+
This is a very elegant mechanism, especially in the way that it
1412+
eliminates the need for ``typing.GetArg`` and its subtle ``Base``
1413+
parameter.
1414+
1415+
Unfortunately it seems very difficult to shoehorn into Python's
1416+
existing syntax in any sort of satisfactory way, especially because of
1417+
the subtle binding structure.
1418+
1419+
Perhaps the most plausible variant would be something like::
1420+
1421+
type ArrayArg[T] = El if IsAssignable[T, list[Infer[El]]] else Never
1422+
1423+
Then, if we wanted to evaluate it at runtime, we'd need to do
1424+
something gnarly involving a custom ``globals`` environment that
1425+
catches the unbound ``Infer`` arguments.
1426+
1427+
Additionally, without major syntactic changes (using type operators
1428+
instead of ternary), we wouldn't be able to match TypeScript's
1429+
behavior of lifting the conditional over unions.
13861430

13871431

13881432
Replace ``IsAssignable`` with something weaker than "assignable to" checking
@@ -1410,6 +1454,34 @@ that is similar to but not the same as subtyping, and that would need
14101454
to either have a long and weird name like ``IsAssignableSimilar`` or a
14111455
misleading short one like ``IsAssignable``.
14121456

1457+
1458+
Don't use dot notation to access ``Member`` components
1459+
------------------------------------------------------
1460+
1461+
Earlier versions of this PEP draft omitted the ability to write
1462+
``m.name`` and similar on ``Member`` and ``Param`` components, and
1463+
instead relied on helper operators such as ``typing.GetName`` (that
1464+
could be implemented under the hood using ``typing.GetArg`` or
1465+
``typing.GetMemberType``).
1466+
1467+
The potential advantage here is reducing the number of new constructs
1468+
being added to the type language, and avoiding needing to either
1469+
introduce a new general mechanism for associated types or having a
1470+
special-case for ``Member``.
1471+
1472+
``PropsOnly`` (from :ref:`the query builder example <qb-impl>`) would
1473+
look like::
1474+
1475+
type PropsOnly[T] = typing.NewProtocol[
1476+
*[
1477+
typing.Member[typing.GetName[p], PointerArg[typing.GetType[p]]]
1478+
for p in typing.Iter[typing.Attrs[T]]
1479+
if typing.IsAssignable[typing.GetType[p], Property]
1480+
]
1481+
]
1482+
1483+
Everyone hated how this looked a lot.
1484+
14131485
.. _less_syntax:
14141486

14151487

@@ -1430,8 +1502,9 @@ Boolean operations would likewise become operators (``Not``, ``And``,
14301502
etc).
14311503

14321504
The advantage of this is that constructing a type annotation never
1433-
needs to do non-trivial computation, and thus we don't need
1434-
:ref:`runtime hooks <rt-support>` to support evaluating them.
1505+
needs to do non-trivial computation (assuming we also get rid of dot
1506+
notation), and thus we don't need :ref:`runtime hooks <rt-support>` to
1507+
support evaluating them.
14351508

14361509
It would also mean that it would be much easier to extract the raw
14371510
type annotation. (The lambda form would still be somewhat fiddly.
@@ -1448,35 +1521,6 @@ worse. Supporting filtering while mapping would make it even more bad
14481521

14491522
We can explore other options too if needed.
14501523

1451-
1452-
Don't use dot notation to access ``Member`` components
1453-
------------------------------------------------------
1454-
1455-
Earlier versions of this PEP draft omitted the ability to write
1456-
``m.name`` and similar on ``Member`` and ``Param`` components, and
1457-
instead relied on helper operators such as ``typing.GetName`` (that
1458-
could be implemented under the hood using ``typing.GetArg`` or
1459-
``typing.GetMemberType``).
1460-
1461-
The potential advantage here is reducing the number of new constructs
1462-
being added to the type language, and avoiding needing to either
1463-
introduce a new general mechanism for associated types or having a
1464-
special-case for ``Member``.
1465-
1466-
``PropsOnly`` (from :ref:`the query builder example <qb-impl>`) would
1467-
look like::
1468-
1469-
type PropsOnly[T] = typing.NewProtocol[
1470-
*[
1471-
typing.Member[typing.GetName[p], PointerArg[typing.GetType[p]]]
1472-
for p in typing.Iter[typing.Attrs[T]]
1473-
if typing.IsAssignable[typing.GetType[p], Property]
1474-
]
1475-
]
1476-
1477-
Everyone hated how this looked a lot.
1478-
1479-
14801524
Perform type manipulations with normal Python functions
14811525
-------------------------------------------------------
14821526

@@ -1494,7 +1538,7 @@ types would be quite a bit *more* complicated.
14941538

14951539
It would require a well-defined and safe-to-run subset of the language
14961540
(and standard library) to be defined that could be run from within
1497-
typecheckers. Subsets like this have been defined in other system
1541+
typecheckers. Subsets like this have been defined in other systems
14981542
(see `Starlark <#starlark_>`_, the configuration language for Bazel),
14991543
but it's still a lot of surface area, and programmers would need to
15001544
keep in mind the boundaries of it.
@@ -1559,7 +1603,7 @@ arguments invariantly.
15591603
Acknowledgements
15601604
================
15611605

1562-
Jukka Lehtosalo
1606+
Jukka Lehtosalo, etc
15631607

15641608
[Thank anyone who has helped with the PEP.]
15651609

0 commit comments

Comments
 (0)