Important
This PEP is a historical document: seeVariance Inference,Type aliases,Type parameter lists,The type statement andAnnotation scopes. for up-to-date specs and documentation. Canonical typing specs are maintained at thetyping specs site; runtime typing behaviour is described in the CPython documentation.
×
See thetyping specification update process for how to propose changes to the typing spec.
This PEP specifies an improved syntax for specifying type parameters withina generic class, function, or type alias. It also introduces a new statementfor declaring type aliases.
PEP 484 introduced type variables into the language.PEP 612 builtupon this concept by introducing parameter specifications, andPEP 646 added variadic type variables.
While generic types and type parameters have grown in popularity, thesyntax for specifying type parameters still feels “bolted on” to Python.This is a source of confusion among Python developers.
There is consensus within the Python static typing community that it is timeto provide a formal syntax that is similar to other modern programminglanguages that support generic types.
An analysis of 25 popular typed Python libraries revealed that typevariables (in particular, thetyping.TypeVar symbol) were used in14% of modules.
While the use of type variables has become widespread, the manner in whichthey are specified within code is the source of confusion among manyPython developers. There are a couple of factors that contribute to thisconfusion.
The scoping rules for type variables are difficult to understand. Typevariables are typically allocated within the global scope, but their semanticmeaning is valid only when used within the context of a generic class,function, or type alias. A single runtime instance of a type variable may bereused in multiple generic contexts, and it has a different semantic meaningin each of these contexts. This PEP proposes to eliminate this source ofconfusion by declaring type parameters at a natural place within a class,function, or type alias declaration statement.
Generic type aliases are often misused because it is not clear to developersthat a type argument must be supplied when the type alias is used. This leadsto an implied type argument ofAny, which is rarely the intent. This PEPproposes to add new syntax that makes generic type alias declarationsclear.
PEP 483 andPEP 484 introduced the concept of “variance” for a typevariable used within a generic class. Type variables can be invariant,covariant, or contravariant. The concept of variance is an advanced detailof type theory that is not well understood by most Python developers, yetthey must confront this concept today when defining their first genericclass. This PEP largely eliminates the need for most developersto understand the concept of variance when defining generic classes.
When more than one type parameter is used with a generic class or type alias,the rules for type parameter ordering can be confusing. It is normally based onthe order in which they first appear within a class or type alias declarationstatement. However, this can be overridden in a class definition byincluding a “Generic” or “Protocol” base class. For example, in the classdeclarationclassClassA(Mapping[K,V]), the type parameters areordered asK and thenV. However, in the class declarationclassClassB(Mapping[K,V],Generic[V,K]), the type parameters areordered asV and thenK. This PEP proposes to make type parameterordering explicit in all cases.
The practice of sharing a type variable across multiple generic contextscreates other problems today. Modern editors provide features like “findall references” and “rename all references” that operate on symbols at thesemantic level. When a type parameter is shared among multiple genericclasses, functions, and type aliases, all references are semanticallyequivalent.
Type variables defined within the global scope also need to be given a namethat starts with an underscore to indicate that the variable is private tothe module. Globally-defined type variables are also often given names toindicate their variance, leading to cumbersome names like “_T_contra” and“_KT_co”. The current mechanisms for allocating type variables also requiresthe developer to supply a redundant name in quotes (e.g.T=TypeVar("T")).This PEP eliminates the need for the redundant name and cumbersomevariable names.
Defining type parameters today requires importing theTypeVar andGeneric symbols from thetyping module. Over the past several releasesof Python, efforts have been made to eliminate the need to importtypingsymbols for common use cases, and the PEP furthers this goal.
Defining a generic class prior to this PEP looks something like this.
fromtypingimportGeneric,TypeVar_T_co=TypeVar("_T_co",covariant=True,bound=str)classClassA(Generic[_T_co]):defmethod1(self)->_T_co:...
With the new syntax, it looks like this.
classClassA[T:str]:defmethod1(self)->T:...
Here is an example of a generic function today.
fromtypingimportTypeVar_T=TypeVar("_T")deffunc(a:_T,b:_T)->_T:...
And the new syntax.
deffunc[T](a:T,b:T)->T:...
Here is an example of a generic type alias today.
fromtypingimportTypeAlias_T=TypeVar("_T")ListOrSet:TypeAlias=list[_T]|set[_T]
And with the new syntax.
typeListOrSet[T]=list[T]|set[T]
Here is a new syntax for declaring type parameters for genericclasses, functions, and type aliases. The syntax adds support fora comma-delimited list of type parameters in square brackets afterthe name of the class, function, or type alias.
Simple (non-variadic) type variables are declared with an unadorned name.Variadic type variables are preceded by* (seePEP 646 for details).Parameter specifications are preceded by** (seePEP 612 for details).
# This generic class is parameterized by a TypeVar T, a# TypeVarTuple Ts, and a ParamSpec P.classChildClass[T,*Ts,**P]:...
There is no need to includeGeneric as a base class. Its inclusion asa base class is implied by the presence of type parameters, and it willautomatically be included in the__mro__ and__orig_bases__ attributesfor the class. The explicit use of aGeneric base class will result in aruntime error.
classClassA[T](Generic[T]):...# Runtime error
AProtocol base class with type arguments may generate a runtimeerror. Type checkers should generate an error in this case becausethe use of type arguments is not needed, and the order of type parametersfor the class are no longer dictated by their order in theProtocolbase class.
classClassA[S,T](Protocol):...# OKclassClassB[S,T](Protocol[S,T]):...# Recommended type checker error
Type parameter names within a generic class, function, or type alias must beunique within that same class, function, or type alias. A duplicate namegenerates a syntax error at compile time. This is consistent with therequirement that parameter names within a function signature must be unique.
classClassA[T,*T]:...# Syntax Errordeffunc1[T,**T]():...# Syntax Error
Class type parameter names are mangled if they begin with a doubleunderscore, to avoid complicating the name lookup mechanism for names usedwithin the class. However, the__name__ attribute of the type parameterwill hold the non-mangled name.
For a non-variadic type parameter, an “upper bound” type can be specifiedthrough the use of a type annotation expression. If an upper bound isnot specified, the upper bound is assumed to beobject.
classClassA[T:str]:...
The specified upper bound type must use an expression form that is allowed intype annotations. More complex expression forms should be flaggedas an error by a type checker. Quoted forward references are allowed.
The specified upper bound type must be concrete. An attempt to use a generictype should be flagged as an error by a type checker. This is consistent withthe existing rules enforced by type checkers for aTypeVar constructor call.
classClassA[T:dict[str,int]]:...# OKclassClassB[T:"ForwardReference"]:...# OKclassClassC[V]:classClassD[T:dict[str,V]]:...# Type checker error: generic typeclassClassE[T:[str,int]]:...# Type checker error: illegal expression form
PEP 484 introduced the concept of a “constrained type variable” which isconstrained to a set of two or more types. The new syntax supports this typeof constraint through the use of a literal tuple expression that containstwo or more types.
classClassA[AnyStr:(str,bytes)]:...# OKclassClassB[T:("ForwardReference",bytes)]:...# OKclassClassC[T:()]:...# Type checker error: two or more types requiredclassClassD[T:(str,)]:...# Type checker error: two or more types requiredt1=(bytes,str)classClassE[T:t1]:...# Type checker error: literal tuple expression required
If the specified type is not a tuple expression or the tuple expression includescomplex expression forms that are not allowed in a type annotation, a typechecker should generate an error. Quoted forward references are allowed.
classClassF[T:(3,bytes)]:...# Type checker error: invalid expression form
The specified constrained types must be concrete. An attempt to use a generictype should be flagged as an error by a type checker. This is consistent withthe existing rules enforced by type checkers for aTypeVar constructor call.
classClassG[T:(list[S],str)]:...# Type checker error: generic type
The upper bounds and constraints ofTypeVar objects are accessible atruntime through the__bound__ and__constraints__ attributes.ForTypeVar objects defined through the new syntax, these attributesbecome lazily evaluated, as discussed underLazy Evaluation below.
We propose to introduce a new statement for declaring type aliases. Similartoclass anddef statements, atype statement defines a scopefor type parameters.
# A non-generic type aliastypeIntOrStr=int|str# A generic type aliastypeListOrSet[T]=list[T]|set[T]
Type aliases can refer to themselves without the use of quotes.
# A type alias that includes a forward referencetypeAnimalOrVegetable=Animal|"Vegetable"# A generic self-referential type aliastypeRecursiveList[T]=T|list[RecursiveList[T]]
Thetype keyword is a new soft keyword. It is interpreted as a keywordonly in this part of the grammar. In all other locations, it is assumed tobe an identifier name.
Type parameters declared as part of a generic type alias are valid onlywhen evaluating the right-hand side of the type alias.
As withtyping.TypeAlias, type checkers should restrict the right-handexpression to expression forms that are allowed within type annotations.The use of more complex expression forms (call expressions, ternary operators,arithmetic operators, comparison operators, etc.) should be flagged as anerror.
Type alias expressions are not allowed to use traditional type variables (i.e.those allocated with an explicitTypeVar constructor call). Type checkersshould generate an error in this case.
T=TypeVar("T")typeMyList=list[T]# Type checker error: traditional type variable usage
We propose to deprecate the existingtyping.TypeAlias introduced inPEP 613. The new syntax eliminates its need entirely.
At runtime, atype statement will generate an instance oftyping.TypeAliasType. This class represents the type. Its attributesinclude:
__name__ is a str representing the name of the type alias__type_params__ is a tuple ofTypeVar,TypeVarTuple, orParamSpec objects that parameterize the type alias if it is generic__value__ is the evaluated value of the type aliasAll of these attributes are read-only.
The value of the type alias is evaluated lazily (seeLazy Evaluation below).
When the new syntax is used, a new lexical scope is introduced, and this scopeincludes the type parameters. Type parameters can be accessed by namewithin inner scopes. As with other symbols in Python, an inner scope candefine its own symbol that overrides an outer-scope symbol of the same name.This section provides a verbal description of the new scoping rules.TheScoping Behavior section below specifies the behavior in termsof a translation to near-equivalent existing Python code.
Type parameters are visible to othertype parameters declared elsewhere in the list. This allows type parametersto use other type parameters within their definition. While there is currentlyno use for this capability, it preserves the ability in the future to supportupper bound expressions or type argument defaults that depend on earliertype parameters.
A compiler error or runtime exception is generated if the definition of anearlier type parameter references a later type parameter even if the name isdefined in an outer scope.
# The following generates no compiler error, but a type checker# should generate an error because an upper bound type must be concrete,# and ``Sequence[S]`` is generic. Future extensions to the type system may# eliminate this limitation.classClassA[S,T:Sequence[S]]:...# The following generates no compiler error, because the bound for ``S``# is lazily evaluated. However, type checkers should generate an error.classClassB[S:Sequence[T],T]:...
A type parameter declared as part of a generic class is valid within theclass body and inner scopes contained therein. Type parameters are alsoaccessible when evaluating the argument list (base classes and any keywordarguments) that comprise the class definition. This allows base classesto be parameterized by these type parameters. Type parameters are notaccessible outside of the class body, including class decorators.
classClassA[T](BaseClass[T],param=Foo[T]):...# OKprint(T)# Runtime error: 'T' is not defined@dec(Foo[T])# Runtime error: 'T' is not definedclassClassA[T]:...
A type parameter declared as part of a generic function is valid withinthe function body and any scopes contained therein. It is also valid withinparameter and return type annotations. Default argument values for functionparameters are evaluated outside of this scope, so type parameters arenot accessible in default value expressions. Likewise, type parameters are notin scope for function decorators.
deffunc1[T](a:T)->T:...# OKprint(T)# Runtime error: 'T' is not defineddeffunc2[T](a=list[T]):...# Runtime error: 'T' is not defined@dec(list[T])# Runtime error: 'T' is not defineddeffunc3[T]():...
A type parameter declared as part of a generic type alias is valid withinthe type alias expression.
typeAlias1[K,V]=Mapping[K,V]|Sequence[K]
Type parameter symbols defined in outer scopes cannot be bound withnonlocal statements in inner scopes.
S=0defouter1[S]():S=1T=1defouter2[T]():definner1():nonlocalS# OK because it binds variable S from outer1nonlocalT# Syntax error: nonlocal binding not allowed for type parameterdefinner2():globalS# OK because it binds variable S from global scope
The lexical scope introduced by the new type parameter syntax is unliketraditional scopes introduced by adef orclass statement. A typeparameter scope acts more like a temporary “overlay” to the containing scope.The only new symbols containedwithin its symbol table are the type parameters defined using the new syntax.References to all other symbols are treated as though they were found withinthe containing scope. This allows base class lists (in class definitions) andtype annotation expressions (in function definitions) to reference symbolsdefined in the containing scope.
classOuter:classPrivate:pass# If the type parameter scope was like a traditional scope,# the base class 'Private' would not be accessible here.classInner[T](Private,Sequence[T]):pass# Likewise, 'Inner' would not be available in these type annotations.defmethod1[T](self,a:Inner[T])->Inner[T]:returna
The compiler allows inner scopes to define a local symbol that overrides anouter-scoped type parameter.
Consistent with the scoping rules defined inPEP 484, type checkers shouldgenerate an error if inner-scoped generic classes, functions, or type aliasesreuse the same type parameter name as an outer scope.
T=0@decorator(T)# Argument expression `T` evaluates to 0classClassA[T](Sequence[T]):T=1# All methods below should result in a type checker error# "type parameter 'T' already in use" because they are using the# type parameter 'T', which is already in use by the outer scope# 'ClassA'.defmethod1[T](self):...defmethod2[T](self,x=T):# Parameter 'x' gets default value of 1...defmethod3[T](self,x:T):# Parameter 'x' has type T (scoped to method3)...
Symbols referenced in inner scopes are resolved using existing rules exceptthat type parameter scopes are also considered during name resolution.
T=0# T refers to the global variableprint(T)# Prints 0classOuter[T]:T=1# T refers to the local variable scoped to class 'Outer'print(T)# Prints 1classInner1:T=2# T refers to the local type variable within 'Inner1'print(T)# Prints 2definner_method(self):# T refers to the type parameter scoped to class 'Outer';# If 'Outer' did not use the new type parameter syntax,# this would instead refer to the global variable 'T'print(T)# Prints 'T'defouter_method(self):T=3# T refers to the local variable within 'outer_method'print(T)# Prints 3definner_func():# T refers to the variable captured from 'outer_method'print(T)# Prints 3
When the new type parameter syntax is used for a generic class, assignmentexpressions are not allowed within the argument list for the class definition.Likewise, with functions that use the new type parameter syntax, assignmentexpressions are not allowed within parameter or return type annotations, norare they allowed within the expression that defines a type alias, or withinthe bounds and constraints of aTypeVar. Similarly,yield,yieldfrom,andawait expressions are disallowed in these contexts.
This restriction is necessary because expressions evaluated within thenew lexical scope should not introduce symbols within that scope other thanthe defined type parameters, and should not affect whether the enclosing functionis a generator or coroutine.
classClassA[T]((x:=Sequence[T])):...# Syntax error: assignment expression not alloweddeffunc1[T](val:(x:=int)):...# Syntax error: assignment expression not alloweddeffunc2[T]()->(x:=Sequence[T]):...# Syntax error: assignment expression not allowedtypeAlias1[T]=(x:=list[T])# Syntax error: assignment expression not allowed
A new attribute called__type_params__ is available on generic classes,functions, and type aliases. This attribute is a tuple of thetype parameters that parameterize the class, function, or alias.The tuple containsTypeVar,ParamSpec, andTypeVarTuple instances.
Type parameters declared using the new syntax will not appear within thedictionary returned byglobals() orlocals().
This PEP eliminates the need for variance to be specified for typeparameters. Instead, type checkers will infer the variance of type parametersbased on their usage within a class. Type parameters are inferred to beinvariant, covariant, or contravariant depending on how they are used.
Python type checkers already include the ability to determine the variance oftype parameters for the purpose of validating variance within a genericprotocol class. This capability can be used for all classes (whether or notthey are protocols) to calculate the variance of each type parameter.
The algorithm for computing the variance of a type parameter is as follows.
For each type parameter in a generic class:
1. If the type parameter is variadic (TypeVarTuple) or a parameterspecification (ParamSpec), it is always considered invariant. No furtherinference is needed.
2. If the type parameter comes from a traditionalTypeVar declaration andis not specified asinfer_variance (see below), its variance is specifiedby theTypeVar constructor call. No further inference is needed.
3. Create two specialized versions of the class. We’ll refer to these asupper andlower specializations. In both of these specializations,replace all type parameters other than the one being inferred by a dummy typeinstance (a concrete anonymous class that is type compatible with itself andassumed to meet the bounds or constraints of the type parameter). Intheupper specialized class, specialize the target type parameter withanobject instance. This specialization ignores the type parameter’supper bound or constraints. In thelower specialized class, specializethe target type parameter with itself (i.e. the corresponding type argumentis the type parameter itself).
4. Determine whetherlower can be assigned toupper using normal typecompatibility rules. If so, the target type parameter is covariant. If not,determine whetherupper can be assigned tolower. If so, the targettype parameter is contravariant. If neither of these combinations areassignable, the target type parameter is invariant.
Here is an example.
classClassA[T1,T2,T3](list[T1]):defmethod1(self,a:T2)->None:...defmethod2(self)->T3:...
To determine the variance ofT1, we specializeClassA as follows:
upper=ClassA[object,Dummy,Dummy]lower=ClassA[T1,Dummy,Dummy]
We find thatupper is not assignable tolower using normal typecompatibility rules defined inPEP 484. Likewise,lower is not assignabletoupper, so we conclude thatT1 is invariant.
To determine the variance ofT2, we specializeClassA as follows:
upper=ClassA[Dummy,object,Dummy]lower=ClassA[Dummy,T2,Dummy]
Sinceupper is assignable tolower,T2 is contravariant.
To determine the variance ofT3, we specializeClassA as follows:
upper=ClassA[Dummy,Dummy,object]lower=ClassA[Dummy,Dummy,T3]
Sincelower is assignable toupper,T3 is covariant.
The existingTypeVar class constructor accepts keyword parameters namedcovariant andcontravariant. If both of these areFalse, thetype variable is assumed to be invariant. We propose to add another keywordparameter namedinfer_variance indicating that a type checker should useinference to determine whether the type variable is invariant, covariant orcontravariant. A corresponding instance variable__infer_variance__ can beaccessed at runtime to determine whether the variance is inferred. Typevariables that are implicitly allocated using the new syntax will alwayshave__infer_variance__ set toTrue.
A generic class that uses the traditional syntax may include combinations oftype variables with explicit and inferred variance.
T1=TypeVar("T1",infer_variance=True)# Inferred varianceT2=TypeVar("T2")# InvariantT3=TypeVar("T3",covariant=True)# Covariant# A type checker should infer the variance for T1 but use the# specified variance for T2 and T3.classClassA(Generic[T1,T2,T3]):...
The existing mechanism for allocatingTypeVar,TypeVarTuple, andParamSpec is retained for backward compatibility. However, these“traditional” type variables should not be combined with type parametersallocated using the new syntax. Such a combination should be flagged asan error by type checkers. This is necessary because the type parameterorder is ambiguous.
It is OK to combine traditional type variables with new-style type parametersif the class, function, or type alias does not use the new syntax. Thenew-style type parameters must come from an outer scope in this case.
K=TypeVar("K")classClassA[V](dict[K,V]):...# Type checker errorclassClassB[K,V](dict[K,V]):...# OKclassClassC[V]:# The use of K and V for "method1" is OK because it uses the# "traditional" generic function mechanism where type parameters# are implicit. In this case V comes from an outer scope (ClassC)# and K is introduced implicitly as a type parameter for "method1".defmethod1(self,a:V,b:K)->V|K:...# The use of M and K are not allowed for "method2". A type checker# should generate an error in this case because this method uses the# new syntax for type parameters, and all type parameters associated# with the method must be explicitly declared. In this case, ``K``# is not declared by "method2", nor is it supplied by a new-style# type parameter defined in an outer scope.defmethod2[M](self,a:M,b:K)->M|K:...
This PEP introduces a new soft keywordtype. It modifies the grammarin the following ways:
class anddef statements.type_params:'['t=type_param_seq']'type_param_seq:a[asdl_typeparam_seq*]=','.type_param+[',']type_param:|a=NAMEb=[type_param_bound]|'*'a=NAME|'**'a=NAMEtype_param_bound:":"e=expression# Grammar definitions for class_def_raw and function_def_raw are modified# to reference type_params as an optional syntax element. The definitions# of class_def_raw and function_def_raw are simplified here for brevity.class_def_raw:'class'n=NAMEt=[type_params]...function_def_raw:a=[ASYNC]'def'n=NAMEt=[type_params]...
type statement for defining type aliases.type_alias:"type"n=NAMEt=[type_params]'='b=expression
This PEP introduces a new AST node type calledTypeAlias.
TypeAlias(exprname,typeparam*typeparams,exprvalue)
It also adds an AST node type that represents a type parameter.
typeparam = TypeVar(identifier name, expr? bound) | ParamSpec(identifier name) | TypeVarTuple(identifier name)
Bounds and constraints are represented identically in the AST. In the implementation,any expression that is aTuple AST node is treated as a constraint, and any otherexpression is treated as a bound.
It also modifies existing AST node typesFunctionDef,AsyncFunctionDefandClassDef to include an additional optional attribute calledtypeparams that includes a list of type parameters associated with thefunction or class.
This PEP introduces three new contexts where expressions may occur that representstatic types:TypeVar bounds,TypeVar constraints, and the value of typealiases. These expressions may contain references to namesthat are not yet defined. For example, type aliases may be recursive, or even mutuallyrecursive, and type variable bounds may refer back to the current class. If theseexpressions were evaluated eagerly, users would need to enclose such expressions inquotes to prevent runtime errors.PEP 563 andPEP 649 detail the problems withthis situation for type annotations.
To prevent a similar situation with the new syntax proposed in this PEP, we proposeto use lazy evaluation for these expressions, similar to the approach inPEP 649.Specifically, each expression will be saved in a code object, and the code objectis evaluated only when the corresponding attribute is accessed (TypeVar.__bound__,TypeVar.__constraints__, orTypeAlias.__value__). After the value issuccessfully evaluated, the value is saved and later calls will return the same valuewithout re-evaluating the code object.
IfPEP 649 is implemented, additional evaluation mechanisms should be added tomirror the options that PEP provides for annotations. In the current version of thePEP, that might include adding an__evaluate_bound__ method toTypeVar takingaformat parameter with the same meaning as in PEP 649’s__annotate__ method(and a similar__evaluate_constraints__ method, as well as an__evaluate_value__method onTypeAliasType).However, until PEP 649 is accepted and implemented, only the default evaluation format(PEP 649’s “VALUE” format) will be supported.
As a consequence of lazy evaluation, the value observed for an attribute maydepend on the time the attribute is accessed.
X=intclassFoo[T:X,U:X]:t,u=T,Uprint(Foo.t.__bound__)# prints "int"X=strprint(Foo.u.__bound__)# prints "str"
Similar examples affecting type annotations can be constructed using thesemantics of PEP 563 or PEP 649.
A naive implementation of lazy evaluation would handle class namespacesincorrectly, because functions within a class do not normally have access tothe enclosing class namespace. The implementation will retain a reference tothe class namespace so that class-scoped names are resolved correctly.
The new syntax requires a new kind of scope that behaves differentlyfrom existing scopes in Python. Thus, the new syntax cannot be described exactly in terms ofexisting Python scoping behavior. This section specifies these scopesfurther by reference to existing scoping behavior: the new scopes behavelike function scopes, except for a number of minor differences listed below.
All examples include functions introduced with the pseudo-keyworddef695.This keyword will not exist in the actual language; it is used toclarify that the new scopes are for the most part like function scopes.
def695 scopes differ from regular function scopes in the following ways:
def695 scope is immediately within a class scope, or within anotherdef695 scope that is immediately within a class scope, then names definedin that class scope can be accessed within thedef695 scope. (Regular functions,by contrast, cannot access names defined within an enclosing class scope.)def695 scope, thoughthey may be used within other scopes nested inside adef695 scope:yieldyieldfromawait:= (walrus operator)__qualname__) of objects (classes and functions) defined withindef695 scopesis as if the objects were defined within the closest enclosing scope.def695 scopes cannot be rebound with anonlocal statement in nested scopes.def695 scopes are used for the evaluation of several new syntactic constructs proposedin this PEP. Some are evaluated eagerly (when a type alias, function, or class is defined); others areevaluated lazily (only when evaluation is specifically requested). In all cases, the scoping semantics are identical:
In the below translations, names that start with two underscores are internal to the implementationand not visible to actual Python code. We use the following intrinsic functions, which in the realimplementation are defined directly in the interpreter:
__make_typealias(*,name,type_params=(),evaluate_value): Creates a newtyping.TypeAlias object with the givenname, type parameters, and lazily evaluated value. The value is not evaluated until the__value__ attributeis accessed.__make_typevar_with_bound(*,name,evaluate_bound): Creates a newtyping.TypeVar object with the givenname and lazily evaluated bound. The bound is not evaluated until the__bound__ attribute is accessed.__make_typevar_with_constraints(*,name,evaluate_constraints): Creates a newtyping.TypeVar object with the givenname and lazily evaluated constraints. The constraints are not evaluated until the__constraints__ attributeis accessed.Non-generic type aliases are translated as follows:
typeAlias=int
Equivalent to:
def695__evaluate_Alias():returnintAlias=__make_typealias(name='Alias',evaluate_value=__evaluate_Alias)
Generic type aliases:
typeAlias[T:int]=list[T]
Equivalent to:
def695__generic_parameters_of_Alias():def695__evaluate_T_bound():returnintT=__make_typevar_with_bound(name='T',evaluate_bound=__evaluate_T_bound)def695__evaluate_Alias():returnlist[T]return__make_typealias(name='Alias',type_params=(T,),evaluate_value=__evaluate_Alias)Alias=__generic_parameters_of_Alias()
Generic functions:
deff[T](x:T)->T:returnx
Equivalent to:
def695__generic_parameters_of_f():T=typing.TypeVar(name='T')deff(x:T)->T:returnxf.__type_params__=(T,)returnff=__generic_parameters_of_f()
A fuller example of generic functions, illustrating the scoping behavior of defaults, decorators, and bounds.Note that this example does not useParamSpec correctly, so it should be rejected by a static type checker.It is however valid at runtime, and it us used here to illustrate the runtime semantics.
@decoratordeff[T:int,U:(int,str),*Ts,**P](x:T=SOME_CONSTANT,y:U,*args:*Ts,**kwargs:P.kwargs,)->T:returnx
Equivalent to:
__default_of_x=SOME_CONSTANT# evaluated outside the def695 scopedef695__generic_parameters_of_f():def695__evaluate_T_bound():returnintT=__make_typevar_with_bound(name='T',evaluate_bound=__evaluate_T_bound)def695__evaluate_U_constraints():return(int,str)U=__make_typevar_with_constraints(name='U',evaluate_constraints=__evaluate_U_constraints)Ts=typing.TypeVarTuple("Ts")P=typing.ParamSpec("P")deff(x:T=__default_of_x,y:U,*args:*Ts,**kwargs:P.kwargs)->T:returnxf.__type_params__=(T,U,Ts,P)returnff=decorator(__generic_parameters_of_f())
Generic classes:
classC[T](Base):def__init__(self,x:T):self.x=x
Equivalent to:
def695__generic_parameters_of_C():T=typing.TypeVar('T')classC(Base):__type_params__=(T,)def__init__(self,x:T):self.x=xreturnCC=__generic_parameters_of_C()
The biggest divergence from existing behavior fordef695 scopesis the behavior within class scopes. This divergence is necessaryso that generics defined within classes behave in an intuitive way:
classC:classNested:...defgeneric_method[T](self,x:T,y:Nested)->T:...
Equivalent to:
classC:classNested:...def695__generic_parameters_of_generic_method():T=typing.TypeVar('T')defgeneric_method(self,x:T,y:Nested)->T:...returngeneric_methodgeneric_method=__generic_parameters_of_generic_method()
In this example, the annotations forx andy are evaluated withinadef695 scope, because they need access to the type parameterTfor the generic method. However, they also need access to theNestedname defined within the class namespace. Ifdef695 scopes behavedlike regular function scopes,Nested would not be visible within thefunction scope. Therefore,def695 scopes that are immediately withinclass scopes have access to that class scope, as described above.
Several classes in thetyping module that are currently implemented inPython must be partially implemented in C. This includesTypeVar,TypeVarTuple,ParamSpec, andGeneric, and the new classTypeAliasType (described above). The implementation may delegate to thePython version oftyping.py for some behaviors that interact heavily withthe rest of the module. Thedocumented behaviors of these classes should not change.
This proposal is prototyped inCPython PR #103764.
The Pyright type checker supports the behavior described in this PEP.
We explored various syntactic options for specifying type parameters thatprecededdef andclass statements. One such variant we consideredused ausing clause as follows:
usingS,TclassClassA:...
This option was rejected because the scoping rules for the type parameterswere less clear. Also, this syntax did not interact well with class andfunction decorators, which are common in Python. Only one other popularprogramming language, C++, uses this approach.
We likewise considered prefix forms that looked like decorators (e.g.,@using(S,T)). This idea was rejected because such forms would be confusedwith regular decorators, and they would not compose well with existingdecorators. Furthermore, decorators are logically executed after the statementthey are decorating, so it would be confusing for them to introduce symbols(type parameters) that are visible within the “decorated” statement, which islogically executed before the decorator itself.
Many languages that support generics make use of angle brackets. (Refer tothe table at the end of Appendix A for a summary.) We explored the use ofangle brackets for type parameter declarations in Python, but we ultimatelyrejected it for two reasons. First, angle brackets are not considered“paired” by the Python scanner, so end-of-line characters between a<and> token are retained. That means any line breaks within a list oftype parameters would require the use of unsightly and cumbersome\ escapesequences. Second, Python has already established the use of square bracketsfor explicit specialization of a generic type (e.g.,list[int]). Weconcluded that it would be inconsistent and confusing to use angle bracketsfor generic declarations but square brackets for explicit specialization. Allother languages that we surveyed were consistent in this regard.
We explored various syntactic options for specifying the bounds and constraintsfor a type variable. We considered, but ultimately rejected, the useof a<: token like in Scala, the use of anextends orwithkeyword like in various other languages, and the use of a function callsyntax similar to today’styping.TypeVar constructor. The simple colonsyntax is consistent with many other programming languages (see Appendix A),and it was heavily preferred by a cross section of Python developers whowere surveyed.
We considered adding syntax for specifying whether a type parameter is intendedto be invariant, covariant, or contravariant. Thetyping.TypeVar mechanismin Python requires this. A few other languages including Scala and C#also require developers to specify the variance. We rejected this idea becausevariance can generally be inferred, and most modern programming languagesdo infer variance based on usage. Variance is an advanced topic thatmany developers find confusing, so we want to eliminate the need tounderstand this concept for most Python developers.
When considering implementation options, we considered a “name mangling”approach where each type parameter was given a unique “mangled” name by thecompiler. This mangled name would be based on the qualified name of thegeneric class, function or type alias it was associated with. This approachwas rejected because qualified names are not necessarily unique, which meansthe mangled name would need to be based on some other randomized value.Furthermore, this approach is not compatible with techniques used forevaluating quoted (forward referenced) type annotations.
Support for generic types is found in many programming languages. In thissection, we provide a survey of the options used by other popular programminglanguages. This is relevant because familiarity with other languages willmake it easier for Python developers to understand this concept. We provideadditional details here (for example, default type argument support) thatmay be useful when considering future extensions to the Python type system.
C++ uses angle brackets in combination with keywordstemplate andtypename to declare type parameters. It uses angle brackets forspecialization.
C++20 introduced the notion of generalized constraints, which can actlike protocols in Python. A collection of constraints can be defined ina named entity called aconcept.
Variance is not explicitly specified, but constraints can enforce variance.
A default type argument can be specified using the= operator.
// Generic classtemplate<typenameT>classClassA{// Constraints are supported through compile-time assertions.static_assert(std::is_base_of<BaseClass,T>::value);public:Container<T>t;};// Generic function with default type argumenttemplate<typenameS=int>Sfunc1(ClassA<S>a,Sb){};// C++20 introduced a more generalized notion of "constraints"// and "concepts", which are named constraints.// A sample concepttemplate<typenameT>conceptHashable=requires(Ta){{std::hash<T>{}(a)}->std::convertible_to<std::size_t>;};// Use of a concept in a templatetemplate<HashableT>voidfunc2(Tvalue){}// Alternative use of concepttemplate<typenameT>requiresHashable<T>voidfunc3(Tvalue){}// Alternative use of concepttemplate<typenameT>voidfunc3(Tvalue)requiresHashable<T>{}
Java uses angle brackets to declare type parameters and for specialization.By default, type parameters are invariant.Theextends keyword is used to specify an upper bound. Thesuper keywordis used to specify a contravariant bound.
Java uses use-site variance. The compiler places limits on which methods andmembers can be accessed based on the use of a generic type. Variance isnot specified explicitly.
Java provides no way to specify a default type argument.
// Generic classpublicclassClassA<T>{publicContainer<T>t;// Generic methodpublic<SextendsNumber>voidmethod1(Svalue){}// Use site variancepublicvoidmethod1(ClassA<?superInteger>value){}}
C# uses angle brackets to declare type parameters and for specialization.Thewhere keyword and a colon is used to specify the bound for a typeparameter.
C# uses declaration-site variance using the keywordsin andout forcontravariance and covariance, respectively. By default, type parameters areinvariant.
C# provides no way to specify a default type argument.
// Generic class with bounds on type parameterspublicclassClassA<S,T>whereT:SomeClass1whereS:SomeClass2{// Generic methodpublicvoidMyMethod<U>(Uvalue)whereU:SomeClass3{}}// Contravariant and covariant type parameterspublicclassClassB<inS,outT>{publicTMyMethod(Svalue){}}
TypeScript uses angle brackets to declare type parameters and forspecialization. Theextends keyword is used to specify a bound. It can becombined with other type operators such askeyof.
TypeScript uses declaration-site variance. Variance is inferred fromusage, not specified explicitly. TypeScript 4.7 introduced the abilityto specify variance usingin andout keywords. This was added to handleextremely complex types where inference of variance was expensive.
A default type argument can be specified using the= operator.
TypeScript supports thetype keyword to declare a type alias, and thissyntax supports generics.
// Generic interfaceinterfaceInterfaceA<S,TextendsSomeInterface1>{val1:S;val2:T;method1<UextendsSomeInterface2>(val:U):S}// Generic functionfunctionfunc1<T,KextendskeyofT>(ojb:T,key:K){}// Contravariant and covariant type parameters (TypeScript 4.7)interfaceInterfaceB<inS,outT>{}// Type parameter with defaultinterfaceInterfaceC<T=SomeInterface3>{}// Generic type aliastypeMyType<TextendsSomeInterface4>=Array<T>
In Scala, square brackets are used to declare type parameters. Squarebrackets are also used for specialization. The<: and>: operatorsare used to specify upper and lower bounds, respectively.
Scala uses use-site variance but also allows declaration-site variancespecification. It uses a+ or- prefix operator for covariance andcontravariance, respectively.
Scala provides no way to specify a default type argument.
It does support higher-kinded types (type parameters that accept typetype parameters).
// Generic class; type parameter has upper boundclassClassA[A<:SomeClass1]{// Generic method; type parameter has lower bounddefmethod1[B>:A](val:B)...}// Use of an upper and lower bound with the same type parameterclassClassB[A>:SomeClass1<:SomeClass2]{}// Contravariant and covariant type parametersclassClassC[+A,-B]{}// Higher-kinded typetraitCollection[T[_]]{defmethod1[A](a:A):T[A]defmethod2[B](b:T[B]):B}// Generic type aliastypeMyType[T<:Int]=Container[T]
Swift uses angle brackets to declare type parameters and for specialization.The upper bound of a type parameter is specified using a colon.
Swift doesn’t support generic variance; all type parameters are invariant.
Swift provides no way to specify a default type argument.
// Generic classclassClassA<T>{// Generic methodfuncmethod1<X>(val:T)->X{}}// Type parameter with upper bound constraintclassClassB<T:SomeClass1>{}// Generic type aliastypealiasMyType<A>=Container<A>
Rust uses angle brackets to declare type parameters and for specialization.The upper bound of a type parameter is specified using a colon. Alternativelyawhere clause can specify various constraints.
Rust does not have traditional object oriented inheritance or variance.Subtyping in Rust is very restricted and occurs only due to variance withrespect to lifetimes.
A default type argument can be specified using the= operator.
// Generic classstructStructA<T>{// T's lifetime is inferred as covariantx:T}fnf<'a>(mutshort_lifetime:StructA<&'ai32>,mutlong_lifetime:StructA<&'statici32>,){long_lifetime=short_lifetime;// error: StructA<&'a i32> is not a subtype of StructA<&'static i32>short_lifetime=long_lifetime;// valid: StructA<&'static i32> is a subtype of StructA<&'a i32>}// Type parameter with boundstructStructB<T:SomeTrait>{}// Type parameter with additional constraintsstructStructC<T>whereT:Iterator,T::Item:Copy{}// Generic functionfnfunc1<T>(val:&[T])->T{}// Generic type aliastypeMyType<T>=StructC<T>;
Kotlin uses angle brackets to declare type parameters and for specialization.By default, type parameters are invariant. The upper bound of a type isspecified using a colon.Alternatively, awhere clause can specify various constraints.
Kotlin supports declaration-site variance where variance of type parameters isexplicitly declared usingin andout keywords. It also supports use-sitevariance which limits which methods and members can be used.
Kotlin provides no way to specify a default type argument.
// Generic classclassClassA<T>// Type parameter with upper boundclassClassB<T:SomeClass1>// Contravariant and covariant type parametersclassClassC<inS,outT>// Generic functionfun<T>func1():T{// Use site variancevalcovariantA:ClassA<outNumber>valcontravariantA:ClassA<inNumber>}// Generic type aliastypealiasTypeAliasFoo<T>=ClassA<T>
Julia uses curly braces to declare type parameters and for specialization.The<: operator can be used within awhere clause to declareupper and lower bounds on a type.
# Generic struct; type parameter with upper and lower bounds# Valid for T in (Int64, Signed, Integer, Real, Number)structContainer{Int<:T<:Number}x::Tend# Generic functionfunctionfunc1(v::Container{T})whereT<:Realend# Alternate forms of generic functionfunctionfunc2(v::Container{T}whereT<:Real)endfunctionfunc3(v::Container{<:Real})end# Tuple types are covariant# Valid for func4((2//3, 3.5))functionfunc4(t::Tuple{Real,Real})end
Dart uses angle brackets to declare type parameters and for specialization.The upper bound of a type is specified using theextends keyword.By default, type parameters are covariant.
Dart supports declaration-site variance, where variance of type parameters isexplicitly declared usingin,out andinout keywords.It does not support use-site variance.
Dart provides no way to specify a default type argument.
// Generic classclassClassA<T>{}// Type parameter with upper boundclassClassB<TextendsSomeClass1>{}// Contravariant and covariant type parametersclassClassC<inS,outT>{}// Generic functionTfunc1<T>(){}// Generic type aliastypedefTypeDefFoo<T>=ClassA<T>;
Go uses square brackets to declare type parameters and for specialization.The upper bound of a type is specified after the name of the parameter, andmust always be specified. The keywordany is used for an unbound type parameter.
Go doesn’t support variance; all type parameters are invariant.
Go provides no way to specify a default type argument.
Go does not support generic type aliases.
// Generic type without a boundtypeTypeA[Tany]struct{tT}// Type parameter with upper boundtypeTypeB[TSomeType1]struct{}// Generic functionfuncfunc1[Tany](){}
| DeclSyntax | UpperBound | LowerBound | DefaultValue | VarianceSite | Variance | |
|---|---|---|---|---|---|---|
| C++ | template<> | n/a | n/a | = | n/a | n/a |
| Java | <> | extends | use | super,extends | ||
| C# | <> | where | decl | in, out | ||
| TypeScript | <> | extends | = | decl | inferred,in, out | |
| Scala | [] | T <: X | T >: X | use, decl | +, - | |
| Swift | <> | T: X | n/a | n/a | ||
| Rust | <> | T: X,where | = | n/a | n/a | |
| Kotlin | <> | T: X,where | use, decl | in, out | ||
| Julia | {} | T <: X | X <: T | n/a | n/a | |
| Dart | <> | extends | decl | in, out,inout | ||
| Go | [] | T X | n/a | n/a | ||
| Python(proposed) | [] | T: X | decl | inferred |
Thanks to Sebastian Rittau for kick-starting the discussions that led to thisproposal, to Jukka Lehtosalo for proposing the syntax for type aliasstatements and to Jelle Zijlstra, Daniel Moisset, and Guido van Rossumfor their valuable feedback and suggested improvements to the specificationand implementation.
This document is placed in the public domain or under the CC0-1.0-Universallicense, whichever is more permissive.
Source:https://github.com/python/peps/blob/main/peps/pep-0695.rst
Last modified:2025-07-07 12:42:34 GMT