Python functions#

Python has a number of built-in functions for things like math calculations, working with the filesystem, and reading in common file formats. First, we’ll talk about how to use modules to access more functions. Then, we’ll see how we can define our own custom functions to do whatever we want.

Modules#

In addition to the basic Python syntax and commands, more tools can be accessed using modules. Modules must be imported into your current namespace using the import statement.

For example, the math module has additional math functions that go beyond just simple arithmetic. We have to first import it to use it.

import math
memory_delay_days = 3.3
memory_delay_ceil = math.ceil(memory_delay_days)
print(memory_delay_ceil)
4

Use the help function to see more information about the math module.

There are multiple ways to import things from modules that give you some flexibility. You can import a specific function or functions from a module.

from math import ceil, floor
print(floor(memory_delay_days))
3

Usually it’s best to just import the module and then call functions using dot syntax, like math.ceil. That way, it’s easy to tell where a given function came from.

You can give a module a different name when importing it. This is often a good idea when the module name is long.

import math as mt
print(mt.exp(.5))
1.6487212707001282

There are conventions for short names to use for some modules. For the math module, most people don’t rename it, because it’s already pretty short. Later on, we’ll see how people usually import the number module using import numpy as np to make commands easier to type, like np.mean.

Exercise: modules#

Use the math module to calculate \(cos(\pi)\). Run help(math) to see a summary of the functions and variables available. See the DATA section at the end, which defines some standard mathematical constants.

# answer here

Functions#

A key method for running programs involves defining a function that runs some code to complete some purpose. This function can then be used whenever you need to complete that task.

output1, output2, ... = function_name(input1, input2, ...)

Functions help you follow the DRY principle: Don’t Repeat Yourself. Beginners often do a lot of copying and pasting code. But this can lead to programs that are harder to read, debug, and improve, because they have a lot of redundancy.

We can make a new function using the def keyword.

def function_name(input1, input2):
    # code to calculate some output
    return output1, output2

Usually, functions have two parts: inputs (also known as arguments) and outputs. The return statement determines the output(s) of the function.

Let’s make a really simple function that takes in a number and doubles it.

def double(x):
    return x * 2

Now that we’ve defined this function, we can call it the same as any built-in Python function.

x = 2
y = double(x)
print(y)
4

The x in the definition of the double function is just a name that’s used in the function. When we’re calling the function, we can name the variable whatever we want…

z = 10
print(double(z))
20

Or not give it a name at all and just input a number directly into the function without assigning it to a variable first.

print(double(9000))
18000

Variables defined a function can only be accessed in the function. A function has a scope where variables are defined locally. They’re temporary variables used when executing the function. After the function runs, they don’t exist anymore.

def test_variables(a):
    b = a * 2
    c = b ** 2
    return c + 7

a = 2
d = test_variables(a)
print(d)
# print(c) throws an error; c is not defined out here (try it!)
23

A function can include a special string known as a docstring, which comes right at the start of the function definition. This is usually specified using triple quotes, like """This is a docstring.""" The triple quotes allow you to split a string over multiple lines, so you can explain what the inputs and outputs are. But often a simple function will just have one line saying what it does.

def print_date():
    """Display the current date."""
    from datetime import date
    print(date.today())

print_date()
2025-05-10

The first line of a docstring should be written as an imperative sentence. That is, say what the function will do. Good: Display the current date.. Bad: This function displays the current date.. Notice that the second version takes more words to say the same thing.

We can see a function’s docstring by using help, or by typing function_name? in Jupyter.

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

print_date()
    Display the current date.
print_date?

We can use functions to repeat things we need to do multiple times. Say that we have data for multiple participants, and we want to be able to print a summary for a given participant.

# data for each participant
data1 = {"id": "001", "accuracy": 0.82, "response_time": 4.2}
data2 = {"id": "002", "accuracy": 0.53, "response_time": 3.2}

We can use a function to make this convenient. Instead of having to copy and paste the printing code whenever we want to use it, we just call the function.

def summarize(data):
    """Summarize participant data."""
    percent = round(data["accuracy"] * 100)
    print(f"sub-{data["id"]}: {percent}%, {data["response_time"]}s")

summarize(data1)
summarize(data2)
sub-001: 82%, 4.2s
sub-002: 53%, 3.2s

Exercise: functions#

Create a function called square that will take in some variable \(x\) and calculate \(x^2\). In Python, you can square a variable using x ** 2. Use the function to calculate the squares of 2, 3, and 4.

Create another function called add_three that takes in three variables \(x\), \(y\), and \(z\) and returns \(x + y + z\). Use it to add 2, 3, and 4 together.

# answer here

Multiple inputs and outputs#

Functions can take in multiple inputs and return multiple outputs. Both inputs and outputs are stored in tuples, which we covered briefly before.

def return_two_things():
    """Return two variables in a tuple."""
    return 1, 2

t = return_two_things()
print(t)
print(type(t))
(1, 2)
<class 'tuple'>

