Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEP 671: Create a PEP for late-bound argument defaults #2123

Merged
merged 3 commits into from
Oct 23, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions pep-0671.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
PEP: 671
Title: Syntax for late-bound function argument defaults
Author: Chris Angelico <rosuav@gmail.com>
Status: Draft
Type: Standards Track
Content-Type: text/x-rst
Created: 24-Oct-2021
Python-Version: 3.11
Post-History:


Abstract
========

Function parameters can have default values which are calculated during
function definition and saved. This proposal introduces a new form of
argument default, defined by an expression to be evaluated at function
call time.


Motivation
==========

Optional function arguments, if omitted, often have some sort of logical
default value. When this value depends on other arguments, or needs to be
reevaluated each function call, there is currently no clean way to state
this in the function header.

Currently-legal idioms for this include::

# Very common: Use None and replace it in the function
def bisect_right(a, x, lo=0, hi=None, *, key=None):
if hi is None:
hi = len(a)

# Also well known: Use a unique custom sentinel object
_USE_GLOBAL_DEFAULT = object()
def connect(timeout=_USE_GLOBAL_DEFAULT):
if timeout is _USE_GLOBAL_DEFAULT:
timeout = default_timeout

# Unusual: Accept star-args and then validate
def add_item(item, *optional_target):
if not optional_target:
target = []
else:
target = optional_target[0]

In each form, ``help(function)`` fails to show the true default value. Each
one has additional problems, too; using ``None`` is only valid if None is not
itself a plausible function parameter, the custom sentinel requires a global
constant; and use of star-args implies that more than one argument could be
given.

Specification
=============

Function default arguments can be defined using the new ``=>`` notation::

def bisect_right(a, x, lo=0, hi=>len(a), *, key=None):
def connect(timeout=>default_timeout):
def add_item(item, target=>[]):

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 the
scope in which the function was defined (as are early-bound defaults). This
allows the expression to refer to other arguments.

Self-referential expressions will result in UnboundLocalError::

def spam(eggs=>eggs): # Nope

Multiple late-bound arguments are evaluated from left to right, and can refer
to previously-calculated values. Order is defined by the function, regardless
of the order in which keyword arguments may be passed.


Choice of spelling
------------------

Our chief syntax proposal is ``name=>expression`` -- our two syntax proposals
... ahem. Amongst our potential syntaxes are::

def bisect(a, hi=>len(a)):
def bisect(a, hi=:len(a)):
def bisect(a, hi?=len(a)):
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 late
bound, the preferred syntax is very similar to the existing early-bind syntax.
The alternatives offer little advantage over the preferred one.

How to Teach This
=================

Early-bound default arguments should always be taught first, as they are the
simpler and more efficient way to evaluate arguments. Building on them, late
bound arguments are broadly equivalent to code at the top of the function::

def add_item(item, target=>[]):

# Equivalent pseudocode:
def add_item(item, target=<OPTIONAL>):
if target was omitted: target = []


Open Issues
===========

- yield/await? Will they cause problems? Might end up being a non-issue.

- annotations? They go before the default, so is there any way an anno could
want to end with ``=>``?


References
==========


Copyright
=========

This document is placed in the public domain or under the
CC0-1.0-Universal license, whichever is more permissive.



..
Local Variables:
mode: indented-text
indent-tabs-mode: nil
sentence-end-double-space: t
fill-column: 70
coding: utf-8
End: