Important
This PEP is a historical document: seeDefaults for Type Parameters andType parameter lists 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 introduces the concept of type defaults for type parameters,includingTypeVar,ParamSpec, andTypeVarTuple,which act as defaults for type parameters for which no type is specified.
Default type argument support is available in some popular languagessuch as C++, TypeScript, and Rust. A survey of type parameter syntax insome common languages has been conducted by the author ofPEP 695and can be found in itsAppendix A.
T=TypeVar("T",default=int)# This means that if no type is specified T = int@dataclassclassBox(Generic[T]):value:T|None=Nonereveal_type(Box())# type is Box[int]reveal_type(Box(value="Hello World!"))# type is Box[str]
One place thisregularly comesup isGenerator. Ipropose changing thestub definition to something like:
YieldT=TypeVar("YieldT")SendT=TypeVar("SendT",default=None)ReturnT=TypeVar("ReturnT",default=None)classGenerator(Generic[YieldT,SendT,ReturnT]):...Generator[int]==Generator[int,None]==Generator[int,None,None]
This is also useful for aGeneric that is commonly over one type.
classBot:...BotT=TypeVar("BotT",bound=Bot,default=Bot)classContext(Generic[BotT]):bot:BotTclassMyBot(Bot):...reveal_type(Context().bot)# type is Bot # notice this is not Any which is what it would be currentlyreveal_type(Context[MyBot]().bot)# type is MyBot
Not only does this improve typing for those who explicitly use it, italso helps non-typing users who rely on auto-complete to speed up theirdevelopment.
This design pattern is common in projects like:
ndarray’sdtype would befloat64. Currently it’sUnknown orAny.numpy.ndarray and would beuseful to simplify the definition ofLayer.The order for defaults should follow the standard function parameterrules, so a type parameter with nodefault cannot follow one withadefault value. Doing so should ideally raise aTypeError intyping._GenericAlias/types.GenericAlias, and a type checkershould flag this as an error.
DefaultStrT=TypeVar("DefaultStrT",default=str)DefaultIntT=TypeVar("DefaultIntT",default=int)DefaultBoolT=TypeVar("DefaultBoolT",default=bool)T=TypeVar("T")T2=TypeVar("T2")classNonDefaultFollowsDefault(Generic[DefaultStrT,T]):...# Invalid: non-default TypeVars cannot follow ones with defaultsclassNoNonDefaults(Generic[DefaultStrT,DefaultIntT]):...(NoNoneDefaults==NoNoneDefaults[str]==NoNoneDefaults[str,int])# All validclassOneDefault(Generic[T,DefaultBoolT]):...OneDefault[float]==OneDefault[float,bool]# Validreveal_type(OneDefault)# type is type[OneDefault[T, DefaultBoolT = bool]]reveal_type(OneDefault[float]())# type is OneDefault[float, bool]classAllTheDefaults(Generic[T1,T2,DefaultStrT,DefaultIntT,DefaultBoolT]):...reveal_type(AllTheDefaults)# type is type[AllTheDefaults[T1, T2, DefaultStrT = str, DefaultIntT = int, DefaultBoolT = bool]]reveal_type(AllTheDefaults[int,complex]())# type is AllTheDefaults[int, complex, str, int, bool]AllTheDefaults[int]# Invalid: expected 2 arguments to AllTheDefaults(AllTheDefaults[int,complex]==AllTheDefaults[int,complex,str]==AllTheDefaults[int,complex,str,int]==AllTheDefaults[int,complex,str,int,bool])# All valid
With the new Python 3.12 syntax for generics (introduced byPEP 695), this canbe enforced at compile time:
typeAlias[DefaultT=int,T]=tuple[DefaultT,T]# SyntaxError: non-default TypeVars cannot follow ones with defaultsdefgeneric_func[DefaultT=int,T](x:DefaultT,y:T)->None:...# SyntaxError: non-default TypeVars cannot follow ones with defaultsclassGenericClass[DefaultT=int,T]:...# SyntaxError: non-default TypeVars cannot follow ones with defaults
ParamSpec DefaultsParamSpec defaults are defined using the same syntax asTypeVar s but use alist of types or an ellipsisliteral “...” or another in-scopeParamSpec (seeScoping Rules).
DefaultP=ParamSpec("DefaultP",default=[str,int])classFoo(Generic[DefaultP]):...reveal_type(Foo)# type is type[Foo[DefaultP = [str, int]]]reveal_type(Foo())# type is Foo[[str, int]]reveal_type(Foo[[bool,bool]]())# type is Foo[[bool, bool]]
TypeVarTuple DefaultsTypeVarTuple defaults are defined using the same syntax asTypeVar s but use an unpacked tuple of types instead of a single typeor another in-scopeTypeVarTuple (seeScoping Rules).
DefaultTs=TypeVarTuple("DefaultTs",default=Unpack[tuple[str,int]])classFoo(Generic[*DefaultTs]):...reveal_type(Foo)# type is type[Foo[DefaultTs = *tuple[str, int]]]reveal_type(Foo())# type is Foo[str, int]reveal_type(Foo[int,bool]())# type is Foo[int, bool]
defaultThis allows for a value to be used again when the type parameter to ageneric is missing but another type parameter is specified.
To use another type parameter as a default thedefault and thetype parameter must be the same type (aTypeVar’s default must beaTypeVar, etc.).
This could be used on builtins.slicewhere thestart parameter should default toint,stopdefault to the type ofstart and step default toint|None.
StartT=TypeVar("StartT",default=int)StopT=TypeVar("StopT",default=StartT)StepT=TypeVar("StepT",default=int|None)classslice(Generic[StartT,StopT,StepT]):...reveal_type(slice)# type is type[slice[StartT = int, StopT = StartT, StepT = int | None]]reveal_type(slice())# type is slice[int, int, int | None]reveal_type(slice[str]())# type is slice[str, str, int | None]reveal_type(slice[str,bool,timedelta]())# type is slice[str, bool, timedelta]T2=TypeVar("T2",default=DefaultStrT)classFoo(Generic[DefaultStrT,T2]):def__init__(self,a:DefaultStrT,b:T2)->None:...reveal_type(Foo(1,""))# type is Foo[int, str]Foo[int](1,"")# Invalid: Foo[int, str] cannot be assigned to self: Foo[int, int] in Foo.__init__Foo[int]("",1)# Invalid: Foo[str, int] cannot be assigned to self: Foo[int, int] in Foo.__init__
When using a type parameter as the default to another type parameter, thefollowing rules apply, whereT1 is the default forT2.
T1 must be used beforeT2 in the parameter list of the generic.
T2=TypeVar("T2",default=T1)classFoo(Generic[T1,T2]):...# ValidclassFoo(Generic[T1]):classBar(Generic[T2]):...# ValidStartT=TypeVar("StartT",default="StopT")# Swapped defaults around from previous exampleStopT=TypeVar("StopT",default=int)classslice(Generic[StartT,StopT,StepT]):...# ^^^^^^ Invalid: ordering does not allow StopT to be bound
Using a type parameter from an outer scope as a default is not supported.
T1’s bound must be a subtype ofT2’s bound.
T1=TypeVar("T1",bound=int)TypeVar("Ok",default=T1,bound=float)# ValidTypeVar("AlsoOk",default=T1,bound=int)# ValidTypeVar("Invalid",default=T1,bound=str)# Invalid: int is not a subtype of str
The constraints ofT2 must be a superset of the constraints ofT1.
T1=TypeVar("T1",bound=int)TypeVar("Invalid",float,str,default=T1)# Invalid: upper bound int is incompatible with constraints float or strT1=TypeVar("T1",int,str)TypeVar("AlsoOk",int,str,bool,default=T1)# ValidTypeVar("AlsoInvalid",bool,complex,default=T1)# Invalid: {bool, complex} is not a superset of {int, str}
Type parameters are valid as parameters to generics inside of adefault when the first parameter is in scope as determined by theprevious section.
T=TypeVar("T")ListDefaultT=TypeVar("ListDefaultT",default=list[T])classBar(Generic[T,ListDefaultT]):def__init__(self,x:T,y:ListDefaultT):...reveal_type(Bar)# type is type[Bar[T, ListDefaultT = list[T]]]reveal_type(Bar[int])# type is type[Bar[int, list[int]]]reveal_type(Bar[int]())# type is Bar[int, list[int]]reveal_type(Bar[int,list[str]]())# type is Bar[int, list[str]]reveal_type(Bar[int,str]())# type is Bar[int, str]
Type parameters currently cannot be further subscripted. This mightchange ifHigher Kinded TypeVarsare implemented.
GenericTypeAliasesGenericTypeAliases should be able to be further subscriptedfollowing normal subscription rules. If a type parameter has a defaultthat hasn’t been overridden it should be treated like it wassubstituted into theTypeAlias. However, it can be specialisedfurther down the line.
classSomethingWithNoDefaults(Generic[T,T2]):...MyAlias:TypeAlias=SomethingWithNoDefaults[int,DefaultStrT]# Validreveal_type(MyAlias)# type is type[SomethingWithNoDefaults[int, DefaultStrT]]reveal_type(MyAlias[bool]())# type is SomethingWithNoDefaults[int, bool]MyAlias[bool,int]# Invalid: too many arguments passed to MyAlias
Subclasses ofGenerics with type parameters that have defaultsbehave similarly toGenericTypeAliases. That is, subclasses can befurther subscripted following normal subscription rules, non-overriddendefaults should be substituted in, and type parameters with such defaults can befurther specialised down the line.
classSubclassMe(Generic[T,DefaultStrT]):x:DefaultStrTclassBar(SubclassMe[int,DefaultStrT]):...reveal_type(Bar)# type is type[Bar[DefaultStrT = str]]reveal_type(Bar())# type is Bar[str]reveal_type(Bar[bool]())# type is Bar[bool]classFoo(SubclassMe[float]):...reveal_type(Foo().x)# type is strFoo[str]# Invalid: Foo cannot be further subscriptedclassBaz(Generic[DefaultIntT,DefaultStrT]):...classSpam(Baz):...reveal_type(Spam())# type is <subclass of Baz[int, str]>
bound anddefaultIf bothbound anddefault are passeddefault must be asubtype ofbound. Otherwise the type checker should generate anerror.
TypeVar("Ok",bound=float,default=int)# ValidTypeVar("Invalid",bound=str,default=int)# Invalid: the bound and default are incompatible
For constrainedTypeVars, the default needs to be one of theconstraints. A type checker should generate an error even if it is asubtype of one of the constraints.
TypeVar("Ok",float,str,default=float)# ValidTypeVar("Invalid",float,str,default=int)# Invalid: expected one of float or str got int
In generic functions, type checkers may use a type parameter’s default when thetype parameter cannot be solved to anything. We leave the semantics of thisusage unspecified, as ensuring thedefault is returned in every code pathwhere the type parameter can go unsolved may be too hard to implement. Typecheckers are free to either disallow this case or experiment with implementingsupport.
T=TypeVar('T',default=int)deffunc(x:int|set[T])->T:...reveal_type(func(0))# a type checker may reveal T's default of int here
TypeVarTupleATypeVar that immediately follows aTypeVarTuple is not allowedto have a default, because it would be ambiguous whether a type argumentshould be bound to theTypeVarTuple or the defaultedTypeVar.
Ts=TypeVarTuple("Ts")T=TypeVar("T",default=bool)classFoo(Generic[Ts,T]):...# Type checker error# Could be reasonably interpreted as either Ts = (int, str, float), T = bool# or Ts = (int, str), T = floatFoo[int,str,float]
With the Python 3.12 built-in generic syntax, this case should raise a SyntaxError.
However, it is allowed to have aParamSpec with a default following aTypeVarTuple with a default, as there can be no ambiguity between a type argumentfor theParamSpec and one for theTypeVarTuple.
Ts=TypeVarTuple("Ts")P=ParamSpec("P",default=[float,bool])classFoo(Generic[Ts,P]):...# ValidFoo[int,str]# Ts = (int, str), P = [float, bool]Foo[int,str,[bytes]]# Ts = (int, str), P = [bytes]
Type parameter defaults do not affect the subtyping rules for generic classes.In particular, defaults can be ignored when considering whether a class iscompatible with a generic protocol.
TypeVarTuples as DefaultsUsing aTypeVarTuple as a default is not supported because:
TypeVarTuples cannot appear in the typeparameter list for a single object, as specified inPEP 646.These reasons leave no current valid location where aTypeVarTuple could be used as the default of anotherTypeVarTuple.
Type parameter defaults should be bound by attribute access(including call and subscript).
classFoo[T=int]:defmeth(self)->Self:returnselfreveal_type(Foo.meth)# type is (self: Foo[int]) -> Foo[int]
At runtime, this would involve the following changes to thetypingmodule.
TypeVar,ParamSpec, andTypeVarTuple shouldexpose the type passed todefault. This would be available asa__default__ attribute, which would beNone if no argumentis passed andNoneType ifdefault=None.The following changes would be required to bothGenericAliases:
Generic[T,DefaultT]) would be valid.The grammar for type parameter lists would need to be updated toallow defaults; see below.
A reference implementation of the runtime changes can be found athttps://github.com/Gobot1234/cpython/tree/pep-696
A reference implementation of the type checker can be found athttps://github.com/Gobot1234/mypy/tree/TypeVar-defaults
Pyright currently supports this functionality.
The syntax added inPEP 695 will be extended to introduce a wayto specify defaults for type parameters using the “=” operator insideof the square brackets like so:
# TypeVarsclassFoo[T=str]:...# ParamSpecsclassBaz[**P=[int,str]]:...# TypeVarTuplesclassQux[*Ts=*tuple[int,bool]]:...# TypeAliasestypeFoo[T,U=str]=Bar[T,U]typeBaz[**P=[int,str]]=Spam[**P]typeQux[*Ts=*tuple[str]]=Ham[*Ts]typeRab[U,T=str]=Bar[T,U]
Similarly to the bound for a type parameter,defaults should be lazily evaluated, with the same scoping rules toavoid the unnecessary usage of quotes around them.
This functionality was included in the initial draft ofPEP 695 butwas removed due to scope creep.
The following changes would be made to the grammar:
type_param:|a=NAMEb=[type_param_bound]d=[type_param_default]|a=NAMEc=[type_param_constraint]d=[type_param_default]|'*'a=NAMEd=[type_param_default]|'**'a=NAMEd=[type_param_default]type_param_default:|'='e=expression|'='e=starred_expression
The compiler would enforce that type parameters without defaults cannotfollow type parameters with defaults and thatTypeVars with defaultscannot immediately followTypeVarTuples.
type.__new__’s**kwargsT=TypeVar("T")@dataclassclassBox(Generic[T],T=int):value:T|None=None
While this is much easier to read and follows a similar rationale to theTypeVarunarysyntax, it would not bebackwards compatible asT might already be passed to ametaclass/superclass or support classes that don’t subclassGenericat runtime.
Ideally, ifPEP 637 wasn’t rejected, the following would be acceptable:
T=TypeVar("T")@dataclassclassBox(Generic[T=int]):value:T|None=None
YieldT=TypeVar("YieldT",default=Any)SendT=TypeVar("SendT",default=Any)ReturnT=TypeVar("ReturnT")classCoroutine(Generic[YieldT,SendT,ReturnT]):...Coroutine[int]==Coroutine[Any,Any,int]
Allowing non-defaults to follow defaults would alleviate the issues withreturning types likeCoroutine from functions where the most usedtype argument is the last (the return). Allowing non-defaults to followdefaults is too confusing and potentially ambiguous, even if only theabove two forms were valid. Changing the argument order now would alsobreak a lot of codebases. This is also solvable in most cases using aTypeAlias.
Coro:TypeAlias=Coroutine[Any,Any,T]Coro[int]==Coroutine[Any,Any,int]
default Implicitly BeboundIn an earlier version of this PEP, thedefault was implicitly settobound if no value was passed fordefault. This whileconvenient, could have a type parameter with no default follow atype parameter with a default. Consider:
T=TypeVar("T",bound=int)# default is implicitly intU=TypeVar("U")classFoo(Generic[T,U]):...# would expand toT=TypeVar("T",bound=int,default=int)U=TypeVar("U")classFoo(Generic[T,U]):...
This would have also been a breaking change for a small number of caseswhere the code relied onAny being the implicit default.
A previous version of this PEP allowedTypeVarLikes with defaults to be used infunction signatures. This was removed for the reasons described inFunction Defaults. Hopefully, this can be added in the future ifa way to get the runtime value of a type parameter is added.
defaultThis was deemed too niche a feature to be worth the added complexity.If any cases arise where this is needed, it can be added in a future PEP.
Thanks to the following people for their feedback on the PEP:
Eric Traut, Jelle Zijlstra, Joshua Butt, Danny Yamamoto, Kaylynn Morganand Jakub Kuczys
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-0696.rst
Last modified:2024-09-03 17:24:02 GMT