Functions in Python

So far in this course, we have been using built-in functions that comes with Python. In this lesson we will learn how we can create our own functions. But before we do that, let's spend some time learning why we even need them in the first place.

Suppose you want to create a program which allows users to calculate sum between two numbers. At this point you shouldn't have any problem writing such programs; anyway, your code might look look this:

sum = 0
start = 10
end = 30
for i in range(start, end+1):
    sum += i    

print("Sum is", sum)

This programs calculates sum of all numbers from 10 to 30. If we want to calculate sum of numbers from 100 to 200, we would need to update the program as follows:

sum = 0
start = 100
end = 200
for i in range(start, end+1):
    sum += i    

print("Sum is", sum)

As you can see both versions of the program are nearly identical, the only difference is in the values of the start and end variable. So everytime we want to calculate sum of two numbers, we would need to update source of the program. It would be good if we somehow just reuse the entire code without doing any modification. We can do this using functions.

What is a Function #

A function is named group of statements, which perform a specific task. The syntax of defining a function is as follows:

def function_name(arg1, arg2, arg3 ... argN):
    # function body
    <indented statement 1>
    <non-indented statement 2>
    ...
    <indented statement n>
    <return statement>

A function consists of two parts: header and body. The function header starts with the def keyword, followed by name of the function, followed by arguments and ends with colon(:).

The def is a reserved keyword, so you shouldn't use it as a variable or function name in your programs. function_name can be be any valid identifier. After the function name we have a list of arguments inside parentheses separated by comma(,). We use these arguments to pass necessary data to the function. A function can take any number of arguments or none at all. If function doesn't accept any argument then the parentheses is left empty.

In the next line, we have a block of statements or function body. The function body contains statements which defines what the function does. As usual, Python uses indentation of statements to determine when block starts and ends. All the statements in the body of the function function must be equally indented otherwise you will get a syntax error.

Pay special attention to the last statement in the function body i.e <return statement>. The return statement is used to return a value from the function. A return statement is not mandatory, some function return values while other don't. If a function doesn't have return statement in the body then a reserved keyword None is returned automatically. None is actually an object of a built-in type NoneType. Don't worry if you find return statement confusing; they are not, we will discuss return statement in detail in the upcoming section.

Here is small function which prints current date and time along with a greeting:

python101/Chapter-13/first_function.py

import datetime

def greet():
    print("Hello !")
    print("Today is", datetime.datetime.now())

The greet() function doesn't except any arguments, that's why parentheses is left empty. The function body contains two print() statements. These two statement will be executed when we call greet() function. The greet() function doesn't return any value.

Function Call #

A function definition does nothing by itself. To use a function we must call it. The syntax of calling a function is as follows:

function_name(arg1, arg2, arg3, ... argN)

If function doesn't accept any arguments then use the following syntax:

function_name()

The following code calls greet() function:

greet()  # call the function greet()

python101/Chapter-13/calling_first_function.py

import datetime

def greet():
    print("Hello !")
    print("Today is", datetime.datetime.now())

greet()

Output:

Hello !
Today is 2017-06-20 13:20:45.281651

The function call must appear after the function is defined otherwise, you will encounter NameError exception. For example:

python101/Chapter-13/call_before_definition.py

import datetime

greet() ## ERROR: trying to call greet() before its defined

def greet():
    print("Hello !")
    print("Today is", datetime.datetime.now())

Output:

q@vm:~/python101/Chapter-13$ python call_before_definition.py 
Traceback (most recent call last):
  File "call_before_definition.py", line 3, in <module>
    greet() ## ERROR: trying to call greet() before its defined
NameError: name 'greet' is not defined
q@vm:~/python101/Chapter-13$

When a function is called the program control jumps to that function definition and executes the statements inside the function body. After executing the body of the function, the program control jumps back to the part of the program which called the function, and program resumes execution at that point.

The following example demonstrates what happens when a function is called.

python101/Chapter-13/transfer_of_control.py

import datetime

def greet():
    print("Hello !")
    print("Today is", datetime.datetime.now())


print("Before calling greet()")
greet()
print("After calling greet()")

Output:

Before calling greet()
Hello !
Today is 2017-06-20 13:20:45.281651
After calling greet()

In lines 3-5, we have defined a greet() statement. The print() statement in line 8, prints string "Before calling greet()" to the console. In line 9, we are calling greet() function. At this point, statement execution of statements following the call to greet() halts and program control jumps to the definition of the greet() function. After executing the body of the greet() function program control again jumps back to the point where it left off and resumes the execution from there.

transfer-of-control.png

