PEP 747 – TypeExpr: Type Hint for a Type Expression
- Author:
- David Foster <david at dafoster.net>
- Sponsor:
- Jelle Zijlstra <jelle.zijlstra at gmail.com>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Topic:
- Typing
- Created:
- 27-May-2024
- Python-Version:
- 3.14
- Post-History:
- 19-Apr-2024, 04-May-2024, 17-Jun-2024
Table of Contents
Abstract
PEP 484 defines the notation type[C]
where C
is a class, to
refer to a class object that is a subtype of C
. It explicitly does
not allow type[C]
to refer to arbitrary
type expression objects such
as the runtime object str | None
, even if C
is an unbounded
TypeVar
. [1] In cases where that restriction is unwanted, this
PEP proposes a new notation TypeExpr[T]
where T
is a type, to
refer to a either a class object or some other type expression object
that is a subtype of T
, allowing any kind of type to be referenced.
This PEP makes no Python grammar changes. Correct usage of
TypeExpr[]
is intended to be enforced only by static and runtime
type checkers and need not be enforced by Python itself at runtime.
Motivation
The introduction of TypeExpr
allows new kinds of metaprogramming
functions that operate on type expressions to be type-annotated and
understood by type checkers.
For example, here is a function that checks whether a value is assignable to a variable of a particular type, and if so returns the original value:
def trycast[T](typx: TypeExpr[T], value: object) -> T | None: ...
The use of TypeExpr[]
and the type variable T
enables the return
type of this function to be influenced by a typx
value passed at
runtime, which is quite powerful.
Here is another function that checks whether a value is assignable to a
variable of a particular type, and if so returns True
(as a special
TypeIs[]
bool [2]):
def isassignable[T](value: object, typx: TypeExpr[T]) -> TypeIs[T]: ...
The use of TypeExpr[]
and TypeIs[]
together enables type
checkers to narrow the return type appropriately depending on what type
expression is passed in:
request_json: object = ...
if isassignable(request_json, MyTypedDict):
assert_type(request_json, MyTypedDict) # type is narrowed!
That isassignable
function enables a kind of enhanced isinstance
check which is useful for checking whether a value decoded from JSON
conforms to a particular structure of nested TypedDict
s,
lists, unions, Literal
s, and other types. This kind
of check was alluded to in PEP 589 but could
not be implemented at the time without a notation similar to
TypeExpr[]
.
Why can’t type[]
be used?
One might think you could define the example functions above to take a
type[C]
- which is syntax that already exists - rather than a
TypeExpr[T]
. However if you were to do that then certain type
expressions like str | None
- which are not class objects and
therefore not type
s at runtime - would be rejected:
# NOTE: Uses a type[C] parameter rather than a TypeExpr[T]
def trycast_type[C](typ: type[C], value: object) -> T | None: ...
trycast_type(str, 'hi') # ok; str is a type
trycast_type(Optional[str], 'hi') # ERROR; Optional[str] is not a type
trycast_type(str | int, 'hi') # ERROR; (str | int) is not a type
trycast_type(MyTypedDict, dict(value='hi')) # questionable; accepted by mypy 1.9.0
To solve that problem, type[]
could be widened to include the
additional values allowed by TypeExpr
. However doing so would lose
type[]
’s current ability to spell a class object which always
supports instantiation and isinstance
checks, unlike arbitrary type
expression objects. Therefore TypeExpr
is proposed as new notation
instead.
For a longer explanation of why we don’t just widen type[T]
to
accept all type expressions, see
Widen type[C] to support all type expressions.
Common kinds of functions that would benefit from TypeExpr
A survey of various Python libraries revealed a few kinds of commonly
defined functions which would benefit from TypeExpr[]
:
- Assignability checkers:
- Returns whether a value is assignable to a type expression. If so then also narrows the type of the value to match the type expression.
- Pattern 1:
def isassignable[T](value: object, typx: TypeExpr[T]) -> TypeIs[T]
- Pattern 2:
def ismatch[T](value: object, typx: TypeExpr[T]) -> TypeGuard[T]
- Examples: beartype.is_bearable, trycast.isassignable, typeguard.check_type, xdsl.isa
- Converters:
- If a value is assignable to (or coercible to) a type expression, a converter returns the value narrowed to (or coerced to) that type expression. Otherwise, it raises an exception.
- Pattern 1:
def convert[T](value: object, typx: TypeExpr[T]) -> T
- Pattern 2:
class Converter[T]: def __init__(self, typx: TypeExpr[T]) -> None: ... def convert(self, value: object) -> T: ...
- Examples: pydantic.TypeAdapter(T).validate_python, mashumaro.JSONDecoder(T).decode
- Typed field definitions:
- Pattern:
class Field[T]: value_type: TypeExpr[T]
- Examples: attrs.make_class, dataclasses.make_dataclass [3], openapify
- Pattern:
The survey also identified some introspection functions that take
annotation expressions as input using plain object
s which would
not gain functionality by marking those inputs as TypeExpr[]
:
- General introspection operations:
- Pattern:
def get_annotation_info(maybe_annx: object) -> object
- Examples: typing.{get_origin, get_args}, typing_inspect.{is_*_type, get_origin, get_parameters}
- Pattern:
Rationale
Before this PEP existed there were already a few definitions in use to describe different kinds of type annotations:
+----------------------------------+
| +------------------------------+ |
| | +-------------------------+ | |
| | | +---------------------+ | | |
| | | | Class object | | | | = type[C]
| | | +---------------------+ | | |
| | | Type expression object | | | = TypeExpr[T] <-- new!
| | +-------------------------+ | |
| | Annotation expression object | |
| +------------------------------+ |
| Object | = object
+----------------------------------+
- Class objects,
spelled as
type[C]
, supportisinstance
checks and are callable.- Examples:
int
,str
,MyClass
- Examples:
- Type expressions
include any type annotation which describes a type.
- Examples:
list[int]
,MyTypedDict
,int | str
,Literal['square']
, any class object
- Examples:
- Annotation expressions
include any type annotation, including those only valid in specific contexts.
- Examples:
Final[int]
,Required[str]
,ClassVar[str]
, any type expression
- Examples:
TypeExpr
aligns with an existing definition from the above list -
type expression - to avoid introducing yet another subset of type annotations
that users of Python typing need to think about.
TypeExpr
aligns with type expression specifically
because a type expression is already used to parameterize type variables,
which are used in combination with TypeIs
and TypeGuard
to enable
the compelling examples mentioned in Motivation.
TypeExpr
does not align with annotation expression for reasons given in
Rejected Ideas » Accept arbitrary annotation expressions.
Specification
A TypeExpr
value represents a type expression
such as str | None
, dict[str, int]
, or MyTypedDict
.
A TypeExpr
type is written as
TypeExpr[T]
where T
is a type or a type variable. It can also be
written without brackets as just TypeExpr
, in which case a type
checker should apply its usual type inference mechanisms to determine
the type of its argument, possibly Any
.
Using TypeExprs
A TypeExpr
is a new kind of type expression, usable in any context where a
type expression is valid, as a function parameter type, a return type,
or a variable type:
def is_union_type(typx: TypeExpr) -> bool: ... # parameter type
def union_of[S, T](s: TypeExpr[S], t: TypeExpr[T]) \
-> TypeExpr[S | T]: ... # return type
STR_TYPE: TypeExpr = str # variable type
assert_type(STR_TYPE, TypeExpr[str])
Note however that an unannotated variable assigned a type expression literal
will not be inferred to be of TypeExpr
type by type checkers because PEP
484 reserves that syntax for defining type aliases:
- No:
STR_TYPE = str # OOPS; treated as a type alias!
If you want a type checker to recognize a type expression literal in a bare
assignment you’ll need to explicitly declare the assignment-target as
having TypeExpr
type:
- Yes:
STR_TYPE: TypeExpr = str
- Yes:
STR_TYPE: TypeExpr STR_TYPE = str
- Okay, but discouraged:
STR_TYPE = str # type: TypeExpr # the type comment is significant
TypeExpr
values can be passed around and assigned just like normal
values:
def swap1[S, T](t1: TypeExpr[S], t2: TypeExpr[T]) -> tuple[TypeExpr[T], TypeExpr[S]]:
t1_new: TypeExpr[T] = t2 # assigns a TypeExpr value to a new annotated variable
t2_new: TypeExpr[S] = t1
return (t1_new, t2_new)
def swap2[S, T](t1: TypeExpr[S], t2: TypeExpr[T]) -> tuple[TypeExpr[T], TypeExpr[S]]:
t1_new = t2 # assigns a TypeExpr value to a new unannotated variable
t2_new = t1
assert_type(t1_new, TypeExpr[T])
assert_type(t2_new, TypeExpr[S])
return (t1_new, t2_new)
# NOTE: A more straightforward implementation would use isinstance()
def ensure_int(value: object) -> None:
value_type: TypeExpr = type(value) # assigns a type (a subtype of TypeExpr)
assert value_type == int
TypeExpr Values
A variable of type TypeExpr[T]
where T
is a type, can hold any
type expression object - the result of evaluating a
type expression
at runtime - which is a subtype of T
.
Incomplete expressions like a bare Optional
or Union
which do
not spell a type are not TypeExpr
values.
TypeExpr[...]
is itself a TypeExpr
value:
OPTIONAL_INT_TYPE: TypeExpr = TypeExpr[int | None] # OK
assert isassignable(Optional[int], OPTIONAL_INT_TYPE)
TypeExpr[]
values include all type expressions including some
non-universal type expressions which are not valid in all annotation contexts.
In particular:
Self
(valid only in some contexts)TypeGuard[...]
(valid only in some contexts)TypeIs[...]
(valid only in some contexts)
Explicit TypeExpr Values
The syntax TypeExpr(T)
(with parentheses) can be used to
spell a TypeExpr[T]
value explicitly:
NONE = TypeExpr(None)
INT1 = TypeExpr('int') # stringified type expression
INT2 = TypeExpr(int)
At runtime the TypeExpr(...)
callable returns its single argument unchanged.
Implicit TypeExpr Values
Historically static type checkers have only needed to recognize type expressions in contexts where a type expression was expected. Now type expression objects must also be recognized in contexts where a value expression is expected.
Static type checkers already recognize class objects (type[C]
):
- As a value expression,
C
has typetype[C]
, for each of the following values of C:name
(wherename
must refer to a valid in-scope class, type alias, or TypeVar)name '[' ... ']'
<type> '[' ... ']'
The following unparameterized type expressions can be recognized unambiguously:
- As a value expression,
X
has typeTypeExpr[X]
, for each of the following values of X:<Any>
<Self>
<LiteralString>
<NoReturn>
<Never>
None: The type expression None
(NoneType
) is ambiguous with the value None
,
so must use the explicit TypeExpr(...)
syntax:
- As a value expression,
TypeExpr(None)
has typeTypeExpr[None]
. - As a value expression,
None
continues to have typeNone
.
The following parameterized type expressions can be recognized unambiguously:
- As a value expression,
X
has typeTypeExpr[X]
, for each of the following values of X:<Literal> '[' ... ']'
<Optional> '[' ... ']'
<Union> '[' ... ']'
<Callable> '[' ... ']'
<tuple> '[' ... ']'
<TypeGuard> '[' ... ']'
<TypeIs> '[' ... ']'
Annotated: The type expression Annotated[...]
is ambiguous with
the annotation expression Annotated[...]
,
so must be disambiguated based on its argument type:
- As a value expression,
Annotated[x, ...]
has typetype[C]
ifx
has typetype[C]
. - As a value expression,
Annotated[x, ...]
has typeTypeExpr[T]
ifx
has typeTypeExpr[T]
. - As a value expression,
Annotated[x, ...]
has typeobject
ifx
has a type that is nottype[C]
orTypeExpr[T]
.
Union: The type expression T1 | T2
is ambiguous with the value int1 | int2
,
so must be disambiguated based on its argument type:
- As a value expression,
x | y
has typeTypeExpr[x | y]
ifx
has typeTypeExpr[t1]
(ortype[t1]
) andy
has typeTypeExpr[t2]
(ortype[t2]
). - As a value expression,
x | y
has typeint
ifx
has typeint
andy
has typeint
The stringified type expression "T"
is ambiguous with both
the stringified annotation expression "T"
and the string literal "T"
,
so must use the explicit TypeExpr(...)
syntax:
- As a value expression,
TypeExpr("T")
has typeTypeExpr[T]
, whereT
is a valid type expression - As a value expression,
"T"
continues to have typeLiteral["T"]
.
No other kinds of type expressions currently exist.
New kinds of type expressions that are introduced should define how they will be recognized in a value expression context.
Implicit Annotation Expression Values
Although this PEP is mostly concerned with type expressions rather than annotation expressions, it is straightforward to extend the rules for recognizing type expressions to similar rules for recognizing annotation expressions, so this PEP takes the opportunity to define those rules as well:
The following unparameterized annotation expressions can be recognized unambiguously:
- As a value expression,
X
has typeobject
, for each of the following values of X:<TypeAlias>
The following parameterized annotation expressions can be recognized unambiguously:
- As a value expression,
X
has typeobject
, for each of the following values of X:<Required> '[' ... ']'
<NotRequired> '[' ... ']'
<ReadOnly> '[' ... ']'
<ClassVar> '[' ... ']'
<Final> '[' ... ']'
<InitVar> '[' ... ']'
<Unpack> '[' ... ']'
Annotated: The annotation expression Annotated[...]
is ambiguous with
the type expression Annotated[...]
,
so must be disambiguated based on its argument type.
The following syntactic annotation expressions cannot be recognized in a value expression context at all:
'*' unpackable
name '.' 'args'
(wherename
must be an in-scope ParamSpec)name '.' 'kwargs'
(wherename
must be an in-scope ParamSpec)
The stringified annotation expression "T"
is ambiguous with both
the stringified type expression "T"
and the string literal "T"
, and
cannot be recognized in a value expression context at all:
- As a value expression,
"T"
continues to have typeLiteral["T"]
.
No other kinds of annotation expressions currently exist.
New kinds of annotation expressions that are introduced should define how they will (or will not) be recognized in a value expression context.
Literal[] TypeExprs
To simplify static type checking, a Literal[...]
value is not
considered assignable to a TypeExpr
variable even if all of its members
spell valid types:
STRS_TYPE_NAME: Literal['str', 'list[str]'] = 'str'
STRS_TYPE: TypeExpr = STRS_TYPE_NAME # ERROR: Literal[] value is not a TypeExpr
Static vs. Runtime Representations of TypeExprs
A TypeExpr
value appearing statically in a source file may be normalized
to a different representation at runtime. For example string-based
forward references are normalized at runtime to be ForwardRef
instances
in some contexts: [4]
>>> IntTree = list[typing.Union[int, 'IntTree']]
>>> IntTree
list[typing.Union[int, ForwardRef('IntTree')]]
The runtime representations of TypeExpr
s are considered implementation
details that may change over time and therefore static type checkers are
not required to recognize them:
INT_TREE: TypeExpr = ForwardRef('IntTree') # ERROR: Runtime-only form
Runtime type checkers that wish to assign a runtime-only representation
of a type expression to a TypeExpr[]
variable must use cast()
to
avoid errors from static type checkers:
INT_TREE = cast(TypeExpr, ForwardRef('IntTree')) # OK
Subtyping
Whether a TypeExpr
value can be assigned from one variable to another is
determined by the following rules:
TypeExpr[]
is covariant in its argument type, just like type[]
:
TypeExpr[T1]
is a subtype ofTypeExpr[T2]
iffT1
is a subtype ofT2
.type[C1]
is a subtype ofTypeExpr[C2]
iffC1
is a subtype ofC2
.
A plain type
can be assigned to a plain TypeExpr
but not the
other way around:
type[Any]
is assignable toTypeExpr[Any]
. (But not the other way around.)
TypeExpr[]
is a kind of object
, just like type[]
:
TypeExpr[T]
for anyT
is a subtype ofobject
.
TypeExpr[T]
, where T
is a type variable, is assumed to have all
the attributes and methods of object
and is not callable.
Interactions with isinstance() and issubclass()
The TypeExpr
special form cannot be used as the type
argument to
isinstance
:
>>> isinstance(str, TypeExpr)
TypeError: typing.TypeExpr cannot be used with isinstance()
>>> isinstance(str, TypeExpr[str])
TypeError: isinstance() argument 2 cannot be a parameterized generic
The TypeExpr
special form cannot be used as any argument to
issubclass
:
>>> issubclass(TypeExpr, object)
TypeError: issubclass() arg 1 must be a class
>>> issubclass(object, TypeExpr)
TypeError: typing.TypeExpr cannot be used with issubclass()
Affected signatures in the standard library
Changed signatures
The following signatures related to type expressions introduce
TypeExpr
where previously object
existed:
typing.cast
typing.assert_type
Unchanged signatures
The following signatures related to annotation expressions continue to
use object
and remain unchanged:
typing.get_origin
typing.get_args
The following signatures related to class objects continue to use
type
and remain unchanged:
builtins.isinstance
builtins.issubclass
builtins.type
typing.get_type_hints(..., include_extras=False)
nearly returns only type
expressions in Python 3.12, stripping out most type qualifiers
(Required, NotRequired, ReadOnly, Annotated
) but currently preserves a
few type qualifiers which are only allowed in annotation expressions
(ClassVar, Final, InitVar, Unpack
). It may be desirable to alter the
behavior of this function in the future to also strip out those
qualifiers and actually return type expressions, although this PEP does
not propose those changes now:
typing.get_type_hints(..., include_extras=False)
- Almost returns only type expressions, but not quite
typing.get_type_hints(..., include_extras=True)
- Returns annotation expressions
Backwards Compatibility
Previously the rules for recognizing type expression objects
in a value expression context were not defined, so static type checkers
varied in what types were assigned
to such objects. Existing programs manipulating type expression objects
were already limited in manipulating them as plain object
values,
and such programs should not break with
the newly-defined rules.
How to Teach This
Normally when using type annotations in Python you’re concerned with defining the shape of values allowed to be passed to a function parameter, returned by a function, or stored in a variable:
parameter type return type
| |
v v
def plus(n1: int, n2: int) -> int:
sum: int = n1 + n2
^
|
variable type
return sum
However type annotations themselves are valid values in Python and can be assigned to variables and manipulated like any other data in a program:
a variable a type
| |
v v
MAYBE_INT_TYPE: TypeExpr = int | None
^
|
the type of a type
TypeExpr[]
is how you spell the type of a variable containing a
type annotation object describing a type.
TypeExpr[]
is similar to type[]
, but type[]
can only used to
spell simple class objects like int
, str
, list
, or MyClass
.
TypeExpr[]
by contrast can additionally spell more complex types,
including those with brackets (like list[int]
) or pipes (like int | None
),
and including special types like Any
, LiteralString
, or Never
.
It is uncommon for a programmer to define their own function which accepts
a TypeExpr
parameter or returns a TypeExpr
value. Instead it is more common
for a programmer to pass a literal type expression to an existing function
accepting a TypeExpr
input which was imported from a runtime type checker
library.
For example the isassignable
function from the trycast
library
can be used like Python’s built-in isinstance
function to check whether
a value matches the shape of a particular type.
isassignable
will accept any kind of type as an input because its input
is a TypeExpr
. By contrast isinstance
only accepts a simple class object
(a type[]
) as input:
- Yes:
from trycast import isassignable if isassignable(some_object, MyTypedDict): # OK: MyTypedDict is a TypeExpr[] ...
- No:
if isinstance(some_object, MyTypedDict): # ERROR: MyTypedDict is not a type[] ...
There are many other runtime type checkers
providing useful functions that accept a TypeExpr
.
Advanced Examples
If you want to write your own runtime type checker or some other
kind of function that manipulates types as values at runtime,
this section gives examples of how you might implement such a function
using TypeExpr
.
Introspecting TypeExpr Values
A TypeExpr
is very similar to an object
at runtime, with no additional
attributes or methods defined.
You can use existing introspection functions like typing.get_origin
and
typing.get_args
to extract the components of a type expression that looks
like Origin[Arg1, Arg2, ..., ArgN]
:
import typing
def strip_annotated_metadata(typx: TypeExpr[T]) -> TypeExpr[T]:
if typing.get_origin(typx) is typing.Annotated:
typx = cast(TypeExpr[T], typing.get_args(typx)[0])
return typx
You can also use isinstance
and is
to distinguish one kind of
type expression from another:
import types
import typing
def split_union(typx: TypeExpr) -> tuple[TypeExpr, ...]:
if isinstance(typx, types.UnionType): # X | Y
return cast(tuple[TypeExpr, ...], typing.get_args(typx))
if typing.get_origin(typx) is typing.Union: # Union[X, Y]
return cast(tuple[TypeExpr, ...], typing.get_args(typx))
if typx in (typing.Never, typing.NoReturn,):
return ()
return (typx,)
Combining with a type variable
TypeExpr[]
can be parameterized by a type variable that is used elsewhere within
the same function definition:
def as_instance[T](typx: TypeExpr[T]) -> T | None:
return typx() if isinstance(typx, type) else None
Combining with type[]
Both TypeExpr[]
and type[]
can be parameterized by the same type
variable within the same function definition:
def as_type[T](typx: TypeExpr[T]) -> type[T] | None:
return typx if isinstance(typx, type) else None
Combining with TypeIs[] and TypeGuard[]
A type variable parameterizing a TypeExpr[]
can also be used by a TypeIs[]
within the same function definition:
def isassignable[T](value: object, typx: TypeExpr[T]) -> TypeIs[T]: ...
count: int | str = ...
if isassignable(count, int):
assert_type(count, int)
else:
assert_type(count, str)
or by a TypeGuard[]
within the same function definition:
def isdefault[T](value: object, typx: TypeExpr[T]) -> TypeGuard[T]:
return (value == typx()) if isinstance(typx, type) else False
value: int | str = ''
if isdefault(value, int):
assert_type(value, int)
assert 0 == value
elif isdefault(value, str):
assert_type(value, str)
assert '' == value
else:
assert_type(value, int | str)
Challenges When Accepting All TypeExprs
A function that takes an arbitrary TypeExpr
as
input must support a large variety of possible type expressions and is
not easy to write. Some challenges faced by such a function include:
- An ever-increasing number of typing special forms are introduced with each new Python version which must be recognized, with special handling required for each one.
- Stringified type annotations [5] (like
'list[str]'
) must be parsed (to something liketyping.List[str]
) to be introspected.- In practice it is extremely difficult for stringified type annotations to be handled reliably at runtime, so runtime type checkers may opt to not support them at all.
- Resolving string-based forward references inside type
expressions to actual values must typically be done using
eval()
, which is difficult/impossible to use in a safe way. - Recursive types like
IntTree = list[typing.Union[int, 'IntTree']]
are not possible to fully resolve. - Supporting user-defined generic types (like Django’s
QuerySet[User]
) requires user-defined functions to recognize/parse, which a runtime type checker should provide a registration API for.
Reference Implementation
The following will be true when mypy#9773 is implemented:
The mypy type checker supportsTypeExpr
types. A reference implementation of the runtime component is provided in thetyping_extensions
module.
Rejected Ideas
Widen type[C] to support all type expressions
type
was designed to only be used to describe class objects. A
class object can always be used as the second argument of isinstance()
and can usually be instantiated by calling it.
TypeExpr
on the other hand is typically introspected by the user in
some way, is not necessarily directly instantiable, and is not
necessarily directly usable in a regular isinstance()
check.
It would be possible to widen type
to include the additional values
allowed by TypeExpr
but it would reduce clarity about the user’s
intentions when working with a type
. Different concepts and usage
patterns; different spellings.
Accept arbitrary annotation expressions
Certain typing special forms can be used in some but not all annotation contexts:
For example Final[]
can be used as a variable type but not as a
parameter type or a return type:
some_const: Final[str] = ... # OK
def foo(not_reassignable: Final[object]): ... # ERROR: Final[] not allowed here
def nonsense() -> Final[object]: ... # ERROR: Final[] not meaningful here
TypeExpr[T]
does not allow matching such annotation expressions
because it is not clear what it would mean for such an expression
to parameterized by a type variable in position T
:
def ismatch[T](value: object, typx: TypeExpr[T]) -> TypeGuard[T]: ...
def foo(some_arg):
if ismatch(some_arg, Final[int]): # ERROR: Final[int] is not a TypeExpr
reveal_type(some_arg) # ? NOT Final[int], because invalid for a parameter
Functions that wish to operate on all kinds of annotation expressions,
including those that are not TypeExpr
s, can continue to accept such
inputs as object
parameters, as they must do so today.
Accept only universal type expressions
Earlier drafts of this PEP only allowed TypeExpr[]
to match the subset
of type expressions which are valid in all contexts, excluding
non-universal type expressions.
However doing that would effectively
create a new subset of annotation expressions that Python typing users
would have to understand, on top of all the existing distinctions between
“class objects”, “type expressions”, and “annotation expressions”.
To avoid introducing yet another concept that everyone has to learn,
this proposal just rounds TypeExpr[]
to exactly match the existing
definition of a “type expression”.
Support pattern matching on type expressions
It was asserted that some functions may wish to pattern match on the interior of type expressions in their signatures.
One use case is to allow a function to explicitly enumerate all the specific kinds of type expressions it supports as input. Consider the following possible pattern matching syntax:
@overload
def checkcast(typx: TypeExpr[AT=Annotated[T, *Anns]], value: str) -> T: ...
@overload
def checkcast(typx: TypeExpr[UT=Union[*Ts]], value: str) -> Union[*Ts]: ...
@overload
def checkcast(typx: type[C], value: str) -> C: ...
# ... (more)
All functions observed in the wild that conceptually take a TypeExpr[]
generally try to support all kinds of type expressions, so it doesn’t
seem valuable to enumerate a particular subset.
Additionally the above syntax isn’t precise enough to fully describe the
actual input constraints for a typical function in the wild. For example
many functions recognize un-stringified type expressions like
list[Movie]
but may not recognize type expressions with stringified
subcomponents like list['Movie']
.
A second use case for pattern matching on the interior of type
expressions is to explicitly match an Annotated[]
form to pull out the
interior type argument and strip away the metadata:
def checkcast(
typx: TypeExpr[T] | TypeExpr[AT=Annotated[T, *Anns]],
value: object
) -> T:
However Annotated[T, metadata]
is already treated equivalent to T
anyway.
There’s no additional value in being explicit about this behavior.
The example above could be more-straightforwardly written as the equivalent:
def checkcast(typx: TypeExpr[T], value: object) -> T:
Footnotes
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.
Source: https://github.com/python/peps/blob/main/peps/pep-0747.rst
Last modified: 2024-06-22 01:26:30 GMT