Function Basics

circuitpython_function-big.png
Placed in the public domain by wikipedia user Wvbailey.

Simply put, functions are a block a code that is packaged up so as to be executed independently, how and when you choose.

Functions have parameters, and you provide arguments when you execute the function. That's generally referred to as calling the function, and the code that calls it is referred to as the caller.

The idea of functions in programming goes all the way back to early assembly languages and the concept of the subroutine. Most assembly languages have a CALL operation used to transfer control to the subroutine, and a RET operation to return control back to the calling code. The BASIC language also used the term subroutine and had a GOSUB statement that was used to call them, and a RETURN statement.

Most modern languages have dropped the explicit CALL or GOSUB statement in favor of implicit syntax to indicate a function call:

Download: file
function_name(arg1, arg2, ..., argn)

In assembly and BASIC, a subroutine had to end with an explicit RET or RETURN. Python loosens that up somewhat. If there is no value that needs to be sent back as the result of the function, the return can be left implicit, as in the following:

Download: file
def foo(x):
    print("X is {}".format(x))
Download: file
>>> foo(5)
x is 5

We can use an explicit return, but it gains nothing and takes an additional line of code.

Download: file
def foo(x):
    print("X is {}".format(x))
    return

There are two cases in Python where we do need to make the return explicit.

  1. When you need to return from the function before it naturally reaches the end.
  2. There is a value (or values) that needs to be sent back to the caller.

Guard Clauses

Below is a trivial example of the first case.

Download: file
def foo(x):
    if x > 5:
      return
    print("X is {}".format(x))
Download: file
>>> foo(4)
X is 4
>>> foo(10)

Using an if/return combination like this is often referred to as a guard clause. The idea being that it gaurds entry into the body of the function much like security at an airport: you have to get past inspection before being allowed in. If you fail any of the checks, you get tossed out immediately.

Preconditions

This is also something called a precondition, although that usually implies a stronger response than simply returning early. Raising an exception, for example. You use a precondition when the "bad" argument value should never be sent into the function, indicating a programming error somewhere. This can be done using assert instead of the if/return. This will raise an AssertionError if the condition results in False.

Download: file
def foo(x):
    assert x <= 5, "x can not be greater than 5"
    print("X is {}".format(x))
Download: file
>>> foo(10)
Traceback (most recent call last):
File "", line 1, in 
File "", line 2, in foo
AssertionError: x can not be greater than 5

Since preconditions are meant to catch programming errors, they should only ever raise an exception while you are working on the code. By the time it's finished, you should have fixed all the problems. Preconditions provide a nice way of helping make sure you have.

Although assert is supported, it's a little different in the context of CircuitPython. Your running project likely does not have a terminal connected. This means that you have no way to see that an assert triggered, or why. The CircuitPython runtime gets around this for runtime errors by flashing out a code on the onboard NeoPixel/DotStar. This lets you know there's a problem and you can connect to the board and see the error output. But as I said, by the time you get to that point, there should be no programming errors remaining to cause unexpected exceptions.

That's the key difference between things like guards and preconditions: your code should check for, and deal with, problems that legitimately could happen; things that could be expected. There's no way to handle situations that should never happen; they indicate that somewhere there's some incorrect code. All you can do is figure out what's wrong and fix it.

Returning a Result

The second case of requiring a return statement is when the function has to return a value to the caller. For example:

Download: file
def the_answer():
    return 42

So what if you have a function that is expected to return a value and you want to use a guard? There is no universal answer to this, but there is often some value that can be used to indicate that it wasn't appropriate to execute the function.

For example, if a function returns a natural number (a positive integer, and sometimes zero depending on which definition you use), you could return a -1 to indicate that a guard caused an early return. A better solution is to return some sort of default value, maybe 0. This, also, is very situation dependant. 

Multiple Return Values

Not only can functions return a value, they can return more than one. This is done by listing the values after the return keyword, separated by commas.

Download: file
def double_and_square(x):
    return x+x, x*x

A function that returns multiple values actually returns a tuple containing those values:

Download: file
>>> double_and_square(5)
(10, 25)

>>> type(double_and_square(5))
<class 'tuple'>

You can then use Python's parallel assignment to extract the values from the tuple into separate variables.

Download: file
>>> d, s = double_and_square(5)
>>> d
10
>>> s
25

Defining Functions

As you can gather from the examples above, you use the def keyword to define a function. The general form is:

Download: file
def function_name (parameter_name_1, ..., parameter_name_n):
    statement
    ...
    statement

You can then call the function by using the name and providing arguments:

Download: file
function_name(argument_1, ..., argument_n)

This much we can see in the examples. Note that a function doesn't require parameters and arguments If it does have some, those names are available inside the function, but not beyond it. We say that the scope of the parameters is the body of the function. The scope of a name is the part of the program where it is usable.

If you use a name outside of its scope, CircuitPython will raise a NameError.

Download: file
>>> foo
Traceback (most recent call last):
File "", line 1, in 
NameError: name 'foo' is not defined

Choosing good names for your functions is very important. They should concisely communicate to someone reading your code exactly what the function does. There's an old programmer joke that the person trying to read and understand your code will quite likely be you in a couple months. This becomes even more crucial if you share your code and others will be reading it.

You don't want to name a function the same name as a Python command, the name of a library function, or any other name that might be confusing to others reading your code.

Default Arguments

When a function is called, it must be provided with an argument for each parameter. This is generally done as part of the function call, but Python provides a way to specify default arguments: follow the parameter name by an equals and the value to be used if the function call doesn't provide one.

Download: file
def foo(x, msg=''):
    print("X is {}. {}".format(x, msg))
Download: file
>>> foo(5, 'hi')
X is 5. hi
>>> foo(5)
X is 5.

We can see that if we provide an argument for the msg parameter, that is the value that will be used. If we don't provide an argument for it, the default value specified when the function was defined will be used.

Keyword Arguments

So far the examples have been using what's called positional arguments. That just means that arguments are matched to parameters by their positions: the first argument is used as the value of the first parameter, the second argument is used as the value of the second parameter, and so on. This is the standard and is used by pretty much every language that uses this style of function definition/call.

Python provides something else: keword arguments (sometimes called named arguments). These let you associate an argument with a specific parameter, regardless of it's position in the argument list. You name an argument by prefixing the parameter name and an equals sign. Using the previous function foo:

Download: file
>>> foo(5, msg="hi")
X is 5. hi
>>> foo(msg='hi', x=5)
X is 5. hi

Notice that by naming arguments their order can be changed. This can be useful to draw attention to arguments that are usually later in the list. There's one limitation: any (and all) positional arguments have to be before any keyword arguments:

Download: file
>>> foo(msg='hi', 5)
File "", line 1
SyntaxError: positional argument follows keyword argument

Even without changing the order of arguments, keywords arguments lets us skip arguments that have default values and only provides the ones that are meaningful for the call. Finally, keyword arguments put labels on the arguments, and if the parameters are well named it's like attaching documentation to the arguments. Trey Hunner has a great write-up on the topic. To pull an example from there, consider this call:

Download: file
GzipFile(None, 'wt', 9, output_file)

What's all this? It's dealing with a file so 'wt' is probably the mode (write and truncate). The output_file argument is clearly the file to write to, assuming it's been well named. Even then, it could be a string containing the name of the output file. None and 9, however,  are pretty vague. Much clearer is a version using keyword arguments:

Download: file
GzipFile(fileobj=output_file, mode='wt', compresslevel=9)

Here it's clear that output_file is the file object, and not the name. 'wt' is, indeed, the mode. And 9 is the compression level.

This is also a prime example of the problems using numeric constant.  What is compression level 9? Is it 9/9, 9/10, or 9/100? Making things like this named constants removes a lot of ambiguity and misdirection.

This guide was first published on Aug 08, 2018. It was last updated on Aug 08, 2018. This page (Function Basics) was last updated on Nov 17, 2019.