Python Function Parameters
Tags: Tech
Nov 3, 2021
16 min. read
In Python, or every programming language, functions are one of the larger building blocks. Functions allow reusing blocks of code and eliminate code duplication. But, not everyone is fully aware of all the different ways to pass inputs to functions. That is what I will be exploring in this blog.
Let's begin with a simple way of defining functions. A function can be described with three steps:
- Take inputs
- Manipulate the inputs (the function body)
- Return an output
As a note, moving forward, I will be referring to the inputs within the function definition as parameters, and the actual input values used when invoking the function as arguments.
So, this is what a typical function looks like in Python, as you may already know:
def function_name(parameter1, parameter2, ...):# indentation important# manipulation of input parametersreturn # something
So let's explore the different ways that arguments can be passed in to functions. When learning about functions, we are usually introduced to them with a fixed number of parameters. These parameters are positional meaning the position of the arguments when invoking the function will matter. If a function with a fixed number of parameters is not invoked with the same number of arguments as the number of parameters, it will raise an error.
def multiply(num1, num2):print(f"Multiplying {num1} and {num2}")return num1 * num2multiply(2, 2) # returns 4multiply(2, 2, 2) # error num arguments != num parameters
Additionally, when invoking the function, we can either use positional arguments or keyword arguments to specify the parameters. As a syntax rule, positional arguments always precede keyword arguments. This is because the positional arguments are first matched with their respective parameters before utilizing keyword arguments to target the remaining parameters. If a keyword argument does not target a parameter that exists, it will raise an error.
def divide(dividend, divisor):print(f"Dividing {dividend} by {divisor}")return dividend / divisordivide(4, 2) # both positional argsdivide(dividend=4, divisor=2) # both named argsdivide(divisor=2, dividend=4) # both named args in a different order also worksdivide(4, divisor=2) # mix of positional and keyword argsdivide(dividend=4, 2) # will raise an error as positional args should be firstdivide(2, dividend=4) # will raise an error as 2 is already assigned to `dividend` positionallydivide(2, quotient=4) # will raise an error as `quotient` parameter does not exist
However, there are often occasions where having a default value for a parameter is useful, usually for specifying default settings to the function. In Python, we can supply default values to parameters by specifying them in the function definition. This makes the parameter optional and if no value is supplied when invoking the function, the default value is utilized. However, the optional parameters, known as keyword parameters, can only be specified after 0 or more positional parameters in the function definition.
def exponent(power, base=10):print(f"Raising {base} to the power {power}")return base ** powerexponent(3, base=2) # returns 8exponent(3, 2) # default parameters can be specified through position, returns 8exponent(3) # uses default `base`, returns 1000exponent() # `power` does not have a default value, so raises error
Now, suppose we want to expand our earlier multiply
function to multiply as
many numbers as we would like. To accomplish this task, we would require a
function that accepts a variable number of arguments. Granted, we could create
a function that just accepts a list or an iterable as its only positional
argument, but that would require the creation of additional containers and
also may require type checking inside the function to make sure we received an
iterable. Wouldn't it be much easier if Python created that list (technically
a tuple) for us? That is exactly what the *args
parameter accomplishes.
Let's take a short detour here. The *
in question here is not the binary
operator for multiplication, but actually a packing/unpacking operation. We
can "unpack" an iterable into arguments to a function when invoking it, or
conversely, we can "pack" function parameters into an iterable for use inside
the function body. There are actually two operators here: *
is used for
packing/unpacking tuples, lists, sets, and keys of dictionaries, while **
is
used for packing/unpacking dictionary item pairs.
First, let's see some examples for unpacking of iterables:
def exponent(power, base):print(f"Raising {base} to the power {power}")return base ** powerargs = [3, 2]exponent(*args) # unpacking the list into positional argsargs = {3, 2}exponent(*args) # unpacking the set works but unpredictable order and outputkwargs = {"power": 3, "base": 2}exponent(**kwargs) # unpacking the dictionary into keyword argsargs = [3]kwargs = {"base": 2}exponent(*args, **kwargs) # unpacking positional and keyword argsexponent(*[3, 2, 1]) # will raise an error as too many argsexponent(*[3]) # will raise an error as too few argsexponent(**{"power": 3, "blah": 2}) # will raise an error as unexpected keyword arg
Going back to the variable number of positional parameters *args
, we can
make an association that if we are unpacking arguments when invoking a
function, we can similarly pack the parameters inside the function itself. We
can either have completely variable number of parameters (0 to computer limit)
or a few fixed positional parameters and then a variable number of additional
parameters (fixed x
to computer limit). It is generally a good practice to
group the variable positional parameters along with the fixed positional
parameters and have the keyword parameters follow if any.
# one number followed by variable amount of numbers followed by an optional factor# *nums is the packing of variable positional parameters into numsdef multiply(num, *nums, factor=1):print(num, nums, factor)ret = num * factor# nums is just a tuple that can be iterated overfor new_num in nums:ret *= new_numreturn retmultiply(2) # returns 2, num=2, nums is (), factor is default 1multiply(2, 3, 4, 5) # returns 120, num=2, nums is (3, 4, 5), factor is default 1multiply(2, factor=0.5) # returns 1, num=2, nums is (), factor is 0.5# the following is equivalent to multiply(2, 3, 4, 5, factor=0.5)args = [2, 3, 4, 5]multiply(*args, factor=0.5) # returns 60, num=2, nums is (3, 4, 5), factor is 0.5multiply() # will raise an error as `num` is still requiredmultiply(factor=2) # will raise an error as `num` is still requiredmultiply(2, factor=2, 3) # will raise an error as keyword arg precedes positional arg
Similarly, we can have a variable number of keyword parameters in the form of
**kwargs
. Any keyword arguments that do not match any of the parameter names
when invoking a function will get packed into a dictionary inside the
function. The variable number of keyword parameters can be used by itself if
all we want are key-value pairs inside the function, but they typically
follow either or all of the following: fixed positional parameters, variable
positional parameters, and fixed keyword parameters.
# one number followed by an optional factor followed by named numbers# **nums is the packing of variable keyword parameters into numsdef multiply(num, factor=1, **nums):print(num, factor, nums)ret = num * factor# nums is just a dictionary that can be iterated overfor name in nums:new_num = nums[name]print(f"Multiplying by {name}={new_num}")ret *= new_numreturn retmultiply(2) # returns 2, num=2, factor is default 1, nums is {}multiply(2, a=3, b=4, c=5) # returns 120, num=2, factor is default 1, nums is {'a':3, 'b':4, 'c':5}multiply(2, factor=0.5) # returns 1, num=2, factor is 0.5, nums is {}multiply(2, factor=0.5, a=3) # returns 3, num=2, factor is 0.5, nums is {'a':3}multiply(2, a=3, factor=0.5) # same as above, but bad style to put variables first# the following is equivalent to multiply(num=2, a=3, b=4, c=5, factor=0.5)args = {'num':2, 'a':3, 'b':4, 'c':5}multiply(**args, factor=0.5) # returns 60, num=2, factor is 0.5, nums is {'a':3, 'b':4, 'c':5}multiply() # will raise an error as `num` is still requiredmultiply(factor=2, a=3, b=4) # will raise an error as `num` is still required
In general, the good practice to avoid unexpected behaviors when passing arguments to a function is to use the following order of parameters in the function definition and also when invoking the function:
- Fixed positional parameters
- Variable positional parameters
- Fixed keyword parameters
- Variable keyword parameters
def example(arg1, arg2, *args, kwarg=None, **kwargs):print(arg1, arg2, args, kwarg, kwargs)example("hello", # arg1"world", # arg2"did", "you", "receive", # argskwarg="my", # kwarghidden="message" # kwargs)
I hope this example-heavy blog is actually useful to learn positional,
keyword, and variable parameters. The packing and unpacking operator often
confused me but when I understood what it meant, *args
and **kwargs
suddenly made a lot more sense.