Python provides many built-in top-level functions and methods for us, but as we start writing more code, it is essential for us to be able to create our own functions specific to the problem we are solving. In this section, we’ll learn how to define our own top-level functions in Python. And later on, we’ll study how to define our own data types and methods as well.
First, let’s recall how we define a function in mathematics. We first specify the function name, domain, and codomain: for example, \(f: \R \to \R\). Then, we write the function header and body, usually in a single line: for example, \(f(x) = x^2\). We do this so often in mathematics that we often take parts of this for granted, for example leaving out the domain/codomain specification, and usually choosing \(f\) as the function name and \(x\) as the parameter name. However, the functions we’ll implement in Python are much more diverse, and so it will be important to be explicit in every part of this process.
Here is the complete definition of a “squaring” function in Python. Take a moment to read through the whole definition, and then continue reading to learn about this definition’s different parts.
def square(x: float) -> float:
"""Return x squared.
>>> square(3.0)
9.0
>>> square(2.5)
6.25
"""
return x ** 2This function definition is the most complex form of Python code we’ve seen so far, so let’s break this down part by part.
The first line, def square(x: float) -> float: is called the function header. Its purpose is to convey the following pieces of information:
square).x and type float.->, float. In this example, the function’s parameter and return type are the same, but this won’t always be the case. The syntax for a function header for a unary function is:
Compared to our mathematical version, there are two main differences. First, we chose the name square rather than f as the function name; in Python, we will always pick descriptive names for our functions rather than relying on the conventional “\(f\)”. And second, we use data types to specify the function domain and codomain: the code x: float specifies that the parameter x must be a float value, and the code -> float specifies that this function always returns a float value.
We can express this restriction in an analogous way to \(f: \R \to \R\) by writing float -> float; we call float -> float the type contract of the square function.
The next seven lines, which start and end with triple-quotes ("""), is called the function docstring. This is another way of writing a comment in Python: text that is meant to be read by humans, but not executed as Python code. The goal of the function docstring is to communicate what the function does.
The first part of the docstring, Return x squared., is an English description of the function. The second part might look a bit funny at first, since it seems like Python code:Or more precisely, it looks like the Python console!
This part of the docstring shows example uses of the function, just like the examples we showed of built-in functions in the previous section. You can read the first example literally as “when you type square(3.0) into the Python console, 9.0 is returned” and the second as “when you type square(2.5) into the Python console, 6.25 is returned”. These examples are called doctest examples, for a reason we’ll see in a future section. While a English description may technically be enough to specify the function’s behaviour, doctest examples are invaluable for aiding understanding of the function behaviour (which is why we use them in teaching as well!).
The function docstring is indented inside the function header, as a visual indicator that it is part of the overall function definition. Unlike many other programming languages, this kind of indentation in Python is mandatory rather than merely recommended. Python’s designers felt strongly enough about indentation improving readability of Python programs that they put indentation requirements like this into the language itself.
The final line, return x ** 2, is called the body of the function, and is the code that is executed when the function is called. Like the function docstring, the function body is also indented so that it is “inside” the function definition.
This code uses another keyword, return, which signals a new kind of statement: the return statement, which has the form:
When a return statement is executed, the following happens:
<expression> is evaluated, producing a value.In the previous section, we called built-in functions, and took for granted that they worked properly, without worrying about how they work. Now that we’re able to define our own functions, we are ready to fully understand what happens when a function is called.
As an example, suppose we’ve defined square as above, and then call it in the Python console:
When we press Enter, the Python interpreter evaluates the function call by doing the following:
2.5, and then assign 2.5 to the function parameter x.square function, by doing:
x ** 2, which is 6.25 (since x refers to the value 2.5).6.25 back to the Python console.square(2.5) evaluates to 6.25, and this is displayed on the screen.As we observed in the previous section, we can combine multiple function calls within a single expression. What happens when we call square twice in the same expression? For example:
We can step through this as well; notice how we’ve duplicated the text from before to illustrate the similarities between calling square(2.5) and -square(-1.0).
+ in left-to-right order, so evaluate square(2.5) first.
2.5, and then assign 2.5 to the function parameter x.square function, by doing:
x ** 2, which is 6.25 (since x refers to 2.5).6.25 back to the Python console.square(-1.0) to be evaluated.
-1.0, and then assign -1.0 to the function parameter x.square function, by doing:
x ** 2, which is 1.0 (since x refers to -1.0).1.0 back to the Python console.6.25 + 1.0, which evaluates to 7.25. This value is displayed on the screen.While it is possible to define functions directly in the Python console, this isn’t a good approach: every time we restart the Python console, we lose all our previous definitions. So instead, we save functions in files so that we can reuse them across multiple sessions in the Python console (and in other files).
For example, suppose we have the following file called my_functions.py:
def square(x: float) -> float:
"""Return x squared.
>>> square(3.0)
9.0
>>> square(2.5)
6.25
"""
return x ** 2In PyCharm, we can right-click and select “Run File in Python Console”. This will start the Python console and run our file, which then allows us to call our function square just like any built-in function:
Let’s now look at a more complex example that will illustrate a function definition that takes in more than one parameter.
Recall the distance formula from Section 1.4 to calculate the distance between two points \((x_1, y_1), (x_2, y_2)\) in the Cartesian plane: \[d = \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}\]
We’ll now write a function in Python that calculates this formula. This function will take two inputs, where each input is a tuple of two floats, representing the \(x\)- and \(y\)-coordinates of each point. When we define a function with multiple parameters, we write the name and type of each parameter using the same format we saw earlier, with parameters separated by commas from each other. Here is the function header and docstring:
def calculate_distance(p1: tuple, p2: tuple) -> float:
"""Return the distance between points p1 and p2.
p1 and p2 are tuples of the form (x, y), where the x- and y-coordinates are points.
>>> calculate_distance((0, 0), (3.0, 4.0))
5.0
"""In order to use the above formula, we need to extract the coordinates from each point. This is a good reminder of tuple indexing, and the fact that function bodies can consist of more than one statement. Remember: the function body’s statements are executed one at a time until a return statement is executed.
Now that we have the four coordinates, we can apply the above formula and return the result
Putting this all together, we have:
def calculate_distance(p1: tuple, p2: tuple) -> float:
"""Return the distance between points p1 and p2.
p1 and p2 are tuples of the form (x, y), where the x- and y-coordinates are points.
>>> calculate_distance((0, 0), (3.0, 4.0))
5.0
"""
x1 = p1[0]
y1 = p1[1]
x2 = p2[0]
y2 = p2[1]
return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5Our above function body is perfectly correct, but you might notice that the ** 2 expressions exactly mimic the body of the first function we defined in this section: square. And so we can reuse the square function inside the body of calculate_distance:
def calculate_distance(p1: tuple, p2: tuple) -> float:
"""Return the distance between points p1 and p2.
p1 and p2 are tuples of the form (x, y), where the x- and y-coordinates are points.
>>> calculate_distance((0, 0), (3.0, 4.0))
5.0
"""
x1 = p1[0]
y1 = p1[1]
x2 = p2[0]
y2 = p2[1]
return (square(x1 - x2) + square(y1 - y2)) ** 0.5This example of function reuse is quite small, but as our programs grow larger, it will be essential to organize our code into different functions. We’ll explore this idea in more detail, and other principles of good function and program design, throughout this course.