A function groups related statements that perform a specific task. The statements inside a given function are only executed when the function is called.

A Function typically has three parts i.e. declaration, implementation and calling.

The following example shows the simplest Python function, the function simply does 'nothing'.

def my_function():
   pass
my_function()

Despite the above program being very simple, it still  illustrates clearly  the three parts of a Function i.e declaration, implementation and calling:

Python function stages

Calling functions

We will begin with exploring how to call functions because you have most likely already done so, unless you are absolutely new to Python and programming in general. 

Functions are inactive until you call them. Without calling a function, the statements within its block would never get executed. 

To call a function you simply use the function name, followed by parentheses. as shown below:

function_name()

Some functions requires some data to be provided in advance in order to accomplish their tasks. In such a case, we pass the data to the functions when calling it in form of arguments.

function_name(arguments)

 Example:

print("Hello, World!")

In the above case the string  "Hello, World!" is the argument  which we are passing to the built-in print() function.

If a function takes more than one argument, we separate the arguments inside the parentheses with commas.

For example to print more than one value, we pass more comma-separated values to the  print() function,as shown below:

print("Hello", "Foo", "Bar")
Positional and Keyword arguments

In Python the arguments can be passed to a function either as positional arguments or as keyword arguments.

Positional arguments are passed to a function in the order in which the parameters are defined in the function declaration. For example:

print('Hello', 'Foo', 'Bar')

In the above example, the values 'Hello', 'Foo', and 'Bar' are passed as positional arguments to the print() function. 

def my_function(x, y, z):
    #statements
my_function(1, 2, 3)

In the above case , when we call my_function with 12, and as the positional arguments, 1 is assigned to x, 2 is assigned to y, and 3 is assigned to z.

Keyword arguments, on the other hand, are passed to a function using the key=value syntax. For example:

def my_function(x, y, z):
   #statements
my_function(x = 1, y = 2, z = 3)

In this example, the values 1, 2, and 3 are passed as keyword arguments to the function my_function. The argument x is explicitly assigned the value 1, the argument y the value 2, and the argument z the value 3. In keyword arguments,  the order is not important but the names must match those specified in the declaration. 

You can also use a combination of positional and keyword arguments in the same function call. However, positional arguments must always come before keyword arguments. For example:

Example:

print('Hello','Foo','Bar', sep = '\n')

In this example, 'Hello','Foo' and 'Bar' are passed as positional arguments while 'sep' is passed as a keyword argument.

def my_function(x, y, z ):
    #statements
my_function(1,2, z = 3)

In this example, the values 1 and 2 are passed as positional arguments, and the argument z is assigned the value in form of a keyword argument.

Trying to put keyword arguments before positional arguments will raise a SyntaxError. The  number of arguments passed must also match the number of parameters defined in the function declaration. If the number of arguments passed is less than or greater than the number of parameters defined, a TypeError will be raised.

Function Declaration

Function declaration involves the following components:

  • The def keyword 

The def keyword is used to mark the start of a function. On encountering this keyword, the Python interpreter recognizes that the subsequent lines of code are meant for function definition.

  • Function name 

The function name is used to uniquely identify the function, it can be any valid identifier. The rules for naming functions are literally the same with those of naming variables, e.g

  1. The name cannot be a keyword
  2. The name can only contain alphanumeric characters ( A-Z, a-z, 0-9 )  and an underscore( _ ) 
  3. The first letter of the name cannot be a numerical value. It can only be an alphabetic letter or an underscore.
  4. Spaces are not allowed in the name.
  5. The name cannot contain special characters except the underscore

It is recommendable that the function name be descriptive such that someone reading the source code can easily tell what task the function is intended to accomplish without necessarily reading through the function body.  

  • Parameters

The difference between parameters and arguments is that, parameters are used in function declaration while arguments are the actual values passed when the function is being called.

difference between parameters and arguments

The parameters are used as placeholders for the values that will be passed as arguments when the function is called. They are enclosed inside parentheses just after the function  name. The parentheses are necessary , even if the function does not define any parameter.

def simple_function():
    print("Hello, World!")

simple_function()

The following example shows a function to add two numbers that defines two parameters ; a and b :

def add(a, b):
   total = a + b
   print(total)

add(a = 9, b = 20)

We can set a default value to a parameter so that if it is not assigned a value when the function is being called, the default value will be used.

def add(a, b = 10):
   total = a + b
   print(total)

add(a = 9)

In the above case, 10, is used as the value of b because we did not assign it a value when calling the function. If we provide a value of b, the default value will get overwritten with the new value. Example:

def add(a, b = 10):
   total = a + b
   print(total)

add(a = 9, b = 6)

We can define functions that takes arbitrary number of arguments, The best example of such a function is the built-in print() function, which can take arbitrary number of values to display to the console.

print('a', 'b', 'c', 'd', 1, 2, 3, 4)

To define a function that takes arbitrary number of positional arguments, we use the operator, used this way, it is called the unpacking operator. 

the syntax is as follows:

def function_name(*args):
    #statements

It is a common convention to use the word "args" with the unpacking operator when specifying arbitrary positional arguments, as shown above.

A tuple containing all the arguments is returned, meaning that we can  do all tuple operations on them such as iteration, indexing, slicing, etc. The following example shows a functions that takes arbitrary numbers and returns their sum.

def add(*args):
   total = 0
   for i in args:
      total += i
   print(total)

add(1, 5, 9, 34, 81, 100)

