Functions

What is a Function?

Function is a set of Python codes that performs a task or returns a result.
So far, you have learnt how to use some of the builtin function in Python, such as print(), len(), range(), min(), max() etc.
You can write lines of codes to perform a task or return results. But the lines of code gets larger from few lines to 100s or 1000s of lines of code. It is recommended to be break down the code to smaller chunks. This is helpful to make your code more maintainable and readable as well as avoid repetition by reusing chunks of codes. These reusable chunks are called Functions.

What are the Types of Functions?

There are generally two types of functions:
  1. 1.
    Perform a task
  2. 2.
    Return a value(s) or an expression(s)

What is a Function Syntax?

In Python, the building block of a function is as follow:
  1. 1.
    Function starts with def keyword
  2. 2.
    A uniquely identified name for the function
  3. 3.
    Parameters to pass the value(s) of the function into the function. This is optional.
  4. 4.
    A way to define where the function header finishes: colon :
  5. 5.
    Multi-line string to document what the function input and output is or performs. This is also optional but it is recommended.
  6. 6.
    Python statement(s) that performs a task
  7. 7.
    If you want the function to return an output, you can use return statement. It is also optional.
1
def function_name(inputs):
2
"""
3
docstrings
4
"""
5
# Python statements
6
7
return outputs
Copied!

What is return Statement?

The return statement is used to exit a function and return an output. The output can be assigned to a variable with assignment operator. The return statement is:
1
return output(s)
Copied!
The return statement evaluates the function and returns the output. It will return a None object where there is either no expression to return or no return statement is present.

An Example of a Function

An example of a function that takes two integers and return the sum of the values. The function is created below:
1
def take_sum(value_1_int, value_2_int):
2
"""
3
inputs:
4
5
output:
6
"""
7
sum_of_values = value_1_int + value_2_int
8
9
return sum_of_values
Copied!

Semantics: Arguments vs Parameters

In the context of take_sum() function, value_1_int and value_2_int are called parameters of take_sum() function. value_1_int and value_2_int are variables locally connected to the take_sum() function.
You can call the function name followed by a parentheses () with the arguments in between.
1
value_1_int = 2
2
value_2_int = 10
3
​
4
take_sum(value_1_int, value_2_int)
5
​
6
#output
7
12
Copied!
Now, value_1_int and value_2_int are called the arguments of take_sum() and you also passed value_1_int and value_2_int by reference. i.e. memory addresses of the value_1_int and value_2_int are passed to the function.
A function can be created without parameters.
Function
Call the function
1
def greeting():
2
print('Good day')
3
print('Stay safe and healthy')
Copied!
1
greeting()
2
​
3
# output
4
Good day
5
Stay safe and healthy
Copied!

What is Positional Arguments?

Positional Arguments is the order in which parameters are passed into a function. It is the most common way of assigning arguments to parameters.
1
take_sum(2, 10) #--> value_1_int = 2, value_2_int = 10
2
​
3
take_sum(10, 2) #--> value_1_int = 10, value_2_int = 2
4
​
5
take_sum(value_1_int = 10, value_2_int = 2)
Copied!

What is a Default Values in a Function?

A positional arguments can be made optional by specifying a default value for the corresponding parameter. Let's take the take_sum() function as an example.
default value
overwrite default value
1
def take_sum(value_1_int, value_2_int=10):
2
sum_of_values = value_1_int + value_2_int
3
4
return sum_of_values
5
6
7
take_sum(2)
8
​
9
# output: 12
Copied!
1
x = 2
2
y = 100 # it was 10
3
​
4
5
take_sum(x, y)
6
take_sum(2, 100)
7
# output: 102
Copied!
The default value always comes as a last parameter in the function or any parameter that comes after it has to be assigned with default value. Let's modify take_sum() function to have three parameters.
three values sum
three values sum corrected
1
def take_sum(value_1_int, value_2_int=10, value_3_int):
2
sum_of_values = value_1_int + value_2_int + value_3_int
3
4
return sum_of_values
5
6
# output
7
# File "<ipython-input-3-4a4bb3b6c326>", line 1
8
# def take_sum(value_1_int, value_2_int=10, value_3_int):
9
# ^
10
# SyntaxError: non-default argument follows default argument
Copied!
1
def take_sum(value_1_int, value_2_int=10, value_3_int=20):
2
sum_of_values = value_1_int + value_2_int + value_3_int
3
4
return sum_of_values
5
​
6
# or
7
​
8
def take_sum(value_1_int, value_3_int, value_2_int=10):
9
sum_of_values = value_1_int + value_2_int + value_3_int
10
11
return sum_of_values
Copied!
But what if you want to specify the 1st and 3rd arguments, but omit the 2nd argument? i.e. you want to specify values for value_1_int and value_3_int, but let value_2_int take on its default value.
1
take_sum(value_1_int=2, value_3_int=30)
2
take_sum(2, 30)
3
# or
4
​
5
take_sum(2, value_3_int=30)
Copied!