Our previous program has only function. It is not unusual for programs to have hundreds or even thousands of functions. In python, it a common convention to define a function called main() which gets called when the program start. This main() function then goes on to call other function as needed. The following program demonstrates flow of program control when we have two functions in a program.

python101/Chapter-13/two_func_program_control.py

import datetime

def greet(name):
    print("Hello", name, "!")
    print("Today is", datetime.datetime.now())


def main():
    print("main() function called")
    greet("Jon")
    print("main() function finished")

main()

In lines 3-5, and 8-11, we have defined two functions greet() and main(). The greet() function is now updated to accept an argument called name, which it then uses in the next line greet the user.

The main() function doesn't accept any arguments and has three statements inside the body.

The statement in line 13, calls the main() function. The program control jumps to the body of the main(). The first statement prints string "main() function called". The statement in line 10, calls the greet() function with an argument "Jon" which is then assigned to variable name in the function header. At this point, execution of statements following call to greet() halts and program control jumps to the body of the greet() function. After executing the body of the greet() function, program control jumps back to where it left off and executes the print() statement in line 11. As there are no more statements left to execute in the main() function and program control jumps again back to where it left off to execute statements after the function call.

Local Variables, Global Variables and Scope #

Variable Scope: Scope of variable refers to the part of the program where a variable can be accessed.

Variable we create inside the function are called local variables. Local variables can only be accessed inside the body of the function in which it is defined. In other words, the scope of local variable starts from the point they are defined and continues until the end of the function. Local variables are subject to garbage collection as soon as the function ends. As a result, trying to access a local variable outside it's scope will result in an error.

On the other end of the spectrum, we have global variables. Global variable are variables that are defined outside of any functions. Scope of global variable starts from the point they are defined and continues until the program ends.

Consider the following examples:

Example 1:

python101/Chapter-13/variable_scope.py

global_var = 200  # a global variable

def func():

    # local_var_1 is local variable
    # and is only available inside func() function
    local_var = 100

    print("Inside func() - local_var =", local_var)

    # accessing a global variable inside a function
    print("Inside func() - global_var =", global_var)


func()

print("Outside func() - global_var =", global_var)

# print("Outside func() - local_var =", local_var)  # ERROR: local_var is not available here

Output:

$: python.exe if_statement.py
Inside func() - local_var = 100
Inside func() - global_var = 200
Outside func() - global_var = 200

In line 1, we have created a global variable named global_var. It is then accessed in line 12, inside the func() function and in line 17, outside the function. We have also declared a local variable named local_var inside the function func(). It is then accessed inside the function in line 9.

Let's see what happens, if we try to access a local variable outside the function. To do so, uncomment the code in line 18 and run program again.

q@vm:~/python101/Chapter-13$ python3 variable_scope.py 
Inside func() - local_var = 100
Inside func() - global_var = 200
Outside func() - global_var = 200
Traceback (most recent call last):
  File "variable_scope.py", line 37, in <module>
    print("Outside func() - local_var =", local_var)
NameError: name 'local_var' is not defined
q@vm:~/python101/Chapter-13$

NameError: name 'local_var' is not defined tells us that there is no variable named local_var exists in this scope.

What if we have local and global variables of same name ? Consider the following program.

Example 2:

python101/Chapter-13/same_global_and_local.py

num = "global"  # global num variable

def func():
    num = "local"   # local num variable, entirely different from global num variable
    print(num)

func()
print(num)

Output:

$: python same_global_and_local.py
local
global

Here we have a global variable num in line 1 and a local variable of the same name inside the function in line 4. Whenever there is a conflict between a local and global variable inside the function, the local variable gets the precedence. This is the reason why print() function printed the value of the local num variable. However, outside the function num refers to the global num variable.

We can also use the same variable names in different function without conflicting with each other.

python101/Chapter-13/same_variables_name_in_different_function.py

def func_1():
    x = 100  # this x is only visible inside func_1()
    print("func_1(): x =", x)
    x = 200
    print("func_1(): x =", x)


def func_2():
    x = "str" # this x is only visible inside func_2()
    print("func_2(): x =", x)
    x = "complex"
    print("func_2(): x =", x)

# x is not visible in here

func_1()
func_2()

Output:

q@vm:~/python101/Chapter-13$ python3  same_variables_name_in_different_function.py
func_1(): x = 100
func_1(): x = 200
func_2(): x = str
func_2(): x = complex

Passing Arguments #

An argument is nothing but a piece of data passed to the function, when it is called. As said before, a function can take any number of arguments or non at all. For example, print() function accepts one or more arguments but random.random() function accepts none.