In order for a function to take arbitrary number of keyword arguments, we use the ** operator, used this way, it is referred to as the " double asterisk unpacking operator ". the syntax is as follows;

def function(**kwargs):
    #statements

The values passed as keyword arguments are utilized as a standard Python dictionary.

def my_func(**kwargs):
   print(type(kwargs))

my_func()

The following example is a function that takes arbitrary numbers and returns their sum:

def add(**kwargs):
  total = 0
  for v in kwargs.values():
     total += v
  print(total)

add(a = 10, b= 20, c= 50, d= 100)

We can use both of the unpacking parameters in the same function declaration, as shown below:

def my_func(*args, **kwargs):
    #statements

The single asterisk parameter always comes before the double asterisk parameter, otherwisa a SyntaxError is raised.

Each of the unpacking operators cannot appear more than once in a function declaration.

def func( *a, *b):
//SyntaxError: * argument may appear only once

We can define other parameters alongside the unpacking parameters, this way, the unpacking parameters are always defined last, otherwise a syntaxError will be raised. for example:

def my_func(a, b, c=0, *args, **kwargs):
    #statements
  • A colon

We must terminate the function's header with a colon to indicate the end of the function's declaration and the beginning of its implementation. Just like in other blocks, the subsequent statements after the colon must be indented. 

Function Implementation

This is the part where we actually put the statements which gets executed when the function is called. It is the body of the function where you define its behavior and what it should do when called.

Any valid statements and expressions such as loops, conditionals, assignment, function calls e.tc,  can be put in the function's body.

Let us look at some examples:

A function to check if a given number is even or odd:

def even_or_odd(num):
   check = 'Odd' if num % 2 else 'Even'
   print("{} is {}".format(num, check))

even_or_odd(10)
even_or_odd(5)

A function that prints all the integers from 0 to 9:

def my_function():
   for i in range(10):
      print(i)

my_function()

1

2

3

4

5

6

7

8

9

A function that takes two arguments a and b, and performs an arithmetic operation on them depending on the operator given in the argument oper:

def evaluate(a, b, oper):
    S = "{} %s {} = {}"%oper
    match oper:
       case '+':
           print(S.format(a, b, a + b))
       case '-':
           print(S.format(a, b, a - b))
       case '*':
           print(S.format(a, b, a * b))
       case '/':
           print(S.format(a, b, a / b))
       case '**':
           print(S.format(a, b, a ** b ))
       case _:
           print('Unsupported operator {}'.format(oper))

evaluate(3, 4, '*')
evaluate(50, 10, '-')
evaluate(3, 9, '&')
Function Docstring

We can put a docstring as the first statement in the function body to  serve as the function's documentation.  The docstring can  describe what the function does, what parameters it takes, what values it returns (if any), and any exceptions that may be raised. The docstring is typically a string using a triple quotes format. Example:

def add(a, b):
    '''Outputs the sum of a and b'''   #this is the docstring
    sum = a + b
    print(a)

def larger(a, b):
    """Displays the larger number of a and b. Prints 'Equal' if a and b have equal value."""
    if a > b:
       print(a)
    elif b > a:
       print(b)
    else:
       print('Equal') 

The help() function includes the function's docstring in the help text when it is  called with a function as its argument. We can also use the  __doc__ attribute to view just the function's docstring without additional information. Example:

def add(a, b):
   """Displays the sum of a and b."""
   sum = a + b
   print(sum)

help(add)
//Help on function add:
//
//add(a, b)
//    Displays the sum of a and b.
//
print(add.__doc__)
//Displays the sum of a and b.
The return statement

The return statement is used when a function wants to send some information back to its caller. This information is usually the final results after the function is done manipulating the provided data or a status indicator that tells the caller whether the function executed successfully or encountered an error.

The value returned by a function can be of any type such as strings, integers, floats, booleans, lists, tuples, dictionaries, user-defined  objects, etc..

The moment a return statement is encountered, the function  terminates  returning the specified value(if any). For example the following function simply returns 1 to the caller:

def func():
   return 1

print(func())
//1

We can perform any valid operations and expressions on the value returned by the function such as assigning it to variables, using it as arguments in function calls such as in print() function, etc.

def func():
   return 1

a = func()
print (a)
//1

Any function that does not have a return statement returns the default value  None, the only exception is functions that use the yield statement, which we will look at later.

def func():
   pass

print(func())
//None

def func2():
    print("Hello, World!")

a = func2()
//Hello, World!
print(a)
//None

As a beginner you should not confuse between  print statement inside a function  and the return statement. The print statement simply displays a value to the console but does not affect the execution of the program and does not return a value that can be used in other parts of the program

None is also returned if the return statement is used without a value, it is especially used this way to terminate the function prematurely when the desired result has already been achieved or when an error condition is encountered.

def func():
   return

print(func())
//None

More Examples:

a function that takes two numbers and returns their sum:

def add(a, b):
    sum = a + b
    return sum

print(add(1, 2))
//3
print(add(4,5))
//9

A function that returns the largest of 3 numbers:

def largest(a, b, c):
   if a > b :
       if a > c:
          return a
   else:
       if b > c :
           return b
       else:
           return c

largest(1, 2, 3)
//3
largest(23, 45, 37)
//45   

As shown above, a function can have multiple return statements , the one that is encountered first causes the function to terminate without executing the statements below it. It is, therefore, necessary to ensure that return statements are in logical order and they cover all possible scenarios.