Keyword arguments

Optionally, positional arguments can be specified by using the parameter name regardless of the parameters with or without default values.
1
take_sum(2, 10, 20)
2
# or
3
take_sum(2, 10, value_3_int=20)
4
# or
5
take_sum(value_1_int=2, value_2_int=10, value_3_int=20)
6
# or
7
take_sum(value_3_int=20, value_1_int=2, value_2_int=10)
Copied!
When you use a named argument, it is required to use named arguments for all arguments after it.
1
take_sum(value_3_int=20, 10, 2)
2
# or
3
take_sum(2, value_3_int=20, 10)
4
​
5
​
6
# output
7
# File "<ipython-input-8-86d09888c0c5>", line 6
8
# take_sum(value_3_int=20, 10, 2)
9
# ^
10
#SyntaxError: positional argument follows keyword argument
Copied!
You can also not call the named arguments when you are calling your function
1
take_sum(2) # value_1_int=2, value_2_int=10, value_3_int=20
2
take_sum(2, 100) # value_1_int=2, value_2_int=100, value_3_int=20
3
take_sum(2, 100, 200) # value_1_int=2, value_2_int=100, value_3_int=200
Copied!

What is *args?

Remember the iterable unpacking
1
x, y, z = 1, 2, 3
Copied!
Similar things happens when positional arugments are passed to a function
1
take_sum(value_1_int, value_2_int, value_3_int)
2
​
3
take_sum(2, 4, 6)
Copied!
There is a handy trick that can be used during tuple unpacking when you have imbalance on right and left of the assignment operator. You can use a * operator to create a tuple/list from several values.
Code
Output
1
x, y, z = 1, 2, 3, 4
Copied!
1
#output
2
ValueError Traceback (most recent call last)
3
​
4
<ipython-input-9-751b9158939d> in <module>()
5
----> 1 x, y, z = (1, 2, 3, 4)
6
​
7
ValueError: too many values to unpack (expected 3)
Copied!
The above code gives an error. But we can overcome that with * operator. The first and second will be assigned to x and y respectively. The third and fourth will be a list and assigned to z
Code
Output
1
x, y, *z = 1, 2, 3, 4
2
​
3
print(x)
4
print(y)
5
print(z)
6
print(type(z))
Copied!
1
#print(x)
2
1
3
#print(y)
4
2
5
#print(z)
6
[3, 4]
7
#print(type(z))
8
<class 'list'>
Copied!
Let's define the function again, notice the start at the start of third value.
1
def take_sum(value_1_int, value_2_int, *value_3_int):
2
print(value_1_int)
3
print(value_2_int)
4
print(value_3_int)
5
6
7
take_sum(1, 2, 'string1', 'string2')
8
# value_1_int = 1
9
# value_2_int = 2
10
# value_3_int = ('string1', 'string2') # a tuple (a minor difference)
Copied!
The parameter name value_3_int is arbitrary. You can choose any name you would like, but it is customary to name it *args. So your function can be updated as below.
1
def take_sum(value_1_int, value_2_int, *args):
2
print(value_1_int)
3
print(value_2_int)
4
print(args)
Copied!
*args will not allow anymore positional arguments. But there is a way around it.
1
def take_sum(value_1_int, value_2_int, *args, value_3_int):
2
print(value_1_int)
3
print(value_2_int)
4
print(args)
5
print(value_3_int)
Copied!
This will not work
1
take_sum(2, 10, 30, 50, 20)
Copied!
This will work
1
take_sum(2, 10, 30, 50, value_3_int = 20)
Copied!

Unpacking arguments

You can pass an iterable argument into a function with the * operator.
1
def take_sum(value_1_int, value_2_int, value_3_int):
2
sum_of_values = value_1_int + value_2_int + value_3_int
3
4
return sum_of_values
5
6
​
7
values = [2, 10, 20]
8
take_sum(*values)
9
# or
10
take_sum(2, 10, 20)
11
# output
12
#32
Copied!

What is Keyword Arguments?

The positional parameters can, optionally, be passed as named (keyword) arguments. For example, if we define a function with three positional arguments:
1
def get_sum(int_1, int_2, int_3):
2
total = int_1 + int_2 + int_3
3
return total
Copied!
We can call the function get_sum() and pass the arguments. The named arguments is optional and it is up to you.
1
# you can call the function
2
get_sum(2, 3, 4)
3
#or
4
get_sum(int_1=2, int_3=4, int_2=3)
Copied!
You can also rewrite the function above to make more intuitive and practical. Using *args, you can pass many values at the same time.
1
def get_sum(*args):
2
total=0
3
for value in args:
4
total += value
5
return total
Copied!

Mandatory Keyword Arguments

