Typically, expressions in Python are evaluated completely (also called eager evaluation), but if you look at the table above you might notice that this isn't strictly necessary for the boolean connectives. For example, the expression
>>> 1 == 2 and 2 > 1
False
is false, because 1 == 2 evaluates to False. This means that there is no need to evaluate the expression on the right, because no matter what it evaluates to, the result of the expression is already known. A similar phenomenon occurs with disjunction as well: in an expression x or y, if x evaluates to True, then we know that x or y will evaluate to True without having to evaluate y.
Python will take advantage of this property to avoid evaluating every expression in a boolean expression. If an operand in a conjunction evaluates to False or an operand in a disjunction evaluates to True, Python will immediately evaluate the expression without evaluating the rest of the operands in the expression. This behaviour is called short-circuiting. This makes it worthwhile to consider the order of the operands in a boolean expression.
Since we don't tell Python the type of a variable, Python must figure it out, or infer the type of the variable. Since the value of a variable can change over the course of the execution of a program, it can be the case that the type of the value that gets assigned changes, thereby changing the type of the variable as well.
The type function can tell us the type of a value or variable.
>>> a = 17
>>>type(a)
int
>>> a = "good"
>>>type(a)
str
Values (and by extension variables) have a single type. But what happens when we need to combine two values of different types? For example, consider the following scenario, where we would like a string that is dependent on some numeric value.
>>> n = 40
>>> "I have " + n + " cakes."
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str
We get an error here because Python is unable to infer the type of the variable s. This can be frustrating because it seems very obvious to us that we would like to construct a string using the value n. However, from the perspective of the computer, it is not clear that this is the right inference to make. For example, maybe we actually wanted to turn the surrounding strings into integers and add the terms together.
So in cases like this, it is necessary to explicitly tell Python that we would like to treat a value as a particular type. This is called casting. To cast a value, as a type, we use the type name:
>>> n = 40
>>> "I have " + str(n) + " cakes."
'I have 40 cakes.'
One should be careful when attempting to cast values. It is possible that there are some values that can't be converted to another type. For instance, int("14") gives us what we might expect, but int("hat") will fail. Or, for a more insidious example, consider int("3.0") or int(3.14).
Along these lines, it's important to recognize that even though two values may appear to have the same value to us, they are considered to be different values. Why is this? Recall that all of these values have some representation. So the integer 5 has a different representation than the string "5" or even the float 5.0.
If you've taken an early look at your homework, you'll notice that all of the problems follow a similar structure: given some input, produce some output. What's being described here is a function, in the mathematical sense.
Formally speaking, a function is just a set of input-output pairs. That's it. We typically think of functions as being things that take input and produce outputs, because we often need to do this to work with it. But there is nothing that says that a function is something that's computed. In fact, there are some functions that can't be computed.
This is a useful distinction because it tells us that problems align with the mathematical notion of a function and that the solution, the program that we write, is the thing that computes the function, thereby solving it.
For example, $\times$ is technically a function mathematically: it maps inputs (two numbers) to an output (another number). We write $12 \times 12 = 144$, but we could write something like $\operatorname{mul}(12, 12) = 144$. But this doesn't describe how we might compute this. There is an algorithm that we can use, the grade school multiplication algorithm. But this algorithm is not the function itself—it's only one way to compute the function.
A function in the context of programming is really this second notion of a function: a program that implements an algorithm that computes the mathematical notion of the function we're trying to implement. Of course, if we can write a program for such a function, there's really no point in distinguishing these from non-computable functions, so we everyone just calls these functions.
Beyond mathematics, the practical purpose of functions is organization and reuse. Once our programs start getting to a certain level of complexity, it becomes difficult to think of programs as just one complicated expression. Just as we did with variables, we can abstract functions and give names, which allows us to reuse them. So while variables allows us to abstract and organize pieces of data, functions can be thought of as an abstraction and organization of computation—in order to do anything interesting, we have to apply some sort of function. In this sense, functions act as a basic unit of computation and we can think of functions as small programs.
This means that we want to put a bit more care in how we define functions. To do this, we'll walk through some steps that will help to guide your problem solving process and writing a complete, documented function.
We'll go through this process with the intent of writing a function to determine whether a given integer is a leap year.
First, we should imagine some examples of what we want our function to do. Here, you might wonder why we're not thinking about the definition of the problem or the function, but coming up with examples is a good way of making your understanding of the problem concrete.
For instance, let's recall the idea of a leap year. In most years, February has 28 days. But once in a while, it has 29 days—these years are called leap years. If we open up a calendar app, we find that this year, 2025, is not a leap year. We may remember that last year, 2024 was.
Doing enough of this should give us an idea of some patterns we might wish to exploit in order to solve the problem. We will find that a year is a leap year if the year is divisible by 4, like 2020 and 2024.
Now, a function takes input and produces output. So what should our inputs and outputs be? While Python doesn't require us to explicitly include this, it's still very important to think about this. Since Python doesn't check or enforce any type restrictions, this means it is up to the programmers working with the function to ensure it is being used correctly. This is why we'll also be including this information in our documentation of the function.
To see why thinking about types is important, we can go back to math. While we may not think of math as having types, we recall that we talk about functions in the following way: \[f : \mathbb R \times \mathbb R \to \mathbb R\] This is kind of like information about types. This says that our function $f$ takes two real numbers and produces a real number. Formally, we say that $\mathbb R \times \mathbb R$ is the domain of $f$ and $\mathbb R$ is the co-domain or range is of $f$.
If we think about what our leap year checking function should do, taking integers as inputs seems to be the correct choice. And since our function is essentially answering a yes or no question, the output of our function should be a boolean. This means we can envision a function that looks like \[f : \mathbb Z \to \{\text{True}, \text{False}\}.\]
Once we're satisfied with this, we can name our function and its parameters. We should give nice descriptive names for things whenever we expect to use them in many contexts. A good name for this particular function is is_leap_year. But what about the input? That's not quite as important, since the varaible will only live inside the function—we'll talk a bit more about this in detail later. But for now, year seems like a perfectly reasonable name.
def is_leap_year(year):
We call the variable that's the placeholder for the input a parameter.
At this point, we should have a relatively clear idea of what we'd like our function to do, after having thought through examples and what its inputs and outputs should be. Here, we want to write a description of what our function does and include it with the definition of our function.
You might wonder what the point of this is. After all, the code describes the solution to the problem. And this is true, the program is indeed a description of the solution to the problem you're solving. The issue is that while the code will explain literally what it does, it is not very helpful for determining whether the code is doing what it was intended to do.
In other words, we need to document intent. We often forget that our programs are not only intended for the computer. If the program describes a solution, then it's natural that other people will be interested in it. And so we are not communciating only with the computer, but with other people as well. Even in this course, it may seem like no one else is reading your programs, but at the very least, the graders will be. And there is also the reader we never expect to get frustrated: ourselves in a couple of weeks.
What would a possible description of our function look like? This function is fairly straightforward: it figures out whether a number is prime or not. So we might write something like
Determines whether a number is a leap year.
But we can be a bit more specific. We know a few things already, like what our inputs and outputs should be.
Determines whether a given year, as an integer, is a leap year.
But wait, what's a leap year? If your audience knows what a leap year is, that's fine. But if not, it's worth describing it a bit more.
Determines whether a given year, as an integer, is a leap year. A year is a leap year if the year is divisible by 4.
We will include our plain-English description of the function in the code. Every programming language has a facility for including notes intended for humans to read that are ignored by computers. These are called comments.
In Python, there are two ways to do this. Ordinary comments begin with #.
great_value = 3193 # this value is very great
Anything that follows a hash is ignored. This is useful for leaving comments about points of interest in your code.
However, a second type of comment exists in Python specifically for documenting functions. These are called docstrings and are denoted by three quotes, """. The docstring should include your plain language description of the function as well as its expected inputs and output.
"""
Determines whether a given year, as an integer, is a leap year.
A year is a leap year if the year is divisible by 4.
Input:
year (int): a year (A.D.)
Output (bool): True if year is a leap year and False otherwise
"""
Notice that the description of the function and its expected inputs and output give us a fairly clear picture of the intent of the function and the assumptions that are being made (for instance, that we really only consider years in A.D.). One more helpful thing that you may consider doing is adding concrete examples.
"""
Determines whether a given year, as an integer, is a leap year.
A year is a leap year if the year is divisible by 4.
Input:
year (int): a year (A.D.)
Output (bool): True if year is a leap year and False otherwise
Examples:
>>> is_leap_year(2024)
True
>>> is_leap_year(2025)
False
"""
The examples here are given in the style of "interactive examples" that mimic a python3 interactive session.
The docstring for the function should go immediately beneath the function header, indented, and before the body of the function definition.
def is_leap_year(year):
"""
Determines whether a given year, as an integer, is a leap year.
A year is a leap year if the year is divisible by 4.
Input:
year (int): a year (A.D.)
Output (bool): True if year is a leap year and False otherwise
Examples:
>>> is_leap_year(2024)
True
>>> is_leap_year(2025)
False
"""
The reason for some of these conventions in writing docstrings is that these are what is used by Python for documentation.
>>> help(is_leap_year)
Help on function is_leap_year in module __main__:
is_leap_year(year)
Determines whether a given year, as an integer, is a leap year.
A year is a leap year if the year is divisible by 4.
Input:
year (int): a year
Output (bool): True if year is a leap year and False otherwise
Examples:
>>> is_leap_year(2024)
True
>>> is_leap_year(2025)
False
This is actually just the body of the function, which consists of the expression(s) you wrote. The real work of this part is solving the problem and writing the code for it. But notice how the groundwork that we did in the previous steps clarifies what a possible solution in code might look like—it's important (and helpful!) to think through the problem carefully before sitting down at a keyboard.
Here is a template for function definition.
def function_name(parameter_1, parameter_2, ...):
"""
Description of the function.
Inputs:
List of inputs and their descriptions and types
Output: description of the output (and its type)
"""
<statements>
...
return <expression>
Notice that the series of statements (also called a block) that comprise the body of the function are indented. We see now why the docstring needs to be indented: to match the indentation level of the body of the function.
In Python, blocks of statements are denoted only by indentation. This is very important! This means that the first thing that is not indented relative to the function header is considered "outside" of the body of the function—because there is no other denotation, Python is particularly sensitive about indentation.
In principle, any amount of indentation is acceptable, as long as the block is indented consistently. However, in practice, the Python style guide calls for four (4) spaces (not tabs) for an indentation level and this is almost universally adhered to.
Our function is relatively simple, so we it consists only of a return statement. When executed, return <expr> ends execution of the function and produces the value that expr evaluates to.
def is_leap_year(year):
"""
Determines whether a given year, as an integer, is a leap year.
A year is a leap year if the year is divisible by 4.
Input:
year (int): a year (A.D.)
Output (bool): True if year is a leap year and False otherwise
Examples:
>>> is_leap_year(2024)
True
>>> is_leap_year(2025)
False
"""
return year % 4 == 0
In summary, a function definition looks like the following:
def followed by the name of the function to begin the definition of the function. The name of the function is how we refer to it.
return statement. This value can be any expression (i.e. literal, variable name, or expression).
The final piece of function design is testing. Why do we need tests? A common question that many beginners have is "How do I know my code is correct?" In fact, this is a tricky problem that even seasoned engineers will struggle with.
As a theoretician, my answer would be that it's simple, just provide a mathematical proof! Surprisingly, it turns out most people do not want to do this. The next best thing is to provide tests as evidence that your program works.
While there are formal testing facilities (which we make use of to grade your work), it is also a good idea to do informal testing to ensure that your work is correct. One underrated method is to actually run your program after making a change to make sure it still works.
Once we have defined a function, we can call it like so:
>>> function_name(a1, a2, ...)
This looks similar to how we would write $f(x,y)$ in math. So we can call our function as follows, and examine the results.
>>> is_leap_year(2025)
False
>>> is_leap_year(2024)
True
In a way, this is a callback to thinking through the problem with examples—this should have given you a good idea of what values to use in your testing and what the expected output should be. Notice that I've included the examples in the docstring in a specific format. This allows for a bit of automated testing. Suppose that I place our function in file leap.py. Then we can run the following.
$ python3 -m doctest -v leap.py
Trying:
is_leap_year(2024)
Expecting:
True
ok
Trying:
is_leap_year(2025)
Expecting:
False
ok
1 items had no tests:
leap
1 items passed all tests:
2 tests in leap.is_leap_year
2 tests in 2 items.
2 passed and 0 failed.
Test passed.
This automatically reads the docstring, finds the examples, and uses them to test the function. We see that our code corresponds with our examples. That is, our code at the very least matches our stated intent and understanding of the problem.
However, you'll want to be sure to be more thorough about the cases you consider here. In fact, we'll find that we do have an error in our program...