If you want a function to receive arguments, when it is called, we must first define one or more parameters. A parameter or parameter variable is simply a variable in the function header which receives an argument when the function is called. Just like local variables, the scope of parameter variables is only limited to the body of the function. Here is an example of a function which accepts a single argument:

def add_100(num):
    print("Result =", num+100)

When function add_100() is called with an argument, the value of the argument is assigned to the variable num and the print() statement prints the value of num after adding 100 to it.

The following program demonstrates how to call a function with argument.

python101/Chapter-13/function_argument.py

def add_100(num):  # num is a parameter
    print("Result =", num+100)


x = 100
add_100(x)

Output:

Result = 200

In line 6, function add_100() is called with an argument 100. The value of argument is then assigned to parameter variable num.

Example 2: Function to calculate the factorial of a number.

python101/Chapter-13/factorial.py

def factorial(n):
    f = 1
    for i in range(n, 0, -1):
        f *= n
        n -= 1

    print(f)

num = input("Enter a number: ")
factorial(int(num))

Output:

Enter a number: 4
24

The factorial of a number n is defined as multiplication of all digits from 1 to n.

n! = 1 * 2 * 3 * ... * (n-1) * n

where n! denotes factorial of n. Here are some examples:

6! = 1 * 2 * 3 * 4 * 5 * 6
4! = 1 * 2 * 3 * 4

Now, let's see how the for loop works when the value of n is 4:

Before for loop starts i not defined f = 1 n = 4
After 1st iteration i = 4 f = n * f = 4 * 1 = 4 n = 3
After 2nd iteration i = 3 f = n * f = 3 * 4 = 12 n = 2
After 3rd iteration i = 2 f = n * f = 2 * 12 = 24 n = 1
After 4th iteration i = 1 f = n * f = 1 * 24 = 24 n = 0

After the 4th iteration loop terminates and print() function prints the factorial of the number.

Example 3: Passing multiple arguments to the function

python101/Chapter-13/multiple_arguments.py

def calc(num1, num2):
    print("Sum = ", num1 + num2)
    print("Difference = ", num1 - num2)
    print("Multiplication = ", num1 * num2)
    print("Division = ", num1 / num2)
    print()  # prints a blank line

calc(10, 20)

Output:

Sum =  30
Difference =  -10
Multiplication =  200
Division =  0.5

When calc() function is called in line 8, the argument 10 is passed to parameter variable num1 and 20 is passed to parameter variable num2.

The order of arguments passed while calling the function must match order of parameters in the function header, otherwise, you may get unexpected results.

Pass by Value #

Recall that everything in Python is object. So a variable for an object, is actually a reference to the object. In other words, a variable stores the address where an object is stored in the memory. It doesn't contain the actual value itself.

When a function is called with arguments, it is the address of the object stored in the argument is passed to the parameter variable. However, just for the sake of simplicity, we say the value of argument is passed the to the parameter while invoking the function. This mechanism is known as Pass By Value. Consider the following example:

def func(para1):
    print("Address of para1:", id(para1))
    print(para1)

arg1 = 100
print("Address of arg1:", id(arg1))
func(arg1)

Output:

Address of arg1: 1536218288
Address of para1: 1536218288
100

Notice that the id values are same. This means that variable arg1 and para1 references the same object. In other words, both arg1 and para1 points to the same memory location where object(100) is stored.

arg1_and_para1_references_same_object.png

This behavior has two important consequences:

  1. If arguments passed to function is immutable, then the changes made to the parameter variable will not affect the argument.

  2. However, if the argument passed to the function is mutable, then the changes made to the parameter variable will affect the argument.

Let's examine this behavior by taking some examples:

Example 1: Passing immutable objects to function.

python101/Chapter-13/passing_immutable_objects.py

def func(para1):
    para1 += 100  # increment para1 by 100
    print("Inside function call, para1 =", para1)

arg1 = 100
print("Before function call, arg1 =", arg1)
func(arg1)
print("After function call, arg1 =", arg1)

Output:

Before function call, arg1 = 100
Inside function call, para1 = 200
After function call, arg1 = 100

In line 7, func() is called with an argument arg1 (which points to immutable object int). The value of arg1 is passed to the parameter para1. Inside the function value of para1 is incremented by 100 (line 3). When the function ends the statement in line 8 is executed and prints "After function call, arg1 = 100". This proves the point that no matter what function does to para1, the value of arg1 remains the same.

If you think about it this behavior makes perfect sense. Recall that the contents of immutable objects can't be changed. So whenever we assign a new integer value to a variable we are essentially creating a complete new int object and at the same time assigning reference of the new object to the variable. This is exactly what's happening inside the func() function.

