Readings
Contents
4.1. Readings#
4.1.1. What are functions?#
Functions are blocks of code that help us reuse code. The structure of a function looks like this:
def function_name (parameter1, parameter2):
body of the function
return something
A function definition always starts with the def
keyword, followed by the function name. We are free to name functions the way we like, but just as in the case of naming variables, it is important to give meaningful names to functions so that it is easy to understand what they do. After the function name, a list of parameters is enclosed in parentheses. These are variables that will be used in the body of the function. Note that the parameter list might be empty as well. After that, :
indicates the end of the function definition. Then the declaration of the body of the function begins (i.e., what code that is run when this function is called). Usually, we find the return
statement at the end of the function body (indicating what will be returned by the function) - in some cases, though, that statement may be ommited (if we do not want to return anything).
To call a function you simply write the function name followed by the provided parameters:
function_name(paramater1_value, paramater2_value)
Suppose that we want to find the minimum value of three numbers that are between 0 and 100. From what we have learned so far, a possible solution would be:
no_1 = 20
no_2 = 44
no_3 = 35
min_value = no_1
if no_1 > no_2:
min_value = no_2
if no_2 > no_3:
min_value = no_3
print(min_value)
35
Note
Did you notice something here? print()
is a function as well, but a built-in one.
Hint
The Python Standard Library provides a lot of already-written, built-in and tested functions. The availability of such functions makes Python one of the most popular programming languages. Before you write your own function it is a good idea to have a look at the Python Standard Library because it might already contain the function needed. Here you can find the official documentation.
Now, imagine that after some lines of code we would need again to find the minimum of 3 numbers. We could copy-paste the code block from above, change the numbers that we provided and get the minimum. After some time, the conditions change and we need to compare 4 numbers instead of 3. We have to go and change every single copy-pasted block that computes the minimum. In this tedious process, we might forget a block or even worse, we might introduce mistakes in one of them without noticing at all. So instead, we switch to using functions.
Below you can find the implementation of the function to find the minimum value of 3 numbers:
def find_minimum(no_1, no_2, no_3):
min_value = no_1
if min_value > no_2:
min_value = no_2
if min_value > no_3:
min_value = no_3
return min_value
And here we use the function we have just declared:
print('The minimum of {20, 35, 44} is', find_minimum(20, 35, 44))
print('The minimum of {27, 40, 63} is', find_minimum(63, 40, 27))
print('The minimum of {53, 44, 89} is', find_minimum(53, 44, 89))
The minimum of {20, 35, 44} is 20
The minimum of {27, 40, 63} is 27
The minimum of {53, 44, 89} is 44
Let’s look at what happened here: everything starts with the call to the print function that prints the first string and then evaluates the second argument which is the call to the function find_minimum. When we call the function, we provide values for the arguments that its expects, i.e., 3 numbers. Then, the control flow goes inside the body of our function, assigns the value of no_1
to the min_value
variable. Next, its starts checking if no_2
is smaller than the min_value
and if so, we assign the value of no_2
to the min_value
. The last check is done with the no_3
variable. If its value is less than the value of min_value
, then we assign its value to the min_value
variable. At the end in the return
statement, we return the value of the smallest number. After the function finishes its execution, the return
statement brings the control back to the print
function and prints the whole statement by appending to the string the smallest number.
Now, if we would like to declare a function that would find the minimum of 4 numbers it would be much easier and we would not have to repeat any code and using the ternary operator:
def find_minimum_of_four(no_1, no_2, no_3, no_4):
min_of_three = find_minimum(no_1, no_2, no_3)
return min_of_three if min_of_three < no_4 else no_4
print('The minimum of {20, 35, 15, 30} is', find_minimum_of_four(20, 35, 15, 30))
The minimum of {20, 35, 15, 30} is 15
Better yet, you could make this even more efficient by using a for
loop to accept any number of numbers as an input, and loop over these to find the minimum. Let’s give that a try! Read the following function but do not worry if you do not understand every line — you will learn more about using the list
type in the next chapter.
def find_minimum_of_list(list_of_numbers):
min_value = list_of_numbers.pop()
for n in list_of_numbers:
if min_value > n:
min_value = n
return min_value
list_of_numbers = [5, 8, 2, 7]
print('The minimum of {0} is'.format(list_of_numbers), find_minimum_of_list(list_of_numbers))
The minimum of [5, 8, 2, 7] is 2
Parameters vs Arguments
Sometimes the words parameters
and arguments
are used interchangeably when it comes to functions. They refer to the same thing. Parameters are the variables in the function header/declaration while the arguments are the actual values passed for each of the parameters when we call the function.

