Skip to content

017 Functions in Python

Introduction

Purpose

In this session, we will learn about Functions in Python. In essence, a function allows us to write better, more compact and re-usable code. This is a concept we will use a lot in later sessions, so make sure you fully familiarise yourself with the material.

Prerequisites

You will need some understanding of the following:

In particular, you will need to recall how to use:

Introduction to Functions

A function is a block of code statements that we can use to carry out a specific purpose.

The simplest form of function has no inputs or outputs, but simply performs some task when we call it:

function

An example of a simple function in Python is:

def hello_world():
    '''
    Purpose:
      print the string 'hello world'
    '''
    print('hello world')

This is designed to print the string hello world when we call it.

Notice the formatting here: the function is declared:

def hello_world():

and the contents of the function are indented in (by 4 spaces here).

We use the function in Python code as:

hello_world()
hello world

and access the function document string by:

help(hello_world)
Help on function hello_world in module __main__:

hello_world()
    Purpose:
      print the string 'hello world'

Exercise 1

  • in a new code cell below, write a function called my_name that prints your name
  • demonstrate that your code works (i.e. run it in a code cell)
  • show the doc string using help()

Advice: make sure it has an appropriate document string, based on the example in the notes, and also check that you have the indentation correct for the code in the function. Notice the semicolon : at the end of the def statement.

Function specification

More generally, we could think of the the function as a sort of filter: it takes some inputs (specified in the arguments), makes some calculation based on these, i.e. that is a function of these inputs, and returns an output.

function

In this sense:

  • It will generally have one or more arguments: (arg1, arg2, ...) that form the inputs.
  • It will often return some value (or set of values) as the output: retval
  • It will have a name: my_function

function io

Anatomy of a function

The format of a function in Python is:

def my_function(arg1,arg2,...):
  '''
  Document string 
  '''

  # comments

  retval =  ...

  # return
  return retval

The keyword def defines a function, followed by the function name, a list (actually, a tuple) of arguments, then a semicolon :.

The contents of the function are indented to a consistent level of spaces.

The function will typically have a document string, generally a multi-line string defined within triple quotes. We use this to document information about the function, such as its author, purpose, and inputs and outputs.

Within the function, we can refer to the arguments (arg1 and arg2 here, though they will generally have more meaningful names), make some calculation based on these, and generally, return some value (retval here).

Code design

This idea of a filter can be useful when thinking how to design a function. We can see that we need to define:

* purpose
* inputs
* output

Let's suppose we need to design a function that will take a first name and last name, and combine them into your full name (assuming for now that you have two names).

The purpose of our function could be stated as:

purpose:

    generate a name string from list of strings

The inputs could be:

inputs:
  - name_list : list of names

And the output:

return:
  - the full name

Without knowing any real coding then, we could develop the template for this function, along with an initial document string.

We do need to give the function a name, so let's use full_name here.

We have started with the idea of some purpose for our code, then defined what the expected inputs and outputs would be. We can call coding at that level of generalisation pseudocode. We could have written our task is a form of pseudocode such as:

algorithm full_name is
    input: List of strings in variable name_list
    output: string in variable retval

    purpose: generate a name string from list of strings

    # CODE BLOCK to achieve aim (NOT DONE)
    # test by passing input to output
    retval = name_list

where we have left the CODE BLOCK blank at the moment, and replaced it by simply sending the function input to the output so we can test the code structure. It can be of value when designing codes to first develop some pseudocode such as above, but in reality such statements are very closely related to what we would write in high-level codes like Python:

def full_name(name_list):
    '''

    purpose: 
      generate a name string from list of strings 

    inputs:
    - name_list : list of names

    return:
    - the full name
    '''
    # CODE BLOCK to achieve aim (NOT DONE)
    # test by passing input to output
    retval = name_list

    # return
    return retval

That's a good start, and it allows us to develop a function that we can run and test.

To test, we can set a list of example strings. We then call the function full_name() with this argument, and set the value returned in the variable full.

names = ['Fred','Bloggs']

full = full_name(names)
print(full)
['Fred', 'Bloggs']

From our test, we can see that the function doesn't yet achieve what we wanted: it simply returns the input list, rather than the full name.

To proceed, we need to know how to make a combined string. It can be useful to test our understanding of the code we will need to achieve the aim of the function. We do not need to do that inside the function, but can instead try to think of some examples we could use to test the ideas.

One way to achieve the aim of the function this would be to use the string join operation that we came across the in Python string methods notes.

This works by placing a key string between string items in a list. For example, if we want to separate strings by :, we would use:

':'.join(names)
':'.join(names)
'Fred:Bloggs'

In our function, we want to use a single 'whitespace' value, so ' ' as the key:

' '.join(names)
'Fred Bloggs'

Now we are sure of the coding concept to achieve what we want in the filter, we can write the function:

def full_name(name_list):
    '''

    purpose: 
      generate a name string from list of strings 

    inputs:
    - name_list : list of names

    return:
    - the full name
    '''
    # join the names in name_list together
    retval = ' '.join(name_list)

    # return
    return retval

