This PEP proposes allowingUnpack[TypedDict] in the parameter list insideCallable, enabling concise and type-safe ways to describe keyword-onlycallable signatures. Currently,Callable assumes positional-onlyparameters, and typing keyword-only functions requires verbose callbackprotocols. With this proposal, the keyword structure defined by aTypedDictcan be reused directly inCallable.
Thetyping specification states:
“Parameters specified using Callable are assumed to be positional-only.The Callable form provides no way to specify keyword-only parameters,or default argument values. For these use cases, see the section on Callbackprotocols.”
This limitation makes it cumbersome to declare callables meant to be invokedwith keyword arguments. The existing solution is to define aProtocol:
classKeywordTD(TypedDict,closed=True):a:intclassKwCallable(Protocol):def__call__(self,**kwargs:Unpack[KeywordTD])->Any:...# orclassKwCallable(Protocol):def__call__(self,*,a:int)->Any:...
This works but is verbose. The new syntax allows the equivalent to be writtenmore succinctly:
typeKwCallable=Callable[[Unpack[KeywordTD]],Any]
The primary goal is to make the common pattern of “callbacks that are intendedto be called with specific keyword arguments” straightforward to express withCallable. Today, such callbacks must be written as aProtocol with a__call__ that uses**kwargs:Unpack[...] or includes each keywordparameter explicitly. This approach is verbose and inconsistentwith how positional variadics are supported: perPEP 646,*args can beexpressed as*tuple[int,...] insideCallable.
AllowingUnpack[TypedDict] insideCallable achieves the following:
Callable[[...],R] shape while enablingkeyword-only parameter descriptions.__call__(self,**kwargs:Unpack[TD])->R.Unpack for**kwargs) andPEP 728 (extra_items andclosed).Protocol-based callbacks only.This keeps the status quo and avoids changingCallable. However, it issyntactically heavier and duplicates concepts already present inCallable.Callable syntax for keywords (e.g., dedicated keywordparameter markers insideCallable).This would require extending the callable parameter grammar with newconstructs, creating fresh semantics for optionality, defaults, and extrakeywords. The design space overlaps withTypedDict andPEP 692 andrisks divergent behavior from existing**kwargs typing.Callable and existing typing semantics.Unpack[TD] retains existingCallable semantics and mirrors realPython functions where positional and keyword-only parameters coexist.Concatenate: CombiningUnpack[TD] withConcatenate would enableinterspersed keyword-only parameters among*args and**kwargs. Thisincreases complexity and is not proposed here.It becomes valid to write:
Callable[[Unpack[TD]],R]
whereTD is aTypedDict. A shorter form is also allowed:
Callable[Unpack[TD],R]
Additionally, positional parameters may be combined with an unpackedTypedDict:
Callable[[int,str,Unpack[TD]],R]
For type-checking purposes,Callable[[Unpack[TD]],R] behaves as if it werespecified via a callback protocol whose__call__ method has**kwargs:Unpack[TD].The semantics ofUnpack itself are exactly those described in the typingspecification’sUnpack for keyword argumentssection andPEP 692, together withPEP 728 forextra_items andclosed.
This PEP only adds the following Callable-specific rules:
Unpack[TD] may appear inside the parameter list ofCallable.Callable beforeUnpack[TD] andfollow existingCallable semantics.ParamSpec may be substituted by an unpackedTypedDict within aCallable.The following examples illustrate how unpacking aTypedDict into aCallable enforces acceptance of specific keyword parameters. A function iscompatible if it can be called with the required keywords (even if they arealso accepted positionally); positional-only parameters for those keys arerejected:
fromtypingimportTypedDict,Callable,Unpack,Any,NotRequiredclassKeywordTD(TypedDict):a:inttypeIntKwCallable=Callable[[Unpack[KeywordTD]],Any]defnormal(a:int):...defkw_only(*,a:int):...defpos_only(a:int,/):...defdifferent(bar:int):...f1:IntKwCallable=normal# Acceptedf2:IntKwCallable=kw_only# Acceptedf3:IntKwCallable=pos_only# Rejectedf4:IntKwCallable=different# Rejected
Keys markedNotRequired in theTypedDict correspond to optionalkeyword arguments.This means that the callable must accept them, but callers may omit them.Functions that accept the keyword argument must also provide a default valuethat is compatible; functions that omit the parameter entirely are rejected:
classOptionalKws(TypedDict):a:NotRequired[int]typeOptCallable=Callable[[Unpack[OptionalKws]],Any]defdefaulted(a:int=1):...defkw_default(*,a:int=1):...defno_params():...defrequired(a:int):...g1:OptCallable=defaulted# Acceptedg2:OptCallable=kw_default# Acceptedg3:OptCallable=no_params# Rejectedg4:OptCallable=required# Rejected
extra_items orclosed)If theTypedDict does not specifyextra_items orclosed, additionalkeyword arguments are permitted with typeobject.This is the default behavior:
# implies extra_items=objectclassDefaultTD(TypedDict):a:inttypeDefaultCallable=Callable[[Unpack[DefaultTD]],Any]defv_any(**kwargs:object):...defv_ints(a:int,b:int=2):...d1:DefaultCallable=v_any# Accepted (implicit object for extras)d1(a=1,c="more")# Accepted (extras allowed)d2:DefaultCallable=v_ints# Rejected (b: int is not a supertype of object)
closed behavior (PEP 728)Ifclosed=True is specified on theTypedDict, no additional keywordarguments beyond those declared are expected:
classClosedTD(TypedDict,closed=True):a:inttypeClosedCallable=Callable[[Unpack[ClosedTD]],Any]defv_any(**kwargs:object):...defv_ints(a:int,b:int=2):...c1:ClosedCallable=v_any# Acceptedc1(a=1,c="more")# Rejected (extra c not allowed)c2:ClosedCallable=v_ints# Acceptedc2(a=1,b=2)# Rejected (extra b not allowed)
extra_items (PEP 728)If aTypedDict specifies theextra_items parameter (with the exceptionofextra_items=Never), the correspondingCallablemust accept additional keyword arguments of the specified type.
For example:
classExtraTD(TypedDict,extra_items=str):a:inttypeExtraCallable=Callable[[Unpack[ExtraTD]],Any]defaccepts_str(**kwargs:str):...defaccepts_object(**kwargs:object):...defaccepts_int(**kwargs:int):...e1:ExtraCallable=accepts_str# Accepted (matches extra_items type)e2:ExtraCallable=accepts_object# Accepted (object is a supertype of str)e3:ExtraCallable=accepts_int# Rejected (int is not a supertype of str)e1(a=1,b="foo")# Acceptede1(a=1,b=2)# Rejected (b must be str)
ParamSpec andConcatenateAParamSpec can be substituted byUnpack[KeywordTD] to define aparameterized callable alias. SubstitutingUnpack[KeywordTD] produces thesame effect as writing the callable with an unpackedTypedDict directly.Using aTypedDict withinConcatenate is not allowed.
typeCallableP[**P]=Callable[P,Any]h:CallableP[Unpack[KeywordTD]]=normal# Acceptedh2:CallableP[Unpack[KeywordTD]]=kw_only# Acceptedh3:CallableP[Unpack[KeywordTD]]=pos_only# Rejected
The current implementation needs to be updated to allow subscripting with agenericUnpack[TypedDict] without extra brackets;seeBackwards Compatibility.
UnpackPositional parameters may precede an unpackedTypedDict insideCallable.Functions that accept the required positional arguments and can be called withthe specified keyword(s) are compatible; making the keyword positional-only isrejected:
fromtypingimportTypedDict,Callable,Unpack,AnyclassKeywordTD(TypedDict):a:inttypeIntKwPosCallable=Callable[[int,str,Unpack[KeywordTD]],Any]defmixed_kwonly(x:int,y:str,*,a:int):...defmixed_poskw(x:int,y:str,a:int):...defmixed_posonly(x:int,y:str,a:int,/):...m1:IntKwPosCallable=mixed_kwonly# Acceptedm2:IntKwPosCallable=mixed_poskw# Acceptedm3:IntKwPosCallable=mixed_posonly# Rejected
This feature is mostly an additive typing-only feature. It does not affectexisting code.Subscripting aParamSpec with a genericUnpack of aTypedDict isonly backwards compatible when placed inside extra brackets; aTypeAliasTypeis not affected by this:
fromtypingimportTypedDict,ParamSpec,Callable,UnpackfromtypingimportTypeAliasTypeclassConfig[T](TypedDict):setting:TtypeCallP1[**P]=Callable[P,None]CallP1_SubbedB=CallP1[Unpack[Config[int]]]# OKP=ParamSpec("P")CallP2=Callable[P,None]CallP2_SubbedA=CallP2[[Unpack[Config[int]]]]# OKCallP2_SubbedB=CallP2[Unpack[Config[int]]]# currently TypeError
This feature is a shorthand for Protocol-based callbacks. Users should betaught that with
classKeywordTD(TypedDict):a:intb:NotRequired[str]
Callable[[Unpack[KeywordTD]],R] is equivalent to defining a Protocol with__call__(self,**kwargs:Unpack[KeywordTD])->Ror__call__(self,a:int,b:str=...,**kwargs:object)->R.TypedDict withCallable first before introducingProtocol.**kwargs:object might be surprising to users;usingclosed=True for definitions will create the more intuitiveequivalence of__call__(self,a:int,b:str=...)->Rextra_items fromPEP 728.A prototype exists in mypy:python/mypy#16083.
Unpack[TD] withConcatenate. With such support, one couldwriteCallable[Concatenate[int,Unpack[TD],P],R] which in turn wouldallow a keyword-only parameter between*args and**kwargs, i.e.deffunc(*args:Any,a:int,**kwargs:Any)->R:...which is currently not allowed perPEP 612.To keep the initial implementation simple, this PEP does not propose suchsupport.TypedDict unpacks be allowed to form a union, and if so,how to handle overlapping keys of non-identical types? Which restrictionsshould apply in such a case? Should the order matter?Callable[Unpack[TD],R] in addition toCallable[[Unpack[TD]],R]?ReadOnly keys?Thanks to Jelle Zijlstra for sponsoring this PEP and his valuable reviewfeedback.
Hugo van Kemenade, for helpful feedback on the draft and PR of this PEP.
Eric Traut, for feedback on the initial idea and discussions.
Unpack with**kwargsextra_items inTypedDictThis 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-0821.rst
Last modified:2026-01-31 20:25:24 GMT