Fig. 4.1 Parameters vs Arguments#
4.1.2. Functions with default values#
When declaring a function, we can specify values for the parameters directly in the function signature (i.e., the collection of parameters input to a function). In this case, we will have a function with default parameter values. This means that when you call the function and do not provide any argument for this parameter, then the function body will use the default parameter specified in the function header to make the computations.
For example, we can define a function to compute the sum of two numbers:
def add(num_1=1, num_2=2):
return num_1 + num_2
Here we have specified a default value for both of the parameters. This allows us to call the function by not specifying any arguments at all, one argument or both of them:
print('The sum is', add())
print('The sum is', add(3))
print('The sum is', add(4, 7))
The sum is 3
The sum is 5
The sum is 11
If we do not specify any argument to pass to the function, then the calculation is made by using the default values of num_1=1
and num_2=2
. When we pass only one argument then the default value of num_1
is overwritten with the value 3
and the second parameter retains its default value, ths we have the result: 5. In the last case, both default values of the parameter are overwritten and we have their sum 11 printed.
Important
We can also pass values of each parameter using their names in the function call. These are called keyword arguments. Passing parameter values in this way allows you to change the order of the arguments. For example:
print('The sum is', add())
print('The sum is', add(num_2=3))
print('The sum is', add(num_2=4, num_1=7))
The sum is 3
The sum is 4
The sum is 11
4.1.3. Functions with an arbitrary number of arguments#
In Python it is possible for a function to receive an arbitrary number arguments each time it is called. This is done in the function signature. Instead of listing all expected arguments one by one, you can just write *parameter_name
, where you can choose whatever name you want for parameter_name
. In this way by using a for-loop you can iterate over the list of arguments passed to the function. This is useful when you do not know beforehand how many arguments will be passed to the function.
def add_many_numbers(*numbers):
result = 0
for number in numbers:
result = result + number
return result
print('The sum is', add_many_numbers(2,11,12,3,5))
print('The sum is', add_many_numbers(2,11,12))
The sum is 33
The sum is 25
There is a way to specify arbitrary keyword arguments as well. In this case, in the parameters of the function definition you should specify **parameter_name
. Again for the parameter_name
you can specify any valid variable name that you want.
def full_name(**name):
print('Full name is', name['first_name'], name['last_name'])
full_name(first_name='John', last_name='Smith')
full_name(first_name='John', middle_name='J.', last_name='Smith')
Full name is John Smith
Full name is John Smith
Here in this example, no mater how many keyword arguments the user specifies when calling the function, inside the function we use only the arguments that correspond to the keys first_name
and last_name
.
4.1.4. Scope of a variable#
Each variable has a scope. The scope of a variable determines where in the code the variable can be used. A variable that is declared inside a block, be it a loop, an if/else block, or a function is said to have local scope. This means that the variable can be used inside the block, from the point it is declared until the end of the block, outside it is outside of scope, hence it is not recognized. On the other hand, variables that are outside of any block, are global variables and they have global scope, they can be accessed from everywhere in the program.
Important
Local Scope
Variables can be accessed from the point they are declared until the end of the block where they are declared. Outside of these boundaries they are out of scope and not recognized.
Global Scope
Variables are not declared within a block but in the global workspace, thus they can be accessed from anywhere within the program.