we try to make the docstring useful and test what it shows:

help(full_name)
Help on function full_name in module __main__:

full_name(name_list)
    purpose: 
      generate a name string from list of strings

    inputs:
    - name_list : list of names

    return:
    - the full name

then run our code:

full = full_name(['Fred','Bloggs'])
print(full)
Fred Bloggs

Test

It is a good idea if we can write a test for our function. This should cover some typical case or cases, and check that we get the correct output for a particular input. We can use the assert method that we have seen in the Python for notes:

assert True

For example:

assert full_name(['Fred','Bloggs']) == "Fred Bloggs"
print('test passed')
test passed

remember that if this assertion fails, we get an AssertionError (you can try that out by putting something incorrect in the assertion above and re-running the cell). If the error is raised, our code will strop running and report the error.

We will learn more about code testing later, but for the moment, we suggest that you use one or more assert statements that try out different inputs-output matches with your function.

Exercise 2

We assume for this exercise that you know how to create a dictionary from two lists of the same length. This was covered in the Python_Groups notes.

In this exercise, we suggest that you follow the design approach we took above:

  • Think first what you will use as inputs and outputs to the function, and come up with some examples of inputs and outputs
  • Then consider the Python code you would need to go from the inputs to the outputs
    • Develop and test the core code to achieve the function purpose in a notebook cell with an example input
    • Consider what you might use as a test for your code
  • Develop skeleton
    • Write a skeleton function defining the purpose, inputs and outputs. In the skeleton code, you can just pass the inputs straight to the outputs.
    • Confirm that that works before going further.
    • Confirm that your document string is useful.
    • Write a test
  • Implement the core code in the function
    • Confirm that that works
    • Confirm that your document string is useful.
    • Write a test
  • Consider any flaws in your code and how you might improve it

Your task for the exercise is:

  • design a function to convert two lists of the same length into a dictionary
  • the design must include relevant comments, document strings and tests

More on arguments

Python functions can take two types of arguments:

  • positional arguments
  • keyword arguments

Positional arguments

The arguments we have used above are positional arguments, in that their definition in the function depends on the order they are specified in. For example:

def hello(s1,s2):
    '''
    Purpose:
      print out positional arguments

    Inputs:
      s1 : first argument
      s2 : secopnd argument
    '''
    print(f'argument 0 is {s1}')
    print(f'argument 1 is {s2}')

hello('hello','world')
argument 0 is hello
argument 1 is world

Sometimes in Python documentation, you will see the arguments specified simply as:

example(*args, **kwargs)

This is the most general way of specifying function arguments. The first item in this case *args are the positional arguments. Although we generally specify them explicitly as above, we can also use

*args

to specify them, where args is a list-like object. In this form, the example above becomes:

def hello(*args):
    '''
    print out positional arguments

    Inputs:
        *args : list of positional arguments
    '''
    # loop over the list
    for i,s in enumerate(args):
        print(f'argument {i} is {s}')

hello('hello','world','again')
argument 0 is hello
argument 1 is world
argument 2 is again
# or using *args where args is a list
l = ['hello','world','again','as','list']
hello(*l)
argument 0 is hello
argument 1 is world
argument 2 is again
argument 3 is as
argument 4 is list

In this example, we have not specified how many positional arguments there are, but obviously we need to attach some meaning to each of them in the order supplied. Sometimes this is useful in code, where we just want to loop over a list of arguments, but you should mostly be wary about using it unless you really need to.

A good example of the use of *args is the print() statement. It will print out however many positional arguments we specify:

print('hello','world','again')

l = ['hello','world','again','as','list']
# print the list, specifying l as a single positional argument
print(l)
# print the list passing each list item as a positional argument
print(*l)
hello world again
['hello', 'world', 'again', 'as', 'list']
hello world again as list

Keyword arguments

The second type of argument we mentioned above was keyword arguments. These are typically used to modify the behaviour of a function are are of the form:

verbose=True
sep=' '

We can see examples of these with the print function:

help(print)
Help on built-in function print in module builtins:

print(...)
    print(value, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

    Prints the values to a stream, or to sys.stdout by default.
    Optional keyword arguments:
    file:  a file-like object (stream); defaults to the current sys.stdout.
    sep:   string inserted between values, default a space.
    end:   string appended after the last value, default a newline.
    flush: whether to forcibly flush the stream.

where a set of optional keyword arguments are specified. All keyword arguments are specified with a default value (sep=' ', end='\n', file=sys.stdout, flush=False above). If we do not specify a keyword when we call the function, this is the value that that variable will take within the function.

Note that keywords must be specified after positional arguments. The keywords can be in any order (they are not positional). Keywords can only be given once.

But, if we want, we can override the defaults by setting the keyword when we call the function:

l = ['hello','world','again','as','list']
# print the list passing each list item as a positional argument
# default with sep as ' '
print(*l)
# with sep as 'X'
print(*l,sep='X')
# with sep as ':'
print(*l,sep=':')
hello world again as list
helloXworldXagainXasXlist
hello:world:again:as:list

This is a very useful feature for functions: we can set default behaviour, but the user can modify this when they call the function.

For example, let's add a verbose keyword to our hello() function. The behaviour we want is that if the verbose flag is set, we print lots of information to the user. In this case:

print(f'argument {i}:',end=' ')

which will print the index i. We have used the kwarg end='' for print() so that if this is called, it does not print a newline, but a space instead.

def hello(*args,verbose=False):
    '''
    print out positional arguments

    Inputs:
        *args : list of positional arguments

    Optional keyword arguments:
        verbose : print the index 
    '''
    # loop over the list
    for i,s in enumerate(args):
        # if the verbose flag is set
        # then print detailed information
        if verbose:
            print(f'argument {i}:',end=' ')
        print(f'{s}')

dash='='*5

# call without verbose
print(f'{dash} verbose=False {dash}')
hello('hello','world','again')

# call with verbose
print(f'{dash} verbose=True {dash}')
hello('hello','world','again',verbose=True)
===== verbose=False =====
hello
world
again
===== verbose=True =====
argument 0: hello
argument 1: world
argument 2: again

Exercise 3

  • Starting from the function list2dict(keys,values) that you developed above, add keyword arguments to the code to achieve the following:
    • if check=True : perform checks on the input data
    • if verbose=True : print out information on what is going on in the function
    • set all default keywords to False
  • Make sure you perform tests as above, and that you update document strings

As a final point of kwargs, you might still be wondering why this was specified as:

   example(*args,**kwargs)

above. We have seen what the *args part means: if args is a list, then each item in the list is passed as a positional argument. The same idea applies to **kwargs but instead of a list, kwargs refers to a dictionary. If you think about the information you need to pass for keword arguments, you would understand why this is the case.

By using **kwargs, where kwargs is a dictionary, the key-value pairs in the dictionary are passed as key=value. For example:

args = ['hello','world','again','as','list']
# set up dictionary for kwargs
# with X as sep and a string at the end oif the line
kwargs = {'sep' : 'X', 'end' : '<- end of the line\n'}

print(*args,**kwargs)
helloXworldXagainXasXlist<- end of the line

The use of **kwargs can be useful sometimes, as you can more easily keep track of keywords for some particular configuration of running a code. For that reason, and because you will see it sometimes in documentation, you should be aware of it. Most likely you won't be using it a lot in your early code development though.

lambda functions

Sometimes the function we want to write is very simple, and might consist of only a few statements on a single line. As an example, consider the function y(x) here:

def y(x):
    '''
    a function y(x) with
    zeros at x=4,x=3
    '''
    return 2.0*(x-3)*(x-4)

Whilst it is fine to write a function in this way, and it has the advantage of being quite explicit about what it is doing, it is not very efficient: there is a computational cost to calling a function, especially in a high-level language like Python.

If we were only ever going to use this function in places in the code where performance is unimportant, then we can go ahead with the above.

If we are worried that it might be used in a case when we would not want to slow it down unnecessarily with as function call, we can use a special type of function, called a lambda function. The syntax is:

    function_name = lambda args : function_code

where function_name is the name of the function, args is a list of arguments, and function_code here represents the (short!) code inside the function.

Our example above translates to:

y = lambda x : 2.0*(x-3)*(x-4)

which is a much neater and more efficient code than a full function. Use these when appropriate.

Generally, you are not expected to use a docstring with a lambda function, as it should be a simple statement that is self-evident from the code. If you do feel the need, you can add one with:

    y.__doc__ = '''
    a function y(x) with
    zeros at x=4,x=3
    '''

Exercise 4

Consider the function:

    def power_of_2(ilist):
        """
        output a list of 2 raised to the power of 
        the values of the input arguments

        Inputs:
            ilist : list of integers

        Output:
            list of [2**arg[0],2**arg[1],...]
        """

        # initialise list
        olist = []

        # loop over the arg list
        for i in ilist:
            # append the 2**i value
            olist.append(2**i)
        # return the list
        return olist
  • Test this function, inputting a list of integers from 0 to 4 inclusive
  • Write a more Pythonic version of this, making use of list comprehensions and lambda functions

Summary

In this section, we have learned about writing a function. We have seen that they generally will have zero or more input positional arguments and zero or more keyword arguments. They will typically return some value. We have also seen how we can define a doc string to give the user information on how to use the function, and also how we can use assert to build tests for our codes. We have been through some design considerations, and seen that it is best to plan you functions by thinking about the purpose, the inputs and the outputs. Then, for the core code, you just need to develop a skeleton code and docstring structure, test that, and insert your core code. You should think about modifications using keyword arguments that you might want to include, but these will often come in a second pass of development.

When we write Python codes from now on, we will often make use of functions.

Remember:

Anatomy of a function:

    def my_function(arg1,arg2,...,kw0=...,kw1=...):
      '''
      Document string 
      '''

      # comments

      retval =  ...

      # return
      return retval

Also written as:

    def my_function(*args,**kwargs):

We have also seen lambda functions, used for short functions:

    function_name = lambda args : function_code

Last update: October 6, 2021