= [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers = [x**2 for x in numbers]
squares_list squares_list
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Python features are best leveraged when the code is written in a more “Pythonic” style. Pythonic code is concise, readable, and idiomatic, making it easier to understand and maintain.
Comprehensions are concise, readable, yet powerful constructs for creating and/or transforming Python structures in an iterative way. A comprehension consists of brackets containing an expression followed by a for
clause, then zero or more for
or if
clauses.
A list comprehension is a concise way to create a list. As an example, we can create a list with the squares of the numbers from 0 to 9 as follows:
= [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers = [x**2 for x in numbers]
squares_list squares_list
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
Analogously to list comprehensions, we can make a set
of the squares from 0 to 9 by using a set comprehension:
= [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers = {x**2 for x in numbers}
squares_set squares_set
{0, 1, 4, 9, 16, 25, 36, 49, 64, 81}
Similarly to the previous types of comprehensions, dictionary comprehensions are a more concise way to create dictionaries and iterate over them. Here we create a dictionary where the keys are numbers from 0 to 9 and the values are their squares:
= [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers = {x: x**2 for x in numbers}
squares_dict squares_dict
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25, 6: 36, 7: 49, 8: 64, 9: 81}
We can use the dict.items()
method to iterate over keys and values in a very idiomatic way - as opposed to extracting each value while iterating over the keys - and use comprehensions to transform either of the two:
= {"a": "aa", "b": "bb", "c": "cc"}
letters = {key.upper(): value for key, value in letters.items()}
upper_keys print(f"{upper_keys=}")
upper_keys={'A': 'aa', 'B': 'bb', 'C': 'cc'}
= {key: value.upper() for key, value in letters.items()}
upper_values print(f"{upper_values=}")
upper_values={'a': 'AA', 'b': 'BB', 'c': 'CC'}
Note how the above is much more idiomatic than e.g. :
= {key: letters[key].upper() for key in letters.keys()} upper_values_by_key
for
clausesComprehensions can be characterized by multiple for
clauses to perform nested iterations. In this subsection, we’ll look at lists, but comprehension like the following can be applied to other structures as well, like sets and dictionaries.
We apply multiple for
clauses to iterate over lists within a single comprehension. In doing so, it’s crucial to understand the looping order and scope, which follows that of the nested for
-loop clauses, namely from the outermost for
clause to the innermost one.
Let’s first look at a generic example with a list of lists:
= [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
nested_list for inner_list in nested_list for one_element in inner_list] [one_element
[1, 2, 3, 4, 5, 6, 7, 8, 9]
This is equivalent to the following non-Pythonic implementation:
= [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
nested_list = []
flattened_list for inner_list in nested_list:
for one_element in inner_list:
flattened_list.append(one_element)
Note that you can also use “nested comprehensions” to e.g. create nested lists:
= [[x**y for x in range(3)] for y in range(3)]
nested_list nested_list
[[1, 1, 1], [0, 1, 2], [0, 1, 4]]
Let’s now look at an example with a 3x3 matrix-like structure, also represented as a list of lists (although in real case scenarios, matrices as mathematical objects are better implemented with dedicated libraries):
= [
matrix 1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[ ]
Similarly here below, we use multiple for
to created a list from a list of lists. When dealing with matrices, this corresponds to the so-called flattening operation. Here, we consider each nested list as a row of the matrix:
= [num for row in matrix for num in row]
flattened flattened
[1, 2, 3, 4, 5, 6, 7, 8, 9]
What if we want to allow only certains elements to be included in the list based on a certain condition? We can keep the Pythonic syntax and add an if
clause. We show the latter by creating a list from our 3x3 matrix, but only including the squares of the even numbers:
= [number**2 for row in matrix for number in row if number % 2 == 0]
matrix_even_squares matrix_even_squares
[4, 16, 36, 64]
Let’s look at another example with 3 for
clauses. We start by creating a 3D matrix-like structure. Without going into much detail about 3d-matrices, we can imagine it as a cube, where each “slice” is a 2-dimensional matrix:
= [
matrix_3d # first slice
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
[
],# second slice
[10, 11, 12],
[13, 14, 15],
[16, 17, 18],
[
], ]
This means that a complete walkthrough of the matrix using nested for
loops consists of an outer for
iterating over slices, an nested for
iterating over rows, and the innermost for
iterating over the elements of each row. Similarly to what shown before, the more Pythonic way using comprehensions looks like the following:
= [num for slice in matrix_3d for row in slice for num in row]
flattened_3d print(flattened_3d)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
Another useful way to leverage multiple for
clauses is to create a Cartesian product of two lists, i.e. all the possible pairs of elements from the two lists:
= [1, 2, 3]
numbers = ['a', 'b', 'c']
letters = [(number, letter) for number in numbers for letter in letters]
cartesian_product print(cartesian_product)
[(1, 'a'), (1, 'b'), (1, 'c'), (2, 'a'), (2, 'b'), (2, 'c'), (3, 'a'), (3, 'b'), (3, 'c')]
Quoting the official docs:
A generator expression is an expression that returns an iterator.
A generator expression takes the form f(x) for x in <range>
, e.g. x*x for x in range(10)
, where the first element can also include tuples, as in (k, func(k)) for k in keylist
. They are especially handy with functions working with iterators, like sum()
, min()
, and max()
. A key aspect of generator expressions is that they evaluate elements one at a time, which brings benefits in terms of memory footprint and allows for “lazy” evaluation of certain operations.
Let’s consider a simple example where we want to sum the squares of the first 10 nonnegative integers. Knowing about list comprehensions, a simple approach is the following:
= sum([x * x for x in range(10)])
sum_of_squares sum_of_squares
285
With this code, the full list of all squares is evaluated and built in memory first with the comprehension [x*x for x in range(10)]
, and then passed as input to sum()
. The same objective can be attained by feeding sum()
directly with the generator expression x*x for x in range(10)
:
= sum(x*x for x in range(10))
sum_of_squares sum_of_squares
285
Since computing the sum can simply evaluate and add one element at a time, no list is created and memory is conserved in the process. In larger scenarios, the iterative nature of generator expressions can have a great impact on the memory footprint.
for
clausesGenerator expressions can be used with multiple for
clauses as well, in a way similar to list comprehensions:
= [[1, 2, 3], [4, 5], [6, 7, 8, 9]]
nested_list = sum(x for inner_list in nested_list for x in inner_list)
sum_nested sum_nested
45
any()
and all()
are used to check conditions over iterables. Like sum
, they can combine with generator expressions. Furthermore, any()
and all()
employ lazy evaluation. any()
stops evaluating elements of an iterable when it evaluates to True
for the first time. all()
short-circuits when it evaluates to the first False
.
= [22, 35, 24, 83, 18]
numbers if any(n % 7 == 0 for n in numbers):
print("There's a multiple of 7")
There's a multiple of 7
any()
only iterates till the 2nd element (35)
= [21, 35, 24, 83, 18]
numbers if all(n % 7 == 0 for n in numbers):
print("All numbers are multiples of 7")
all()
stops iterating after the 3d element (24)
next(iterator, default)
retrieves the next item from an iterator. Combining next()
with generator expressions makes for a very concise syntax. It can also be used to e.g. get the first element satisfying a condition, or a fallback value if no element exists.
next()
will evaluate each value, one at a time from an iterator, in this case the one returned by the generator expression.
The following example prints the square of the first multiple of 5:
= [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
numbers print(next(x**2 for x in numbers if x % 5 == 0))
0
If the iterator has no elements, a StopIteration
exception is raised:
next(x**2 for x in range(1, 5) if x % 5 == 0)
--------------------------------------------------------------------------- StopIteration Traceback (most recent call last) Cell In[20], line 1 ----> 1 next(x**2 for x in range(1, 5) if x % 5 == 0) StopIteration:
However, a default value, like None
, can be passed to next
to be returned in case of no elements, thus avoiding the exception:
print(next((x**2 for x in range(1, 5) if x % 5 == 0), None))
None
When using next()
with a generator expression and a default value, the generator expression must be parenthesized to be a valid Python syntax.
Tuples are immutable sequences used to store collections of items.
Unpacking is a concise syntax to assign multiple members of a tuple at once, useful when e.g. the various elements have a special meaning. The amount of assigned variables on the left-hand side must match the number of tuple elements.
= (1.0, 2.0, 3.0)
coords = coords
x, y, z print(f"{x=}, {y=}, {z=}")
x=1.0, y=2.0, z=3.0
Note how assigning x
, y
, z
this way is more idiomatic than using coords[0]
, coords[1]
, coords[2]
. If the number of unpacked elements does not match, and exception is raised:
= (1.0, 2.0, 3.0, 4.0)
coords = coords x, y, z
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[23], line 2 1 coords = (1.0, 2.0, 3.0, 4.0) ----> 2 x, y, z = coords ValueError: too many values to unpack (expected 3)
Combining list comprehension and tuple unpacking can make for more expressive syntax when looping over tuple members:
= [(2, 3), (3, 4), (4, 5), (5, 6)]
tuples **exp for base, exp in tuples] [base
[8, 81, 1024, 15625]
Note how this is much more idiomatic and expressive than:
0]**x[1] for x in tuples] [x[
[8, 81, 1024, 15625]
The unpacking operator *
allows us to unpack iterables into individual elements:
# Basic unpacking of a list
= [1, 2, 3]
numbers print(*numbers) # equivalent to print(1, 2, 3)
# Unpacking of a tuple
= ('red', 'green', 'blue')
colors print(*colors) # equivalent to print('red', 'green', 'blue')
# Unpacking of a dictionary
= {'name': 'John', 'age': 30, 'city': 'New York'}
person print(*person) # equivalent to print('name', 'age', 'city')
1 2 3
red green blue
name age city
*
can also be used to conveniently capture multiple elements as a list during the tuple/list unpacking with left and right side variables having different lengths. In the example below, the *
operator allows one variable (reptiles
in this case) to capture multiple remaining values. In this scenario, the variable with *
will always receive a list, even if it is unpacking a tuple.
= ('dog', 'chicken', 'snake', 'lizard')
animals *reptiles) = animals
(mammal, bird, print(f"{mammal=}, {bird=}, {reptiles=}")
mammal='dog', bird='chicken', reptiles=['snake', 'lizard']
zip()
can be conveniently used to iterate on matching elements of multiple sequences.
= ['Dave', 'Richard', 'Martin']
names = [25, 30, 35]
ages
for name, age in zip(names, ages):
print(f"{name} is {age} years old")
Dave is 25 years old
Richard is 30 years old
Martin is 35 years old
This way, we spare explicit index lookups in the equivalent non-Pythonic implementation:
= ['Dave', 'Richard', 'Martin']
names = [25, 30, 35]
ages
for i in range(len(names)):
print(f"{names[i]} is {ages[i]} years old")
zip()
can be used with the unpacking operator *
to distribute each nth element of tuples from a list into separate tuples.
= [(1.0, 10.0), (2.0, 20.0), (3.0, 30.0)]
coordinates = zip(*coordinates)
x, y print(f"x: {x}\ny: {y}")
x: (1.0, 2.0, 3.0)
y: (10.0, 20.0, 30.0)
Once more, the code is much more idiomatic and expressive than
= tuple([coord[0] for coord in coordinates])
x = tuple([coord[1] for coord in coordinates]) y
zip()
in conjunction with the *
operator can be used to unzip a list:
= [1, 2, 3]
a = ['a', 'b', 'c']
b
= zip(*zip(a, b))
a2, b2 print(f"{a2=}, {b2=}")
a2=(1, 2, 3), b2=('a', 'b', 'c')
Using enumerate()
allows to iterate over elements and their corresponding index.
for index, value in enumerate(['a', 'b', 'c']):
print(f"Index: {index}, Value: {value}")
Index: 0, Value: a
Index: 1, Value: b
Index: 2, Value: c
By default, the index first value is 0, but this can be changed using the start
argument.
for index, value in enumerate(['a', 'b', 'c'], start=1):
print(f"Counter: {index}, Value: {value}")
Counter: 1, Value: a
Counter: 2, Value: b
Counter: 3, Value: c
Similarly to zip()
, the use of enumerate()
saves us from explicit index lookups:
= ['a', 'b', 'c']
values
for i in range(len(values)):
print(f"Index: {i}, Value: {values[i]}")
Index: 0, Value: a
Index: 1, Value: b
Index: 2, Value: c
The following examples combine enumerate and list comprehension to raise a list of values to powers incrementally larger. Let’s start with showing the more Pythonic way of iteration over (index, value). It combines list comprehension and enumerate:
= [4, 9, 3, 6]
values
= [val**i for i, val in enumerate(values)]
powers powers
[1, 9, 9, 216]
Here below we show the less Pythonic way of iteration over (index, value) using index lookups:
= [values[i]**i for i in range(len(values))]
powers powers
[1, 9, 9, 216]
Finally, we show the non-Pythonic equivalent with neither enumerate nor list comprehension:
= []
powers for i in range(len(values)):
**i)
powers.append(values[i] powers
[1, 9, 9, 216]