Type expressions provide a standardized wayto specify types in the Python type system. When a type expression isevaluated at runtime, the resultingtype form object encodes the informationsupplied in the type expression. This enables a variety of use cases includingruntime type checking, introspection, and metaprogramming.
Such use cases have proliferated, but there is currently no way to accuratelyannotate functions that accept type form objects. Developers are forced to usean overly-wide type likeobject, which makes some use cases impossible andgenerally reduces type safety. This PEP addresses this limitation byintroducing a new special formtyping.TypeForm.
This PEP makes no changes to the Python grammar. Correct usage ofTypeForm isintended to be enforced only by type checkers, not by the Python runtime.
A function that operates on type form objects must understand how typeexpression details are encoded in these objects. For example,int|str,"int|str",list[int], andMyTypeAlias are all valid typeexpressions, and they evaluate to instances oftypes.UnionType,builtins.str,types.GenericAlias, andtyping.TypeAliasType,respectively.
There is currently no way to indicate to a type checker that a function acceptstype form objects and knows how to work with them.TypeForm addresses thislimitation. For example, here is a function that checks whether a value isassignable to a specified type and returns None if it is not:
deftrycast[T](typx:TypeForm[T],value:object)->T|None:...
The use ofTypeForm and the type variableT describes a relationshipbetween the type form passed to parametertypx and the function’sreturn type.
TypeForm can also be used withTypeIs to define custom typenarrowing behaviors:
defisassignable[T](value:object,typx:TypeForm[T])->TypeIs[T]:...request_json:object=...ifisassignable(request_json,MyTypedDict):assert_type(request_json,MyTypedDict)# Type of variable is narrowed
Theisassignable function implements something like an enhancedisinstance check. This is useful for validating whether a value decodedfrom JSON conforms to a particular structure of nestedTypedDicts,lists, unions,Literals, or any other type form that can be describedwith a type expression. This kind of check was alluded to inPEP 589 but could not be implemented withoutTypeForm.
type[C]?One might think thattype[C] would suffice for these use cases. However,only class objects (instances of thebuiltins.type class) are assignabletotype[C]. Many type form objects do not meet this requirement:
deftrycast[T](typx:type[T],value:object)->T|None:...trycast(str,'hi')# OKtrycast(Literal['hi'],'hi')# Type violationtrycast(str|None,'hi')# Type violationtrycast(MyProtocolClass,obj)# Type violation
A survey of Python libraries reveals several categories of functions thatwould benefit fromTypeForm:
defis_assignable[T](value:object,typx:TypeForm[T])->TypeIs[T]defis_match[T](value:object,typx:TypeForm[T])->TypeGuard[T]defconvert[T](value:object,typx:TypeForm[T])->T
classConverter[T]:def__init__(self,typx:TypeForm[T])->None:...defconvert(self,value:object)->T:...
classField[T]:value_type:TypeForm[T]
The survey also identified some introspection functions that accept runtimetype forms as input. Today, these functions are annotated withobject:
defget_annotation_info(typx:object)->objectThese functions accept values evaluated from arbitrary annotation expressions,not just type expressions, so they cannot be altered to useTypeForm.
When a type expression is evaluated at runtime, the resulting value is atype form object. This value encodes the information supplied in the typeexpression, and it represents the type described by that type expression.
TypeForm is a special form that, when used in a type expression, describesa set of type form objects. It accepts a single type argument, which must be avalid type expression.TypeForm[T] describes the set of all type formobjects that represent the typeT or types that areassignable toT. For example,TypeForm[str|None] describes the set of all type form objectsthat represent a type assignable tostr|None:
ok1:TypeForm[str|None]=str|None# OKok2:TypeForm[str|None]=str# OKok3:TypeForm[str|None]=None# OKok4:TypeForm[str|None]=Literal[None]# OKok5:TypeForm[str|None]=Optional[str]# OKok6:TypeForm[str|None]="str | None"# OKok7:TypeForm[str|None]=Any# OKerr1:TypeForm[str|None]=str|int# Errorerr2:TypeForm[str|None]=list[str|None]# Error
By this same definition,TypeForm[object] describes a type form objectthat represents the typeobject or any type that is assignable toobject.Since all types in the Python type system are assignable toobject,TypeForm[object] describes the set of all type form objectsevaluated from all valid type expressions.
TypeForm[Any] describes aTypeForm type whose type argument is notstatically known but is a valid type form object. It is thus assignable bothto and from any otherTypeForm type (becauseAny is assignable bothto and from any type).
The type expressionTypeForm, with no type argument provided, isequivalent toTypeForm[Any].
TypeForm EvaluationWhen a static type checker encounters a valid type expression, the evaluated type of this expression should be assignabletoTypeForm[T] if the type it describes is assignable toT.
For example, if a static type checker encounters the expressionstr|None,it may normally evaluate its type asUnionType because it produces aruntime value that is an instance oftypes.UnionType. However, becausethis expression is a valid type expression, it is also assignable to thetypeTypeForm[str|None]:
v1_actual:UnionType=str|None# OKv1_type_form:TypeForm[str|None]=str|None# OKv2_actual:type=list[int]# OKv2_type_form:TypeForm=list[int]# OK
TheAnnotated special form is allowed in type expressions, so it canalso appear in an expression that is assignable toTypeForm. Consistentwith the typing spec’s rules forAnnotated, a static type checker may chooseto ignore anyAnnotated metadata that it does not understand:
v3:TypeForm[int|str]=Annotated[int|str,"metadata"]# OKv4:TypeForm[Annotated[int|str,"metadata"]]=int|str# OK
A string literal expression containing a valid type expression should likewisebe assignable toTypeForm:
v5:TypeForm[set[str]]="set[str]"# OK
The typing spec defines syntactic rules for type expressions in the form of aformal grammar. Semantic rules are specifiedas comments along with the grammar definition. Contextual requirements are detailedthroughout the typing spec in sections that discuss concepts that appear withintype expressions. For example, the special formSelf can be used in atype expression only within a class, and a type variable can be used withina type expression only when it is associated with a valid scope.
A valid type expression is an expression that follows all of the syntactic,semantic, and contextual rules for a type expression.
Expressions that are not valid type expressions should not evaluate to aTypeForm type:
bad1:TypeForm=tuple()# Error: Call expression not allowed in type expressionbad2:TypeForm=(1,2)# Error: Tuple expression not allowed in type expressionbad3:TypeForm=1# Non-class object not allowed in type expressionbad4:TypeForm=Self# Error: Self not allowed outside of a classbad5:TypeForm=Literal[var]# Error: Variable not allowed in type expressionbad6:TypeForm=Literal[f""]# Error: f-strings not allowed in type expressionbad7:TypeForm=ClassVar[int]# Error: ClassVar not allowed in type expressionbad8:TypeForm=Required[int]# Error: Required not allowed in type expressionbad9:TypeForm=Final[int]# Error: Final not allowed in type expressionbad10:TypeForm=Unpack[Ts]# Error: Unpack not allowed in this contextbad11:TypeForm=Optional# Error: Invalid use of Optional special formbad12:TypeForm=T# Error if T is an out-of-scope TypeVarbad13:TypeForm="int + str"# Error: invalid quoted type expression
TypeForm EvaluationTypeForm also acts as a function that can be called with a single argument.Type checkers should validate that this argument is a valid type expression:
x1=TypeForm(str|None)reveal_type(v1)# Revealed type is "TypeForm[str | None]"x2=TypeForm('list[int]')revealed_type(v2)# Revealed type is "TypeForm[list[int]]"x3=TypeForm('type(1)')# Error: invalid type expression
The static type of aTypeForm(T) isTypeForm[T].
At runtime theTypeForm(...) callable simply returns the value passed to it.
This explicit syntax serves two purposes. First, it documents the developer’sintent to use the value as a type form object. Second, static type checkersvalidate that all rules for type expressions are followed:
x4=type(1)# No error, evaluates to "type[int]"x5=TypeForm(type(1))# Error: call not allowed in type expression
TypeForm has a single type parameter, which is covariant. That meansTypeForm[B] is assignable toTypeForm[A] ifB is assignable toA:
defget_type_form()->TypeForm[int]:...t1:TypeForm[int|str]=get_type_form()# OKt2:TypeForm[str]=get_type_form()# Error
type[T] is a subtype ofTypeForm[T], which means thattype[B] isassignable toTypeForm[A] ifB is assignable toA:
defget_type()->type[int]:...t3:TypeForm[int|str]=get_type()# OKt4:TypeForm[str]=get_type()# Error
TypeForm is a subtype ofobject and is assumed to have all of theattributes and methods ofobject.
This PEP clarifies static type checker behaviors when evaluating typeexpressions in “value expression” contexts (that is, contexts where typeexpressions are not mandated by the typing spec). In the absence of aTypeForm type annotation, existing type evaluation behaviors persist,so no backward compatibility issues are anticipated. For example, if a statictype checker previously evaluated the type of expressionstr|None asUnionType, it will continue to do so unless this expression is assignedto a variable or parameter whose type is annotated asTypeForm.
Type expressions are used in annotations to describe which values are acceptedby a function parameter, returned by a function, or stored in a variable:
parameter type return type | | v vdef plus(n1: int, n2: int) -> int: sum: int = n1 + n2 ^ | variable type return sum
Type expressions evaluate to validtype form objects at runtime and can beassigned to variables and manipulated like any other data in a program:
a variable a type expression | | v vint_type_form: TypeForm = int | None ^ | the type of a type form object
TypeForm[] is how you spell the type of atype form object, which isa runtime representation of a type.
TypeForm is similar totype, buttype is compatible only withclass objects likeint,str,list, orMyClass.TypeForm accommodates any type form that can be expressed usinga valid type expression, including those with brackets (list[int]), unionoperators (int|None), and special forms (Any,LiteralString,Never, etc.).
Most programmers will not define theirown functions that accept aTypeFormparameter or return aTypeForm value. It is more common to pass a typeform object to a library function that knows how to decode and use such objects.
For example, theisassignable function in thetrycast librarycan be used like Python’s built-inisinstance function to check whethera value matches the shape of a particular type.isassignable acceptsanytype form object as input.
fromtrycastimportisassignableifisassignable(some_object,MyTypedDict):# OK: MyTypedDict is a TypeForm[]...
ifisinstance(some_object,MyTypedDict):# ERROR: MyTypedDict is not a type[]...
If you want to write your own runtime type checker or a function thatmanipulates type form objects as values at runtime, this section providesexamples of how such a function can useTypeForm.
Functions liketyping.get_origin andtyping.get_args can be used toextract components of some type form objects.
importtypingfromtypingimportTypeForm,castdefstrip_annotated_metadata[T](typx:TypeForm[T])->TypeForm[T]:iftyping.get_origin(typx)istyping.Annotated:typx=cast(TypeForm[T],typing.get_args(typx)[0])returntypx
isinstance andis can also be used to distinguish between differentkinds of type form objects:
importtypesimporttypingfromtypingimportTypeForm,castdefsplit_union(typx:TypeForm)->tuple[TypeForm,...]:ifisinstance(typx,types.UnionType):# X | Yreturncast(tuple[TypeForm,...],typing.get_args(typx))iftyping.get_origin(typx)istyping.Union:# Union[X, Y]returncast(tuple[TypeForm,...],typing.get_args(typx))iftypxin(typing.Never,typing.NoReturn,):return()return(typx,)
TypeForm can be parameterized by a type variable that is used elsewherewithin the same function definition:
defas_instance[T](typx:TypeForm[T])->T|None:returntypx()ifisinstance(typx,type)elseNone
typeBothTypeForm andtype can be parameterized by the same typevariable within the same function definition:
defas_type[T](typx:TypeForm[T])->type[T]|None:returntypxifisinstance(typx,type)elseNone
TypeIs andTypeGuardA type variable can also be used by aTypeIs orTypeGuard return type:
defisassignable[T](value:object,typx:TypeForm[T])->TypeIs[T]:...count:int|str=...ifisassignable(count,int):assert_type(count,int)else:assert_type(count,str)
A function that takes anarbitraryTypeForm as input must support avariety of possible type form objects. Such functions are not easy to write.
'list[str]')must beparsed (to something likelist[str]).eval(), which is difficult to use in a safe way.IntTree=list[int|'IntTree'] are difficultto resolve.QuerySet[User]) can introducenon-standard behaviors that require runtime support.Pyright (version 1.1.379) provides a reference implementation forTypeForm.
Mypy (commit 1b7e71; Nov 3, 2025) providesa reference implementation forTypeForm.
A reference implementation of the runtime component is provided in thetyping_extensions module.
Alternate names were considered forTypeForm.TypeObjectandTypeType were deemed too generic.TypeExpression andTypeExprwere also considered, but these were considered confusing because these objectsare not themselves “expressions” but rather the result of evaluating a typeexpression.
type[C] to support all type expressionstype wasdesigned to describe class objects, subclasses of thetype class. A value with the typetype is assumed to be instantiablethrough a constructor call. Widening the meaning oftype to representarbitrary type form objects would present backward compatibility problemsand would eliminate a way to describe the set of values limited to subclassesoftype.
Certain special forms act as type qualifiers and can be used insome but notall annotation contexts:
For example. the type qualifierFinal can be used as a variable type butnot as a parameter type or a return type:
some_const:Final[str]=...# OKdeffoo(not_reassignable:Final[object]):...# Error: Final not allowed heredefnonsense()->Final[object]:...# Error: Final not allowed here
With the exception ofAnnotated, type qualifiers are not allowed in typeexpressions.TypeForm is limited to type expressions because itsassignability rules are based on the assignability rules for types. It isnonsensical to ask whetherFinal[int] is assignable toint because theformer is not a valid type expression.
Functions that wish to operate on objects that are evaluated from annotationexpressions can continue to accept such inputs asobject parameters.
It was asserted that some functions may wish to pattern match on theinterior of type expressions in their signatures.
One use case is to allow a function to explicitly enumerate all thespecific kinds of type expressions it supports as input.Consider the following possible pattern matching syntax:
@overloaddefcheckcast(typx:TypeForm[AT=Annotated[T,*A]],value:str)->T:...@overloaddefcheckcast(typx:TypeForm[UT=Union[*Ts]],value:str)->Union[*Ts]:...@overloaddefcheckcast(typx:type[C],value:str)->C:...# ... (more)
All functions observed in the wild that conceptually accept type formobjects generally try to supportall kinds of type expressions, so itdoesn’t seem valuable to enumerate a particular subset.
Additionally, the above syntax isn’t precise enough to fully describe theinput constraints for a typical function in the wild. For example, manyfunctions do not support type expressions with quoted subexpressionslikelist['Movie'].
A second use case for pattern matching is to explicitly match anAnnotatedform to extract the interior type argument and strip away any metadata:
defcheckcast(typx:TypeForm[T]|TypeForm[AT=Annotated[T,*A]],value:object)->T:
However,Annotated[T,metadata] is already treated equivalent toTby static type checkers. There’s no additional value in being explicit aboutthis behavior. The example above could more simply be written as the equivalent:
defcheckcast(typx:TypeForm[T],value:object)->T:
typing_extensions implementation of theTypeExpr special form.type to be assignedtoTypeForm or not.TypeFormspecial form in a real-world runtime type checker (beartype).dataclass.make_dataclass allows the type qualifierInitVar[...],soTypeForm cannot be used in this case.ForwardRef instancesat runtime using internal helper functions in thetyping module.Runtime type checkers may wish to implement similar functions whenworking with string-based forward references.This document is placed in the public domain or under theCC0-1.0-Universal license, whichever is more permissive.
Source:https://github.com/python/peps/blob/main/peps/pep-0747.rst
Last modified:2025-12-08 19:11:10 GMT