Introduction
By default, a computer program executes the instructions in the order
in which they are provided, line by line, and stops once the last
instruction has been executed.
# file: main.py
print("Hey!")
print("Ho!")
print("Let's go!")
$ python main.py
Hey!
Ho!
Let's go!
However, branching allows you to control this execution flow when needed.
In Python, branching constructs include:
- Conditional execution
- Loops
- Function calls
- Exceptions
Conditional execution
Booleans
The boolean type bool can take two values: False and True.
Logic
The logical operators “not” (¬), “and” (∧) and “or” (∨)
are denoted in Python by the keywords not, and and or,
and evaluate as follows:
| Symbol | Operator | Expression | Value |
|---|
| ¬ | not | not False | True |
| ¬ | not | not True | False |
| ∧ | and | False and False | False |
| ∧ | and | False and True | False |
| ∧ | and | True and False | False |
| ∧ | and | True and True | True |
| ∨ | or | False or False | False |
| ∨ | or | False or True | True |
| ∨ | or | True or False | True |
| ∨ | or | True or True | True |
Comparison
Python objects can be compared using the operators == (equal)
and != (not equal).
>>> 0 == 0
True
>>> 0 == 1
False
>>> "A" == "A"
True
>>> "A" == "B"
False
>>> [1, 2, 3] == [1, 2, 3]
True
>>> [1, 2, 3] == [4, 5, 6]
False
If the objects are of an ordered type (e.g., integers, floating-point
numbers, etc.), you can also use < (strictly less than),
<= (less than or equal to), > (strictly greater than) and >= (greater
than or equal to).
The ordering depends on the type of the object; for example, for strings,
lexicographic order is used:
>>> "ABC" < "XYZ"
True
Membership
The operators in and not in allow you to test whether an object
belongs to a container (a list, a string, a set, etc.):
>>> 1 in [1, 2, 3]
True
>>> 0 in [1, 2, 3]
False
>>> 1 not in [1, 2, 3]
False
>>> 0 not in [1, 2, 3]
True
>>> 1 in set([1, 2, 3])
True
>>> 0 in set()
False
>>> "Hello" in "Hello world!"
True
>>> "x" in {"x": 0.0, "y": 1.0}
True
Equality and Identity
The equality of x and y is tested with the operator ==:
x == y
Their identity is tested with the keyword is:
x is y
The negation of these properties is tested with != and is not:
x != y
x is not y
The identity x is y means that the variables x and y refer to the
same Python object: the data is at the same address in memory. A perfect
copy of an object will therefore have a different identity from the
original, while it will be considered equal to the original. However, if two
objects are identical (in the sense of: have the same identity, are a single
unique object), then they are necessarily equal.
As an example, let’s consider the three lists a, b and c:
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> c = b
The lists a and b are equal, as are b and c, but they are not
identical—they do not designate the same object (in memory);
the variables b and c, on the other hand, designate the same object:
>>> a == b
True
>>> b == c
True
>>> a is b
False
>>> b is c
True
You can verify that b and c designate the same object by evaluating
the identifier of these objects (an integer) with the function id:
>>> id(a)
140636096399680
>>> id(b)
140636098130688
>>> id(c)
140636098130688
>>> id(a) == id(b)
False
>>> id(b) == id(c)
True
One important consequence of this distinction: modifications to the list
(designated by) b will affect the list c (which is the same object), but
not the list a (which is a distinct object):
>>> b.append(4)
>>> b
[1, 2, 3, 4]
>>> c
[1, 2, 3, 4]
>>> a
[1, 2, 3]
Priorities
📖 Python Language Reference / Expressions / Operator Precedence
Some boolean expressions may seem ambiguous; you could a priori
interpret the expression not x and y or z in multiple ways,
for example as (not x) and (y or z) or as not (x and (y or z)).
To resolve this ambiguity, Python defines a precedence list between
expressions; from highest to lowest precedence, we have:
- Function call
in, not in, is, is not, <, <=, >, >=, !=, ==
not
and
or
The expression not x and y or z is therefore interpreted as
((not x) and y) or z.
Explicit Conversions
🏷️ Boolean Value
We say that a value x is:
- false-ish if
bool(x) is False,
- true-ish if
bool(x) is True.
The conversion table above applies to the most common types:
type(x) | bool(x) is False | bool(x) is True |
|---|
type(None) | Always | Never |
bool | x == False | x != True |
int | x == 0 | x != 0 |
float | x == 0.0 | x != 0.0 |
complex | x == 0.0j | x != 0.0j |
str | x == "" | x != "" |
bytes | x == b"" | x != b"" |
tuple | x == () | x != () |
list | x == [] | x != [] |
set | x == set() | x != set() |
dict | x == {} | x != {} |
If
The keyword if and the associated keywords elif and else allow
conditional execution of code.
if condition_1:
... # block 1
elif condition_2:
... # block 2
elif condition_3:
... # block 3
...
else:
... # block n
The elif and else clauses are optional. The keyword elif should be
understood as a shortcut for else if: the code above is equivalent to:
if condition_1:
... # block 1
else:
if condition_2:
... # block 2
else:
if condition_3:
... # block 3
...
else:
... # block n
The expressions condition_* are implicitly converted to booleans and
determine what the execution flow will be.
Loops
While
The while loop executes as long as its condition is (false-ish:
>>> numbers = [1, 2, 3]
>>> while numbers:
... number = numbers.pop(0)
... print(number)
...
1
2
3
For
The for loop allows you to iterate over all elements of a collection:
>>> numbers = [1, 2, 3]
>>> for number in numbers:
... print(number)
...
1
2
3
Refer to the section Iteration and Comprehension for more details.
Exiting Loops
The execution of while and for loops can be interrupted, either to
go directly to the next iteration, with the keyword continue,
or to terminate the loop entirely, with the keyword break.
For example:
>>> i = -1
>>> while i < 6:
... i += 1
... if i % 2 == 0: # i is even
... continue
... print(i)
...
1
3
5
and
>>> i = 0
>>> while True:
... if i >= 3:
... break
... print(i)
... i += 1
...
0
1
2
The same mechanism applies to for loops. Note the optional else clause
associated with for loops, which is only executed if no break
occurred.
>>> for i in [1, 2, 3]:
... print(i)
... else:
... print("ok")
...
1
2
3
ok
Function calls
When a function is called, its code is executed, and then execution resumes
the normal execution flow.
>>> def print_ho():
... print("Ho!")
...
>>> print("Hey!")
>>> print_ho()
>>> print("Let's go!")
Hey!
Ho!
Let's go!
This principle applies recursively (a function can call functions,
which can themselves call functions, etc.)
Exceptions
Errors
In case of an invalid operation, Python raises an error; for example if you
divide $1$ by $0$, compute $\sqrt-1$ or evaluate the absolute value
of an empty list, you will observe the following messages:
>>> 1.0 / 0.0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: float division by zero
>>> import math
>>> math.sqrt(-1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: math domain error
>>> abs([])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'list'
Technically, Python raises an error by raising an exception.
In interactive mode, the exception manifests as follows:
-
a traceback pointing to the origin of the error:
File "<stdin>", line 1, in <module>
(here rather uninformative, we must admit.)
-
an exception type:
-
ZeroDivisionError,
-
ValueError and
-
TypeError.
-
an explanatory message:
Consequences of an Exception
When an exception is raised in interactive mode (Python REPL, Jupyter
notebook, etc.), the environment handles the exception;
it signals that an exception occurred, but you can continue to
type commands.
However, in the classic execution of a Python program,
in the absence of specific exception handling, the occurrence of an
exception abruptly halts the program. For example, executing the Python
program
# file: main.py
print("Hello")
1 / 0
print("world!")
leads to the following output
$ python main.py
Hello
Traceback (most recent call last):
File "main.py", line 3, in <module>
1 / 0
ZeroDivisionError: division by zero
In any case, the string "world!" will never be printed. The classic linear
execution flow is thus disrupted by the exception.
Exception Handling
Exceptions that occur can be caught and handled as you wish
before they cause the program to stop.
For example:
>>> def ratio(numerator, denominator):
... try:
... return numerator / denominator
... except ZeroDivisionError as error:
... message = "denominator is equal to zero."
... raise ValueError(message) from error
...
>>> ratio(640, 480)
1.3333333333333333
>>> ratio(640, 0)
Traceback (most recent call last):
File "<stdin>", line 3, in ratio
ZeroDivisionError: division by zero
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in ratio
ValueError: denominator is equal to zero.
>>> import math
>>> x = 4
>>> try:
... r = math.sqrt(x)
... except ValueError:
... r = math.sqrt(-x)
...
>>> r
2.0
>>> xs = [0.0, 1.0, -1.0, "Hello!", [], 1.0j, None, 42]
>>> abs_xs = []
>>> for x in xs:
... try:
... abs_xs.append(abs(x))
... except TypeError:
... abs_xs.append(None)
...
>>> abs_xs
[0.0, 1.0, 1.0, None, None, 1.0, None, 42]
Raising an Exception
You can raise an exception using the keyword raise.
Simulation
For example, to reproduce the errors we have encountered so far:
>>> # 1 / 0
>>> raise ZeroDivisionError("float division by zero")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: float division by zero
>>> # math.sqrt(-1)
>>> raise ValueError("math domain error")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: math domain error
>>> # abs([])
>>> raise TypeError("bad operand type for abs(): 'list'")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: bad operand type for abs(): 'list'
A More Realistic Example
You can quickly define a factorial function:
import math
def factorial(n):
integers = range(1, n+1) # 1, 2, 3, ..., n (iterable)
return math.prod(integers)
which gives the correct result “when all goes well”
>>> factorial(0)
1
>>> factorial(1)
1
>>> factorial(2)
2
>>> factorial(3)
6
>>> factorial(10)
3628800
>>> factorial(20)
2432902008176640000
But in case of an error with the argument type, the associated error is somewhat
cryptic:
>>> factorial("100")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in factorial
TypeError: can only concatenate str (not "int") to str
It is possible to do better, for example with the following code:
def factorial(n):
if not isinstance(n, int):
message = f"{n!r} is not an integer"
raise TypeError(message)
integers = range(1, n+1) # 1, 2, 3, ..., n (iterable)
return math.prod(integers)
You will then get the more explicit error
>>> factorial("100")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in factorial
TypeError: '100' is not an integer
But even then we can still have surprises. Thus, if n is an integer but it is
strictly negative, factorial will evaluate math.prod([]), which equals 1.
>>> factorial(-1)
1
Let’s fix this flaw in our implementation!
def factorial(n):
if not isinstance(n, int):
message = f"{n!r} is not an integer."
raise TypeError(message)
if n < 0:
message = f"{n} < 0."
raise ValueError(message)
integers = range(1, n+1) # 1, 2, 3, ..., n (iterable)
return math.prod(integers)
>>> factorial(-1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in factorial
ValueError: -1 < 0.
Exceptions & Flow Control
It may not be obvious that exceptions — ostensibly intended to
describe errors — provide a powerful flow control mechanism.
And yet it is the case!
As an example, let’s show how an exception allows us to exit
from multiple nested loops (unlike the keyword break).
We can thus use (for example) the exception StopIteration
in the following code:
>>> try:
... for i in range(10):
... for j in range(10):
... for k in range(10):
... print(i, j, k)
... if i + j + k == 7:
... raise StopIteration()
... except StopIteration:
... pass
...
0 0 0
0 0 1
0 0 2
0 0 3
0 0 4
0 0 5
0 0 6
0 0 7
Note that this usage is not as exotic as it might seem.
Indeed, Python itself uses (implicitly) the exception StopIteration
in for loops to signal the exhaustion of an iterable.
Thus the code
for i in [1, 2, 3]:
print(i)
is (schematically) equivalent to:
it = iter([1, 2, 3])
while True:
try:
i = next(it)
print(i)
except StopIteration:
break