Sometimes, you need to make the keyword arguments mandatory and force the user to call the named arguments. You can do so by exhausting all the positional arguments, and specify another additional parameter or parameters in the function definition.
1
def get_sum(int_1, int_2, *args, int_last):
2
total = int_1 + int_2 + int_last
3
return total, args
Copied!
The presence of *args exhausts all positional arguments and int_3 MUST be passed as a named argument.
In the function get_sum(), you will need to pass two positional arguments, one or more additional arguments (optional), and a mandatory keyword argument which goes into int_3. The int_3 argument can only be passed to the function using a named (keyword) argument.
This will not work
Code
Output
1
get_sum(2, 3, 22, 33, 44, 4)
Copied!
1
TypeError Traceback (most recent call last)
2
<ipython-input-9-91cc736c70b0> in <module>()
3
----> 1 get_sum(2, 3, 22, 33, 44, 4)
4
​
5
TypeError: get_sum() missing 1 required keyword-only argument: 'int_3'
Copied!
But using named argument for int_3, it works.
1
get_sum(2, 3, 22, 33, 44, int_3=4)
2
​
3
#or
4
​
5
get_sum(1, 2,int_3=5)
Copied!
You can even define a function that has only optional positional arguments and mandatory keyword arguments, i.e. you can force no positional arguments at all.
1
def get_sum(*args, max):
2
total=0
3
for value in args:
4
total += value
5
total += max
6
return total
Copied!
The function get_sum() can be rewritten to provide no positional arguments at all.
1
def get_sum(*, max):
2
total=0
3
for value in args:
4
total += value
5
total += max
6
return total
Copied!
Now, you have only * without specifying the name (usually args). The * indicates the end of positional arguments. You tell your function that you are not looking for positional arguments, effectively, there is no positional arguments.
if you call the function and provide positional arguments, Python raises an Exception error.
Code
Output
1
get_sum(2, 3, 22, 33, 44, max=4)
Copied!
1
TypeError Traceback (most recent call last)
2
<ipython-input-14-a7f9508d7a76> in <module>()
3
----> 1 get_sum(2, 3, 22, 33, 44, max=4)
4
​
5
TypeError: get_sum() takes 0 positional arguments but 5 positional arguments (and 1 keyword-only argument) were given
Copied!
Note: The aims is to showcase no positional arguments and the get_sum() function has to be fixed.
In nutshell: *args vs *
1
# Function 1
2
def func(x, y=1, *args, z, factor=True):
3
# statements
4
​
5
​
6
# Function 2
7
def func(x, y=1, *, z, factor=True):
8
# statements
Copied!
  1. 1.
    x: mandatory positional argument (may be specified using a named argument)
  2. 2.
    y: optional positional argument (may be specified positionally, as a named argument, or not at all), defaults to 1
  3. 3.
    args vs *
    1. 1.
      args: catch-all for any (optional) additional positional arguments
    2. 2.
      *: no additional positional arguments allowed
  4. 4.
    z: mandatory keyword argument
  5. 5.
    factor: optional keyword argument, defaults to True

What is **kwargs?

You have a clear understanding what*args is by now. **kwargs works very similar *args, but instead of accepting positional arguments (*args returns a tuple) and accepts named arguments.
**kwargs is used to accept a variable amount of remaining keyword arguments. It returns a dictionary.
**kwargs can be specified even if the positional arguments have NOT been exhausted. This is opposite to *args.
Let's define a function:
Code
Output
1
def new_func(*, int_1, **kwargs):
2
print(int_1)
3
print(kwargs)
4
5
new_func(int_1=1, x=2, y=4)
Copied!
1
#print(int_1)
2
1
3
​
4
#print(kwargs)
5
{'x': 2, 'y': 4}
Copied!
You can also use it by passing only the named argument int_1
1
new_func(int_1=1)
Copied!
This will print only the mandatory positional argument and an empty dictionary.
1
# Output
2
1
3
{}
Copied!
You can also create a function by just passing a **kwargs. For example:
Code
Output
1
def new_func(**kwargs):
2
print(kwargs)
3
4
new_func(x=2, y=4, z=6)
Copied!
1
{'x': 2, 'y': 4, 'z': 6}
Copied!
You can even create a function with both *args and **kwargs.
Code
Output
1
def new_func(*args, **kwargs):
2
print(args)
3
print(kwargs)
4
5
new_func(11, 22, 33, x=2, y=4, z=6)
Copied!
1
#print(args)
2
(11, 22, 33)
3
​
4
#print(kwargs)
5
{'x': 2, 'y': 4, 'z': 6}
Copied!
Can we mix args and kwargs as input to a function?
You can even call the function without providing any arguments. This result in returning an empty tuple () and an empty dictionary {}.
1
new_func()
2
​
3
# Output
4
()
5
{}
Copied!
You can check the Jupyter Notebook in Colab