Movatterモバイル変換


[0]ホーム

URL:


Following system colour schemeSelected dark colour schemeSelected light colour scheme

Python Enhancement Proposals

PEP 696 – Type Defaults for Type Parameters

Author:
James Hilton-Balfe <gobot1234yt at gmail.com>
Sponsor:
Jelle Zijlstra <jelle.zijlstra at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Topic:
Typing
Created:
14-Jul-2022
Python-Version:
3.13
Post-History:
22-Mar-2022,08-Jan-2023
Resolution:
Discourse message

Table of Contents

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.

Abstract

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.

Motivation

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:

  • discord.py — where theexample above was taken from.
  • NumPy — the default for typeslikendarray’sdtype would befloat64. Currently it’sUnknown orAny.
  • TensorFlow — thiscould be used for Tensor similarly tonumpy.ndarray and would beuseful to simplify the definition ofLayer.

Specification

Default Ordering and Subscription Rules

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 Defaults

ParamSpec 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 Defaults

TypeVarTuple 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]

Using Another Type Parameter asdefault

This 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.

Scoping Rules

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.

Bound Rules

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

Constraint Rules

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 as Parameters to Generics

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]

Specialisation Rules

Type parameters currently cannot be further subscripted. This mightchange ifHigher Kinded TypeVarsare implemented.

GenericTypeAliases

GenericTypeAliases 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

Subclassing

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]>

Usingbound anddefault

If 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

Constraints

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

Function Defaults

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

Defaults followingTypeVarTuple

ATypeVar 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]

Subtyping

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 Defaults

Using aTypeVarTuple as a default is not supported because:

  • Scoping Rules does not allow usage of type parametersfrom outer scopes.
  • MultipleTypeVarTuples 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.

Binding rules

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]

Implementation

At runtime, this would involve the following changes to thetypingmodule.

  • The classesTypeVar,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:

  • logic to determine the defaults required for a subscription.
  • ideally, logic to determine if subscription (likeGeneric[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.

Grammar changes

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.

Rejected Alternatives

Allowing the Type Parameters Defaults to Be Passed totype.__new__’s**kwargs

T=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

Allowing Non-defaults to Follow Defaults

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]

Havingdefault Implicitly Bebound

In 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.

Allowing Type Parameters With Defaults To Be Used in Function Signatures

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.

Allowing Type Parameters from Outer Scopes indefault

This 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.

Acknowledgements

Thanks to the following people for their feedback on the PEP:

Eric Traut, Jelle Zijlstra, Joshua Butt, Danny Yamamoto, Kaylynn Morganand Jakub Kuczys

Copyright

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


[8]ページ先頭

©2009-2025 Movatter.jp