Python: Function parameters

Default arguments

Default parameter values are evaluated once when the function is defined, not on each call.

def func(x, things=[]):
    things.append(x)
    return things
func(1) # [1]
func(2) # [1, 2]
func(3) # [1, 2, 3]

Best practice:

  • DO use immutable objects (None, number, string) as default argument values

Variadic arguments

Below function accept variable number of arguments.

def compute_product(first_number, *rest):
    product = first_number
    for x in rest:
        product *= x
    return product

compute_product(10, 20) # -> 200
compute_product(2, 3, 4, 5) # -> 120

All extra arguments are placed in rest variable as a tuple.

Keyword arguments

Below, debug variable must be supplied as keyword argument like read_data('foo.txt', debug=True).

def read_data(file_name, *, debug):
    statements

Variadic keyword arguments

If the last argument of a function definition has ** prefix, all the additional keyword arguments are placed in a dictionary and passed to the function. The order of items in the dictionary is guaranteed to match the order in which keyword arguments were provided.

def make_table(data, **params):
    # Get configuration parameters from params (a dict)
    fgcolor = params.pop('fgcolor', 'black')
    bgcolor = params.pop('bgcolor', 'white')
    width = params.pop('width', None)
    ...
    # No more options
    if params:
        raise TypeError(f"Unsupported configuration options: {list(params)}")

make_table(items, fgcolor='blue', bgcolor='white', border=1, borderstyle='grooved', cellpadding=0, width=400)

All inputs

Using both * and ** let a function accept any combination of inputs.

# Accept variable number of positional or keyword arguments
def func(*args, **kwargs):
    # args is a tuple of positional args
    # kwargs is a dict of keyword args

Commonly used to write wrappers, decorators, proxies.

Below, parse_file wraps parse_lines providing a file as the source of the lines to parse. The parse_file does not need to know what args parse_lines need. If a new arg is added to parse_line, that automatically works with parse_file.

def parse_lines(lines, separator=',', types=(), debug=False):
    for line in lines:
        ...
        statements
        ...
def parse_file(filename, *args, **kwargs):
    with open(filename, 'rt') as file:
        return parse_lines(file, *args, **kwargs)

Positional-only arguments

All args before / must be positional.

def func(x, y, /):
    pass
func(1, 2) # Ok
func(1, y=2) # Error

A useful way to avoid potential name clashes between arguments.

import time
def duration(*, seconds, minutes, hours):
    return seconds + 60 * minutes + 3600 * hours
def after(seconds, func, /, *args, **kwargs):
    time.sleep(seconds)
    return func(*args, **kwargs)

after(5, duration, seconds=20, minutes=3, hours=2)

duration() and after(), both have seconds as arg name, but after() forces it to be positional arg. As a result, on a call to after() the seconds keyword can be used for duration().

Parameter passing

Python passes the supplied objects to the function “as is” without any extra copying. The function parameters are local names that get bound to the passed input objects.

def square(items):
    for i, x in enumerate(items):
        items[i] = x*x # modify items in-place

a = [1, 2, 3, 4]
square(a) # a --> [1, 4, 9, 16]
def sum_squares(items):
    # name "items" is now bound to the result of 
    # the list comprehension, it is not overwriting
    # the original object "a"
    items = [x*x for x in items]  
    return sum(items)

a = [1, 2, 3, 4]
result = sum_squares(a)
print(a) # [1, 2, 3, 4] (a is unchanged)

Functions with side-effects, like sort(), return None.

Data in a sequence or mapping can be passed to a function. As long as the function gets the required args the calls work.

def func(x, y, z):
    ...
s = (1, 2, 3)
# pass a sequence as args
result = func(*s)
# pass a mapping as kwargs
d = {'x': 1, 'y': 2, 'z': 3}
result = func(**d)

Leave a comment