Skip to content
168 changes: 146 additions & 22 deletions pep-0677.rst
Original file line number Diff line number Diff line change
Expand Up @@ -635,24 +635,107 @@ callable types and ``=>`` for lambdas.
Runtime Behavior
----------------

Our tentative plan is that:

- The ``__repr__`` will show an arrow syntax literal.
- We will provide a new API where the runtime data structure can be
accessed in the same manner as the AST data structure.
- We will ensure that we provide an API that is backward-compatible
with ``typing.Callable`` and ``typing.Concatenate``, specifically
the behavior of ``__args__`` and ``__parameters__``.
The new AST nodes need to evaluate to runtime types, and we have two goals for the
behavior of these runtime types:

- They should expose a structured API that is descriptive and powerful
enough to be compatible with extending the type to include new features
like named and variadic arguments.
- They should also expose an API that is fully backward-compatible with
``typing.Callable``.

Evaluation and Structured API
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

We intend to create new builtin types to which the new AST nodes will
evaluate, exposing them in the ``types`` module.

Our plan is to expose a structured API as if they were defined as follows::

class CallableType:
is_async: bool
arguments: Ellipsis | tuple[CallableTypeArgument]
return_type:: Typing.type

class CallableTypeArgument:
kind: CallableTypeArgumentKind
annotation: typing.Type | typing.TypeVar | typing.ParamSpec

class CallableTypeArgumentKind(Enum):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes for very long enum names. Although perhaps @global_enum could help?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know @global_enum was a thing!

Should https://docs.python.org/3.11/library/enum.html say something about it?

I had to do a code search on CPython to confirm it exists, search engines appear to be unaware of it.

POSITIONAL_ONLY: int = ...
PARAM_SPEC: int = ...


The evaluation rules are expressed in terms of the following
pseudocode::

def evaluate_callable_type(callable_type):
return CallableType(
is_async=isinstance(callable_type, ast.AsyncCallableType),
arguments=evaluate_arguments(callable_type.arguments),
return_type=evaluate_expression(callable_type.returns),
)

def evaluate_arguments(arguments):
match arguments:
case ast.AnyArguments():
return Ellipsis
case ast.ArgumentsList(posonlyargs):
return tuple(
evaluate_arg(arg) for arg in args
)
case ast.ArgumentsListConcatenation(posonlyargs, param_spec):
return tuple(
*(evaluate_arg(arg) for arg in args),
evaluate_arg(arg=param_spec, kind=PARAM_SPEC)
)
if isinstance(arguments, Any
return Ellipsis

def evaluate_arg(arg, kind=POSITIONAL_ONLY):
return CallableTypeArgument(
kind=POSITIONAL_ONLY,
annotation=evaluate_expression(value)
)


Backward-Compatible API
~~~~~~~~~~~~~~~~~~~~~~~

Because these details are still under debate we are currently
maintaining `a separate doc
<https://docs.google.com/document/d/15nmTDA_39Lo-EULQQwdwYx_Q1IYX4dD5WPnHbFG71Lk/edit>`_
with details about the new builtins, the evaluation model, how to
provide both a backward-compatible and more structured API, and
possible alternatives to the current plan.
To get backward compatibility with the existing ``types.Callable`` API,
which relies on fields ``__args__`` and ``__parameters__``, we can define
them as if they were written in terms of the following::

import itertools
import typing

def get_args(
t: CallableType
):
return_type_arg = (
typing.Awaitable[t.return_type]
if t.is_async
else t.return_type
)
arguments = t.arguments
if isinstance(arguments, Ellipsis):
argument_args = (Ellipsis,)
else:
argument_args = (arg.annotation for arg in arguments)
return (
*arguments_args,
return_type_arg
)

Additional Behaviors of ``types.CallableType``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

- The ``__eq__`` method should treat equivalent ``typing.Callable``
values as equal to values constructed using the builtin syntax, and
otherwise should behave like the ``__eq__`` of ``typing.Callable``.
- The ``__repr__`` method should produce an arrow syntax representation that,
when evaluated, gives us back an equal ``types.CallableType`` instance.

Once the plan is finalized we will include a full specification of
runtime behavior in this section of the PEP.

Rejected Alternatives
=====================
Expand Down Expand Up @@ -908,9 +991,9 @@ We rejected this change because:
syntax errors.
- Moreover, if a type is complicated enough that readability is a concern
we can always use type aliases, for example::

IntToIntFunction: (int) -> int

def make_adder() -> IntToIntFunction:
return lambda x: x + 1

Expand Down Expand Up @@ -991,6 +1074,46 @@ Moreover, none of these ideas help as much with reducing verbosity
as the current proposal, nor do they introduce as strong a visual cue
as the ``->`` between the parameter types and the return type.

Alternative Runtime Behaviors
-----------------------------

The hard requirements on our runtime API are that:

- It must preserve backward compatibility with ``typing.Callable`` via
``__args__`` and ``__params__``.
- It must provide a structured API, which should be extensible if
in the future we try to support named and variadic arguments.

Alternative APIs
~~~~~~~~~~~~~~~~

We considered having the runtime data ``types.CallableType`` use a
more structured API where there would be separate fields for
``posonlyargs`` and ``param_spec``. The current proposal was
was inspired by the ``inspect.Signature`` type.

We use "argument" in our field and type names, unlike "parameter"
as in ``inspect.Signature``, in order to avoid confusion with
the ``callable_type.__parameters__`` field from the legacy API
that refers to type parameters rather than callable parameters.

Using the plain return type in ``__args__`` for async types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

It is debatable whether we are required to preserve backward compatiblity
of ``__args__`` for async callable types like ``async (int) -> str``. The
reason is that one could argue they are not expressible directly
using ``typing.Callable``, and therefore it would be fine to set
``__args__`` as ``(int, int)`` rather than ``(int, typing.Awaitable[int])``.

But we believe this would be problematic. By preserving the appearance
of a backward-compatible API while actually breaking its semantics on
async types, we would cause runtime type libraries that attempt to
interpret ``Callable`` using ``__args__`` to fail silently.

It is for this reason that we automatically wrap the return type in
``Awaitable``.

Backward Compatibility
======================

Expand Down Expand Up @@ -1033,10 +1156,11 @@ Open Issues
Details of the Runtime API
--------------------------

Once we have finalized all details of the runtime behavior, we
will need to add a full specification of the behavior to the
`Runtime Behavior`_ section of this PEP as well as include that
behavior in our reference implementation.
We have attempted to provide a complete behavior specification in
the `Runtime Behavior`_ section of this PEP.

But there are probably more details that we will not realize we
need to define until we build a full reference implementation.

Optimizing ``SyntaxError`` messages
-----------------------------------
Expand Down