Fig. 4.2 Scope of Variables#
Fig. 4.2 shows how variables declared inside nested blocks can be accessed. Block 1 is the outer block. Variable x
is declared there. This means that x
can be accessed from the point of declaration until the end of block 1. Since block 1 encloses all the other blocks, this means that x
can be accessed in all these blocks (indicated by the red dashed line that cuts all other blocks). Variable y
is declared in block 2. It cannot be accessed in block 1, but it can be accessed in blocks 3 and 4, besides block 2 where it is declared, thus complying with the local scope rule. Similarly, variable z
is accessed only in blocks 3 and 4, while variable w
can only be accessed in block 4.
Below you can see some code examples to understand how the scope of a variable affects the outcomes of a piece of code.
x = 5
def print_value_of_x():
print('Global value of x printed:', x)
def print_value_of_x_y_local():
x = 10
y = 20
print('Local value of x overrides global value of x:', x)
print('Local value of y:', y)
print_value_of_x()
print('----------')
print_value_of_x_y_local()
print('----------')
print('Global value of x:', x, 'unchanged')
print('Value of y:', y)
Global value of x printed: 5
----------
Local value of x overrides global value of x: 10
Local value of y: 20
----------
Global value of x: 5 unchanged
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
Cell In[16], line 6
4 print('----------')
5 print('Global value of x:', x, 'unchanged')
----> 6 print('Value of y:', y)
NameError: name 'y' is not defined
As we can see, the value of x=5
is a global variable since it is not declared within any block or any function. Thus, it can be accessed from within the function print_value_of_x()
. On the other hand, in the print_value_of_x_local()
function we have introduced two variables: x=10
and y=20
. Here, the local value of x
overrides the global value of x
, in the sense that the function will recognize only its local variable named x
and will print its value. There is also the variable y=20
, which is declared within a block (the function print_value_x_y_local
body), whose value is printed within the function as well. Notice that, when we try to access the value of y
from outside the function block, it will output an error, because y
is out of scope and is not recognized outside the boundaries where it is defined using the scope rules.
Important
Scope rules change a bit in case of control statements. If a control statement block is declared in the global scope (not inside a function), then the variables initialized there have global scope. Otherwise, if the control statements are inside a block/function, then variables declared there have local scope, accessible only within the block they were declared.
Something important to note here is the fact that statements that are declared globally (not inside a function), are executed as soon as they are encountered by the IPython Interpreter, while statements inside function blocks are executed only when the function is called.
Caution
Be careful when naming variables. If their identifier has the same name as a built-in function in Python, then they will shadow the function. The interpreter will not recognize the built-in function anymore and a TypeError
will be raised. The code below illustrates the situation. The interpreter sees sum
as a variable now and does not know about the sum()
function anymore.
sum = 1+2+3
print(sum)
6
sum([1,2,3])
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/var/folders/7f/7nw_x13n5q965rss_qz6061m0000gq/T/ipykernel_44632/980638477.py in <module>
----> 1 sum([1,2,3])
TypeError: 'int' object is not callable
4.1.5. import ...
and from ... import ...
statements#
In most of the cases, some built-in functions that we need to use are not part of our workspace, so we need to import the module that contains them. We do this using the import module
statements. For example, to import the math
module we would write:
import math # after importing the module we can use its features
# using sqrt() function of math module
math.sqrt(4)
2.0
We can also import certain functions of a module by using the following syntax:
from module_name import function1, function2,..., function_n
This would allow us to use the functions directly by their names.
from math import sqrt, log
# this way of importing
# allows to call the functions directly
print(sqrt(4))
print(log(2))
2.0
0.6931471805599453
Tip
In order to import all functions of a module you can use a wildcard (*), e.g from module_name import *
. However, this can introduce potential shadowing related to the variables we have in our code. So, it is advisable to avoid using it.
Note
In addition, it is possible to give aliases to modules. This is done to easily access the module functions later on, saving you some precious keystrokes. The syntax is:
import numpy as np
Later on, we can access all functions of the numpy
library using np.function_name
instead of numpy.function_name
.
4.1.6. What are methods? (Optional)#
Attention
This section includes concepts from object-oriented programming that is out of the scope of this book. We introduce it for the sake of completeness. Skipping it will not have any impact on understanding the other concepts that will come next.
Methods are very similar to functions. The only difference is that they belong to objects. The difference is seen in their declaration and in the way how they are called. Here we provide examples of the string
class built-in methods. Methods are declared within classes just like functions, but when they are called the syntax differs:
object_name.method_name(parameter_list)
This means that we are specifically referring to the method specified in the class. All objects of that class will have that method. An example will be:
word = 'hello'
print('Before calling method:', word)
word = word.capitalize()
print('After calling method:', word)
Before calling method: hello
After calling method: Hello
Here capitalize()
is a method of the class string
, since we are defining word
to be a string. We cannot call capitalize()
as we had done before by just specifying its name and the parenthesis, because this function is specific to a class and only objects of that class can call it, that is why it is called a method.