Example 2: Passing mutable objects to function

python101/Chapter-13/passing_mutable_objects.py

def func(para1):
    para1.append(4)
    print("Inside function call, para1 =", para1)

arg1 = [1,2,3]
print("Before function call, arg1 =", arg1)
func(arg1)
print("After function call, arg1 =", arg1)

Output:

Before function call, arg1 = [1, 2, 3]
Inside function call, para1 = [1, 2, 3, 4]
After function call, arg1 = [1, 2, 3, 4]

The code is almost the same, but here we are passing a list to a function instead of an integer. As list is a mutable object, consequently changes made by the func() function in line 3, affects the object pointed to by variable arg1.

Positional and Keyword Arguments #

Arguments to a function can be passed in two ways:

  1. Positional argument.
  2. Keyword argument.

In the first method we pass arguments to a function in same order as their respective parameters in the function header. We have been using this method to pass arguments to our functions. For example:

python101/Chapter-13/pythagorean_triplets.py

def is_pythagorean_triplet(base, height, perpendicular):
    if base ** 2 + height ** 2 ==  perpendicular ** 2:
        print("Numbers passed are Pythagorean Triplets")
    else:
        print("Numbers passed are not Pythagorean Triplets")

The statement is_pythagorean_triplet(3, 4, 5) passes 3 to base, 4 to height and 5 to base, and prints "Numbers passed are Pythagorean Triplets". However, the statement is_pythagorean_triplet(3, 5, 4), passes 3 to base, 5 to height and 4 to perpendicular and prints "Numbers passed are not Pythagorean Triplets", which is wrong. So when using positional arguments always make sure that order of arguments in function call and order of parameters in function header match. Otherwise, you may get expected results.

The other way to pass arguments to a function is to use Keyword arguments. In this method we pass each argument in the following form:

parameter_name = val

where parameter_name is the name of the parameter variable in the function header and val refers to the value you want to pass to the parameters variables. Because, we are associating parameter name with values, the order of arguments in the function call doesn't matter.

Here are some different ways in which we can call is_pythagorean_triplet() function using keyword arguments:

is_pythagorean_triplet(base=3, height=4, perpendicular=5) 

is_pythagorean_triplet(base=3, perpendicular=5, height=4)

is_pythagorean_triplet(perpendicular=5, height=4, base=3)

Keyword arguments are little bit flexible because we don't have to remember the order of parameters in the function header.

Mixing Positional and Keyword arguments #

We can also mix positional arguments and keyword arguments in a function call. In doing so, the only requirement is that positional arguments must appear before any keyword arguments. It means that the following two calls are perfectly valid because in both calls positional arguments are appearing before keyword arguments.


## valid 

is_pythagorean_triplet(3, 4, perpendicular=5)
is_pythagorean_triplet(3, perpendicular=5, height=4)

However, we can't do this:


# not valid

is_pythagorean_triplet(3, height=4, 4)

The problem is that the positional argument is appearing after the keyword argument. Trying to call is_pythagorean_triplet() in this way results in following error.

q@vm:~/python101/Chapter-13$ python3 pythagorean_triplets.py 
  File "pythagorean_triplets.py", line 8
    is_pythagorean_triplet(3, height=4, 4)
                                       ^
SyntaxError: positional argument follows keyword argument

Returning Values #

Up to this point we have been creating functions which don't return any values, such functions are also known as void functions.

To return value from a function we use return statement. It's syntax is:

return [expression]

Square brackets ([]) around expression indicates that expression is optional. If omitted a special value None is returned.

When return statement is encountered inside a function, the function terminates and the value of the expression followed by the return keyword is sent back to the part of the program that called the function. The return statement can appear anywhere in the body of the function. The functions which returns values are known as value-returning functions.

Here is an example:

def add(num1, num2):
    return num1 + num2

A function can be called in two ways, depending upon they return value or not.

If a function returns a value then a call to such a function can be used as an operand in any expression in the program. For example:

result = add(12, 10)

In the above expression, we are first calling the add() function, and then assigning the return value of the function to the result variable. Had we not used the return statement in the add() function, we wouldn't be able to write this code. Here are some other ways in which we can call add() function.

result = add(12, 10) * 10  # return value of add() is multiplied by 10 and then assigned to result

if add(12, 10) == 100:      #  return value of add() is compared with 100 in the if statement
    print("It is True")

print(add(12, 10))    # return value of add() is printed

We are not obliged to use return value from the function. If we don't want to use return value, just call function as a statement. For example:

add(12, 10)

In this case the return value of add() is simply discarded.

