Function parameters can have default values which are calculated duringfunction definition and saved. This proposal introduces a new form ofargument default, defined by an expression to be evaluated at functioncall time.
Optional function arguments, if omitted, often have some sort of logicaldefault value. When this value depends on other arguments, or needs to bereevaluated each function call, there is currently no clean way to statethis in the function header.
Currently-legal idioms for this include:
# Very common: Use None and replace it in the functiondefbisect_right(a,x,lo=0,hi=None,*,key=None):ifhiisNone:hi=len(a)# Also well known: Use a unique custom sentinel object_USE_GLOBAL_DEFAULT=object()defconnect(timeout=_USE_GLOBAL_DEFAULT):iftimeoutis_USE_GLOBAL_DEFAULT:timeout=default_timeout# Unusual: Accept star-args and then validatedefadd_item(item,*optional_target):ifnotoptional_target:target=[]else:target=optional_target[0]
In each form,help(function) fails to show the true default value. Eachone has additional problems, too; usingNone is only valid if None is notitself a plausible function parameter, the custom sentinel requires a globalconstant; and use of star-args implies that more than one argument could begiven.
Function default arguments can be defined using the new=> notation:
defbisect_right(a,x,lo=0,hi=>len(a),*,key=None):defconnect(timeout=>default_timeout):defadd_item(item,target=>[]):defformat_time(fmt,time_t=>time.time()):
The expression is saved in its source code form for the purpose of inspection,and bytecode to evaluate it is prepended to the function’s body.
Notably, the expression is evaluated in the function’s run-time scope, NOT thescope in which the function was defined (as are early-bound defaults). Thisallows the expression to refer to other arguments.
Multiple late-bound arguments are evaluated from left to right, and can referto previously-defined values. Order is defined by the function, regardless ofthe order in which keyword arguments may be passed.
def prevref(word=”foo”, a=>len(word), b=>a//2): # Validdef selfref(spam=>spam): # UnboundLocalErrordef spaminate(sausage=>eggs + 1, eggs=>sausage - 1): # Confusing, don’t do thisdef frob(n=>len(items), items=[]): # See below
Evaluation order is left-to-right; however, implementations MAY choose to do soin two separate passes, first for all passed arguments and early-bound defaults,and then a second pass for late-bound defaults. Otherwise, all arguments will beassigned strictly left-to-right.
While this document specifies a single syntaxname=>expression, alternatespellings are similarly plausible. The following spellings were considered:
def bisect(a, hi=>len(a)):def bisect(a, hi:=len(a)):def bisect(a, hi?=len(a)):def bisect(a, @hi=len(a)):
Since default arguments behave largely the same whether they’re early or latebound, the chosen syntaxhi=>len(a) is deliberately similar to the existingearly-bind syntax.
One reason for rejection of the:= syntax is its behaviour with annotations.Annotations go before the default, so in all syntax options, it must beunambiguous (both to the human and the parser) whether this is an annotation,a default, or both. The alternate syntaxtarget:=expr runs the risk ofbeing misinterpreted astarget:int=expr with the annotation omitted inerror, and may thus mask bugs. The chosen syntaxtarget=>expr does nothave this problem.
Early-bound default arguments should always be taught first, as they are thesimpler and more efficient way to evaluate arguments. Building on them, latebound arguments are broadly equivalent to code at the top of the function:
defadd_item(item,target=>[]):# Equivalent pseudocode:defadd_item(item,target=<OPTIONAL>):iftargetwasomitted:target=[]
A simple rule of thumb is: “target=expression” is evaluated when the functionis defined, and “target=>expression” is evaluated when the function is called.Either way, if the argument is provided at call time, the default is ignored.While this does not completely explain all the subtleties, it is sufficient tocover the important distinction here (and the fact that they are similar).
PEP 661 attempts to solve one of the same problems as this does. It seeks toimprove the documentation of sentinel values in default arguments, where thisproposal seeks to remove the need for sentinels in many common cases.PEP 661is able to improve documentation in arbitrarily complicated functions (itcitestraceback.print_exception as its primary motivation, which has twoarguments which must both-or-neither be specified); on the other hand, manyof the common cases would no longer need sentinels if the true default couldbe defined by the function. Additionally, dedicated sentinel objects can beused as dictionary lookup keys, wherePEP 671 does not apply.
A generic system for deferred evaluation has been proposed at times (not to beconfused withPEP 563 andPEP 649 which are specific to annotations).While it may seem, on the surface, that late-bound argument defaults are of asimilar nature, they are in fact unrelated and orthogonal ideas, and both couldbe of value to the language. The acceptance or rejection of this proposal wouldnot affect the viability of a deferred evaluation proposal, and vice versa. (Akey difference between generalized deferred evaluation and argument defaults isthat argument defaults will always and only be evaluated as the function beginsexecuting, whereas deferred expressions would only be realized upon reference.)
The following relates to the reference implementation, and is not necessarilypart of the specification.
Argument defaults (positional or keyword) have both their values, as alreadyretained, and an extra piece of information. For positional arguments, theextras are stored in a tuple in__defaults_extra__, and for keyword-only,a dict in__kwdefaults_extra__. If this attribute isNone, it isequivalent to havingNone for every argument default.
For each parameter with a late-bound default, the special valueEllipsisis stored as the value placeholder, and the corresponding extra informationneeds to be queried. If it isNone, then the default is indeed the valueEllipsis; otherwise, it is a descriptive string and the true value iscalculated as the function begins.
When a parameter with a late-bound default is omitted, the function will beginwith the parameter unbound. The function begins by testing for each parameterwith a late-bound default using a new opcode QUERY_FAST/QUERY_DEREF, and ifunbound, evaluates the original expression. This opcode (available only forfast locals and closure variables) pushes True onto the stack if the givenlocal has a value, and False if not - meaning that it pushes False if LOAD_FASTor LOAD_DEREF would raise UnboundLocalError, and True if it would succeed.
Out-of-order variable references are permitted as long as the referent has avalue from an argument or early-bound default.
When no late-bound argument defaults are used, the following costs should beall that are incurred:
Ellipsis as a default value will require run-time verificationto see if late-bound defaults exist.These costs are expected to be minimal (on 64-bit Linux, this increases allfunction objects from 152 bytes to 168), with virtually no run-time cost whenlate-bound defaults are not used.
Where late-bound defaults are not used, behaviour should be identical. Careshould be taken if Ellipsis is found, as it may not represent itself, butbeyond that, tools should see existing code unchanged.
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-0671.rst
Last modified:2025-02-01 08:55:40 GMT