# default arguments
def is_it_freezing(temp, is_celsius=True):
if is_celsius:
= 0
freezing_line else:
= 32
freezing_line return temp < freezing_line
7 Arguments
While sometimes we can write a function that takes no arguments, usually we’ll want to parameterize a function.
We call the variables that are passed in to a function arguments or parameters.
Passing Arguments
In some languages, you can pass arguments by value or by reference. In Python all values are “passed by assignment”.
def func(a, b):
...
= 7
x = [1, 2, 3]
y
func(x, y)
# you can think of the function starting with assignments to its parameters
= x
a = y b
This means mutability determines whether or not a function can modify a parameter in the outer scope.
Unless otherwise specified, function arguments are required, and can be passed either by position or by name.
As we will see, we can also make optional arguments, as well as arguments that can only be passed by position or by name.
def calculate_cost(items, tax):
pass
# all arguments passed by position
"salmon", "eggs", "bagels"], 0.05)
calculate_cost([# tax passed by name
"salmon", "eggs", "bagels"], tax=0.05) calculate_cost([
Optional Arguments
Python allows default values to be assigned to function parameters.
Arguments with default values are not required. Passed in values will override default.
print(is_it_freezing(65))
print(is_it_freezing(30))
print(is_it_freezing(30, False))
print(is_it_freezing(-1, is_celsius=True))
False
False
True
True
You can have as many optional parameters as you wish, but they must all come after any required parameters.
# intentional error
def bad_function(a, b="spam", c):
pass
Argument Matching
There are two important rules in determining which arguments matched to which parameters:
- Positional arguments are matched from left to right.
- Keywords matched by name.
# print() as an example [call help to see docstring]
help(print)
Help on built-in function print in module builtins:
print(*args, sep=' ', end='\n', file=None, flush=False)
Prints the values to a stream, or to sys.stdout by default.
sep
string inserted between values, default a space.
end
string appended after the last value, default a newline.
file
a file-like object (stream); defaults to the current sys.stdout.
flush
whether to forcibly flush the stream.
print("hello", "positional", "world", sep="~", end=";")
hello~positional~world;
keyword and positional-only arguments
Including a bare *
as a parameter means everything after can only be passed by keyword.
For example:
def request_page(
url,
verify,=True,
cache=False,
retry_if_fail=False,
send_cookies=True):
https_onlypass
"https://example.com", True, False, True, False)
request_page(# or did you mean
"https://example.com", True, False, False, True) request_page(
# instead, use keyword-only
def request_page(url, *, verify, follow_redirects=False, cache=True, send_cookies=False, https_only=True):
pass
# forces keyword parameters
# & allows you to change the function definition leter
"https://example.com", verify=True) request_page(
Including a bare /
means everything beforehand is positional only:
def pos_only(x1, x2, /):
print(x1, x2)
"hello", "world") pos_only(
hello world
# ERROR: not allowed
="hello", x2="world") pos_only(x1
Variable Length Arguments
Sometimes we want a function that can take any number of parameters (seen above in print
).
Collect arbitrary positional arguments with *param_name
. (Often *args
)
Collect arbitrary named arguments with **param_name
. (Often **kwargs
)
# *args example
def add_many(*args):
#print(args, type(args))
= 0
total for num in args:
+= num
total return total
1, 2, 3, 4, 5) add_many(
15
# **kwargs example
def show_table(**kwargs):
for name, val in kwargs.items():
print(f"{name:>10} | {val}")
(Using advanced string formatting, see https://docs.python.org/3/library/string.html#formatstrings)
=100, eggs=12, other=42.0) show_table(spam
spam | 100
eggs | 12
other | 42.0
# combining args & kwargs
def func(a, *args, n=5, **kwargs):
print(a, args, n, kwargs, sep="\n")
1, 2, 3, 4, c=1, b=2) func(
1
(2, 3, 4)
5
{'c': 1, 'b': 2}
# ERROR: why?
=7, x=8) func(x
1, 2, 3, rest=5) func(
1
(2, 3)
5
{'rest': 5}
# some built-in functions work this way as well
max(1, 2, 3, 4)
4
Unpacking/Splatting
*
and **
are also known as unpacking or splatting operators.
When in a function signature, they coalesce arguments into a tuple
and dict
as we’ve seen.
When used on a parameter when calling a function, they “unpack” the values from a sequence or dict.
def takes_many(a, b, c, d):
print(f"{a=} {b=} {c=} {d=}")
# any iterable can be splatted
= ["A", "B", "C"]
three = (1, 2, 3, 4)
four = (False, False, False, False, False) five
*four) takes_many(
a=1 b=2 c=3 d=4
*three, 4) takes_many(
a='A' b='B' c='C' d=4
4, *three) takes_many(
a=4 b='A' c='B' d='C'
# ERROR
*five) takes_many(
# double-splat, unpack a dictionary into keyword args
= {"a": "sun", "c": "venus", "b": "mars"}
keywords **keywords, d="moon") takes_many(
a='sun' b='mars' c='venus' d='moon'
import math
def distance(x1, y1, x2, y2):
"""
Find distance between two points.
Inputs:
point1: 2-element tuple (x, y)
point2: 2-element tuple (x, y)
Output: Distance between point1 and point2 (float).
"""
return math.sqrt(math.pow(x2-x1, 2) + math.pow(y2-y1, 2))
# we can use sequence-unpacking to turn tuples/lists into multiple arguments
= (3, 4)
a = [5, 5]
b *a, *b) # our 2 parameters become 4 distance(
2.23606797749979
= [1,2,3,4]
a 0:2], a[2:] a[
([1, 2], [3, 4])
= {"verify": False, "https_only": True, "timeout_sec": 3}
http_params "http://example.com", **http_params) request_page(
def fn1(url, kw1=None, kw2=None, kw3=None):
...
def fn2(url, **kwargs):
if is_valid(url):
"additional_arg"] = 4
kwargs[return fn1(url, **kwargs)
return None
Caveat: Mutable Default Arguments
An important rule to remember is that the declaration (def
statement) of a function is only evaluated once, not every time the function is called.
This can lead to surprising behavior at first. It is important to understand & remember that only the inner-block of a function is executed on each call.
This is a common cause of bugs when a mutable is a part of the default arguments.
def add_many(item, n, base_list=[]):
print("adding to", id(base_list))
* n)
base_list.extend([item] return base_list
# passing in a list for base_list works as expected...
= ["cow"]
animals print("animals id", id(animals))
"bear", 3, animals)
add_many("fish", 5, animals)
add_many(print(animals)
animals id 4424213760
adding to 4424213760
adding to 4424213760
['cow', 'bear', 'bear', 'bear', 'fish', 'fish', 'fish', 'fish', 'fish']
# let's invoke without a base_list parameter
= add_many("dog", 3)
animals2 print(animals2)
adding to 4424214592
['dog', 'dog', 'dog']
= add_many("turtle", 4)
animals3 print(animals3)
adding to 4424214592
['dog', 'dog', 'dog', 'turtle', 'turtle', 'turtle', 'turtle']
animals2
['dog', 'dog', 'dog', 'turtle', 'turtle', 'turtle', 'turtle']
is animals3 animals2
True
The correct pattern is to avoid mutable defaults and instead assign a fresh mutable within the function as needed:
# fixed version
def add_many(item, n, base_list=None):
if base_list is None:
= []
base_list * n)
base_list.extend([item] return base_list
= []
temp print(id(temp))
= add_many("fish", 3)
returned print(returned)
print(id(returned))
4424210496
['fish', 'fish', 'fish']
4424210176
This is not a bug per se, but a somewhat unfortunate side effect of how Python proceses statements and handles mutables.
There are times when it can be used to your advantage.
In the below example, the cache_dict
persists between calls, allowing it to be used as a sort of cache/memory for the calling function. This should only be done thoughtfully and with ample commenting to explain the intended behavior.
def add_cached(x, y, cache_dict={}):
print(cache_dict)
if (x, y) not in cache_dict:
print("did calculation", x, y)
= x + y
cache_dict[x, y] return cache_dict[x, y]
4, 5) add_cached(
{}
did calculation 4 5
9
6, 10) add_cached(
{(4, 5): 9}
did calculation 6 10
16
# will use cache
4, 5) add_cached(
{(4, 5): 9, (6, 10): 16}
9
Discussion
What types are args
and kwargs
?
When should you use defaults, name-only, positional-only?
Your function provides an “interface” for other programmers to interact with.
Proper choices help other programmers understand how to call your functions and should be chosen to make things easier for others.
What would you prefer?
get("https://example.com", [500, 501, 503], 2.5, 2, False)
or
get("https://example.com", retry_if=[500, 501, 503], timeout_sec=2.5, retries=2, verify_ssl=False)
Always consider “future you” among those hypothetical “other programmers”.
Examples
# two required args
def f(x, y):
print(f"{x=} {y=}")
# a default argument
def g(x, y=3):
print(f"{x=} {y=}")
# all default args
def h(x="abc", y=3, z=True):
print(f"{x=} {y=} {z=}")
# mixture of arg types
def j(x, *args, y=3, **kwargs):
print(f"{x=} {y=} {args=} {kwargs=}")
# 1.
f()
# 2.
=1, 2)
f(x
# 3.
= {"x": 0, "y": 0}
d **d)
f(
# 4.
**d)
g(
# 5.
**d, x=2)
g(
# 6.
=1)
g(x
# 7.
h()
# 8.
**d)
h(
# 9.
=False, **d)
h(z
# 10.
1, 2, 3, 4, 5, 6, 7)
j(
# 11.
**d)
j(
# 12.
= (9, 9, 9, 9, 9, 9)
t
j(t)
# 13.
*t) j(