Let's rewrite out factorial program to return the factorial intead of printing it.

python101/Chapter-13/return_factorial.py

def factorial(n):
    f = 1
    for i in range(n, 0, -1):
        f *= n
        n -= 1

    return f


print("Factorial of 4 is", factorial(4))
print("Factorial of 4 is", factorial(6))

Output:

Factorial of 4 is 24
Factorial of 4 is 720

In the above example, we are returning an integer value from the function, in fact, we can use any type of data int, float, str, bool; you name it. The following program demonstrates how to return bool type from the function:

python101/Chapter-13/is_even_or_odd.py

def is_even(number):
    if number % 2 == 0:
        return True
    else:
        return False


num = int(input("Enter a number: "))

if is_even(num):
    print(num, "is even")
else:
    print(num, "is odd")

1st run Output:

Enter a number: 13
13 is odd

2nd run Output:

Enter a number: 456
456 is even

If expression followed by return keyword is omitted then a special value None is returned.

def foo():
    return

result = foo()
print(result)

Output:

None

We can also use return statement multiple times inside the function but as soon as the first return statement is encountered the function terminates and all the statements following it are not executed. For example:

python101/Chapter-13/grade_calculator.py

def calculate_grade(marks):
    if marks >= 90:
        return 'A'
    elif marks >= 80:
        return 'B'
    elif marks >= 70:
        return 'C'
    elif marks >= 60:
        return 'D'
    else:
        return 'F'


m = int(input("Enter your marks: "))
print("Your grade is", calculate_grade(m))

1st run Output:

Enter your marks: 68
Your grade is D

2nd run Output:

Enter your marks: 91
Your grade is A

Void Function returns None. #

In Python, void functions are slightly different than functions found in C, C++ or Java. If the function body doesn't have any return statement then a special value None is returned when the function terminates. In Python, None is a literal of type NoneType which used to denote absence of value. It is commonly assigned to a variable to indicate that the variable does not points to any object.

The following program demonstrates that the void functions return None.

python101/Chapter-13/void_function.py

def add(num1, num2):
    print("Sum is", num1 + num2)

print(add(100, 200))

Output:

Sum is 300
None

Sure enough! add() function indeed returns None. So we can say that in Python, all functions return value whether you use return statement or not. However, this doesn't mean that you can use void functions just like a value-returning function(). Consider the following example:

python101/Chapter-13/using_void_function_as_non_void_function.py

def add(num1, num2):
    print("Sum is", num1 + num2)


result = add(10, 200) + 100

Output:

Traceback (most recent call last):
  File "D:/python101/if_statement.py", line 5, in <module>
    result = add(10, 200) + 100
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
Sum is 210

In line 5, we are trying to add value returned from add() i.e None to the integer 100. But the operation failed because + operation can't add NoneType to int.

Generally a void function is invoked as statement like this:

add(100, 200)

Returning Multiple Values #

To return multiple values from a function just specify each value separated by comma(,) after the return keyword.

return val1, val2, val3, ..., valN

When calling a function returning multiple values, the number of variables on the left side of = operator must be equal to the number of values returned by the return statement. So if a function returns two values then you must use 2 variables to on the left side of = operator.

Here is an example:

python101/Chapter-13/returning_multiple_values.py

def sort_two_num(num1, num2):
    if num1 < num2:
        return num1, num2
    else:
        return num2, num1


number1, number2 = sort_two_num(100, 15)
print("number1 is", number1)
print("number2 is", number2)

Output:

q@pro-vm:~/python101/Chapter-13$ python3 sort_two_num.py 
number1 is 15
number2 is 100

Notice how values are assigned while calling the function. The statement:

The statement `number1, number2 = sort_two_num(100, 15)`

assigns smaller number to variable number1 and greater number to variable number2.

Default Arguments #

In Python, we can define function with default parameter values, this default value will be used when a function is invoked without any argument. To specify default value for the parameter just specify the value using assignment operator followed by parameter name. Consider the following example:

def calc_area(length=2, width=3):
    print("length=", length,  ", width = ", width)
    print("Area of rectangle is", width * length)


calc_area()
calc_area(4, 6)
calc_area(width=100, length=23)
calc_area(length=12)

In line 6, we are calling function calc_area() without any arguments, so default values 2 and 3 will be assigned to length and width parameters.

In line 7, we are calling calc_area() by passing 4 to length and 6 to width. Because values to both parameters are provided while calling the function, default value will not be used in this case. The same is true for calc_area() call in line 8, except here we are using keyword arguments.

In line 9, we are only providing value to length parameter using keyword argument, as a result default value for width parameter will be used.