Usually, we’ll want to assign multiple outputs to different variables. We can do this using a feature called tuple unpacking.

a, b = return_two_things()
print(a)
print(b)
1
2

This is mostly used when calling a function, but it works with any tuple. Sometimes people will use it to define multiple variables in one line.

a, b, c = 1, 2, 3
print(a)
print(b)
print(c)
1
2
3

Let’s look at another example with multiple inputs and multiple outputs.

def double_double(x, y):
    """Multiply two numbers by two."""
    return x * 2, y * 2

If we call this function, we’ll get two values back.

x = 2
y = 4
a, b = double_double(x, y)
print(a, b)
4 8

Usually, when we define a function, there will be a fixed number of inputs. Sometimes, it’s helpful to allow the user to have any number of inputs. The print function is an example of this; we can print multiple objects by passing them all into a call.

print(1, 2, 3, 4, 5, 6)
1 2 3 4 5 6

We can make a function that takes in any number of inputs using the * syntax. Here, args is short for arguments, which is another name for function inputs.

def get_args(*args):
    return args
outputs = get_args(1, 2, 3)
print(outputs)
print(type(outputs))
(1, 2, 3)
<class 'tuple'>

By putting a * in front of an argument, like *variables (or whatever we want to call it), we’re indicating that the rest of the arguments should be placed into a tuple called variables.

We can use the * syntax to write a function that will work with any number of arguments.

def get_maximum(*args):
    """Get the maximum out of the inputs."""
    return max(args)

get_maximum(2, 6, 4)
6

Exercise: multiple inputs and outputs#

Write a function called two_powers that takes three variables, x, y, and z, and returns two values: \(x^y\) and \(x^z\). Test it with \(x=2\), \(y=3\), and \(z=4\).

Advanced#

Create a function called add_multiple that takes in a variable amount of numbers and adds them all together. The built-in function sum will sum together a list or tuple of numbers. Test your function with inputs 2, 3, 4.

# answer here

Function arguments#

There are two types of arguments to functions: positional arguments, like we’ve used so far; and keyword arguments, which have names attached to them. Keyword arguments make it so we can label arguments when we’re defining a function. This can sometimes make it easier to see what we’re trying to do when calling a function. It also means that we don’t have to specify all the inputs; we can leave some as their default values instead.

def myfunction(pos1, pos2, pos3, kw1="default", kw2=42):
    ...

In this example, there are three positional arguments, which are required, and two optional keyword arguments, which default to "default" and 42 if they are not specified.

Let’s use a keyword argument to set a default value for a function. We’ll multiply an input by 3 by default.

def mult_three_default(x, mult=3):
    return x * mult

print(mult_three_default(4))
12

We can also change the default value that we multiply by, in two ways. We can either specify it by just putting a value in the second position when calling the function:

print(mult_three_default(4, 100))
400

Or we can specify the keyword explicitly:

print(mult_three_default(4, mult=100))
400

If we have multiple keywords, we can just specify one, and the other one will use the default.

def prod(x=2, y=3):
    return x * y

print(prod())
print(prod(y=6))
6
12

Exercise: function arguments#

Define two variables: c = 1 and d = 2. Run print(c, d) and see what it prints. The print function has an option that determines how multiple variables are separated in the output. Run help(print) to see the documentation. Run print again, but this time set the sep keyword so that there is a comma between the arguments instead of a space.

# answer here

Variable numbers of keyword arguments#

Like how you can capture multiple positional inputs using a star, like *args, it is also possible to capture multiple keyword arguments using a double star, like **kwargs.

def keywords(**kwargs):
    return kwargs
kwargs = keywords(a=1, b=2, c=3)
print(kwargs)
{'a': 1, 'b': 2, 'c': 3}

Notice that, when you use the ** syntax, keyword arguments get placed in a dictionary.

Like *args, the use of name kwargs is just a common convention. You can call it whatever you want as long as you have ** in front of the variable name. Taking in a variable number of keyword arguments is sometimes useful to take in keyword arguments that will be passed to another function.

def add_then_print(x, y, **print_args):
    x = x + 1
    y = y + 1
    print(x, y, **print_args)

add_then_print(2, 4)
add_then_print(2, 4, sep=",")
3 5
3,5

By using the ** syntax again when calling print, we pass the keywords directly to it.

Summary#

Functions are very important in Python. They allow us to write a piece of code once and then reuse it over and over again.

A function with positional arguments and keyword arguments looks something like this:

def my_function(pos1, pos2, kw1=1, kw2=None):
    """Perform some function."""
    # do something with required arguments pos1 and pos2,
    # plus optional arguments kw1 and kw2.

We can also make functions that take in variable numbers of positional and keyword arguments, using * and ** syntax:

def variable_args(*pos, **kw):
    """Use variable numbers of inputs."""
    # do something with the tuple of position arguments pos,
    # and the dict of keyword arguments kw.