Wednesday, September 17th, 2025¶
The zip
function¶
It often happens that we want to simultaneously iterate through two or more lists. For example, suppose we have a list false_primes
of false primes and a list prime_factorizations
that contains the prime factorization for each of the false primes. Suppose we then want to iterate through each false prime and its corresponding prime factorization in order to display this data to the reader.
The zip
function allows us to "zip" multiple lists together so that we can iterate through each simultaneously. The syntax works as follows:
for <item 1>, <item 2>, ... in zip(<list 1>, <list 2>, ...):
Consider the following examples:
numbers = [0,1,2,3,4]
letters = ['a','b','c','d','e']
for number in numbers:
for letter in letters:
print(number, letter)
0 a 0 b 0 c 0 d 0 e 1 a 1 b 1 c 1 d 1 e 2 a 2 b 2 c 2 d 2 e 3 a 3 b 3 c 3 d 3 e 4 a 4 b 4 c 4 d 4 e
With nested for
loops, we get every possible choice of number paired with every possible choice of letter.
for number, letter in zip(numbers, letters):
print(number, letter)
0 a 1 b 2 c 3 d 4 e
With the zip
function, we pair off each number and corresponding letter, then iterate through each pair.
List slicing¶
We've discussed how to access specific elements from a list by index using square brackets.
my_list = ['a','b','c','d','e','f','g','h']
my_list[0]
'a'
my_list[3]
'd'
my_list[-1]
'h'
Sometimes, we might want to get several elements from a list. For example, suppose we want a list containing every other element from my_list
.
every_other = []
for i in range(0,len(my_list), 2):
every_other.append(my_list[i])
print(my_list)
print(every_other)
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] ['a', 'c', 'e', 'g']
A more elegant approach is to use list slicing. There are several ways that we can take slices of a list.
my_list[start_index:]
will start the slice atstart_index
and go the end of the list.my_list[:stop_index]
will start the list at the beginning (i.e.0
) and proceed until one less thanstop_index
.my_list[start_index:stop_index]
will start the slice atstart_index
and proceed until one less thanstop_index
.
Note: We can also use negative indices as stop_index
to count backward from the end of the list. For example, stop_index=-1
will skip the last element of the list.
print(my_list)
print(my_list[3:])
print(my_list[:5])
print(my_list[3:5])
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] ['d', 'e', 'f', 'g', 'h'] ['a', 'b', 'c', 'd', 'e'] ['d', 'e']
Just like the range
function, we can optionally include a step size to our slice. For example:
my_list[::skip]
will start at the beginning of the list and go to the end, but will go in steps ofskip
.my_list[start_index:stop_index:skip]
will start atstart_index
, proceed in steps ofskip
, and will stop when the index is greater than or equal tostop_index
.
print(my_list)
print(my_list[::2])
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] ['a', 'c', 'e', 'g']
List comprehension¶
So far, we've primarily constructed lists of data by using for
loops and appending to an empty list. Another possibility is to use list comprehension. The syntax is as follows:
[<some expression> for <some item> in <some iterable>]
For example, suppose we want to construct a list containing the squares of the first 10 positive integers.
squares = []
for i in range(1,11):
squares.append(i**2)
print(squares)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
squares = [ i**2 for i in range(1,11) ]
print(squares)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
We can optionally include a condition when using list comprehension. In this case, the syntax is:
[<some expression> for <some item> in <some iterable> if <some condition>]
.
For example, suppose we want to construct a list containing the squares of the first 10 positive integers that have remainder 1
after division by 4
.
squares_with_remainder_1 = [ i**2 for i in range(1,11) if i**2 % 4 == 1 ]
print(squares_with_remainder_1)
[1, 9, 25, 49, 81]
Note: In the above cell, we compute i**2
twice in the list comprehension. Can we avoid this somehow?
squares = [ i**2 for i in range(1,11) ]
squares_with_remainder_1 = [ square for square in squares if square % 4 == 1 ]
print(squares_with_remainder_1)
[1, 9, 25, 49, 81]
Exercise: Use list comprehension to generate a list of all prime numbers less than 100
.
from math import sqrt
def is_prime(n):
for d in range(2,int(sqrt(n))+1):
if n % d == 0:
return False
return True
primes = [ n for n in range(2,1001) if is_prime(n) ]
print(primes[:10])
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Mutable and immutable objects¶
The word mutable means "liable to change." Consider the following experiments.
Experiment #1:
n = 3
m = n
print('n = {}'.format(n))
print('m = {}'.format(m))
n = 3 m = 3
What happens to m
if we make changes to n
?
n = n + 5
print('n = {}'.format(n))
print('m = {}'.format(m))
n = 8 m = 3
Experiment #2:
list1 = [1,2,3,4]
list2 = list1
print('list1 =',list1)
print('list2 =',list2)
list1 = [1, 2, 3, 4] list2 = [1, 2, 3, 4]
What happens to list2
if we make changes to list1
?
list1[0] = 99
print('list1 =',list1)
print('list2 =',list2)
list1 = [99, 2, 3, 4] list2 = [99, 2, 3, 4]
What's going on here? Setting m = n
did not cause changes in n
to propogate back to m
, but in the list example, setting list2=list1
did cause changes to list1
to propogate to list2
.
The difference is that integers are immutable objects in Python while lists are mutable obects.
- Immutable objects:
- Integers
- Strings
- Floats
- Booleans
- Tuples
- Mutable objects:
- Lists
- Dictionaries
- Sets
- NumPy arrays
For mutable objects, variable assigments point to a place in memory that stores the obect (in this case a list). That is, list1
and list2
point to the same location in memory. Making modifications to the object modifies the values stored in this shared memory location, so all variables pointing to this location are affected. We can use the id
function to get an identifier for the memory location of an object.
id(list1)
1824045649280
id(list2)
1824045649280
We can check whether two variables have the same ID to see whether they share the same memory location (so that changes to one affect the other).
list1 = [1,2,3,4]
list2 = list1
list3 = [1,2,3,4]
id(list1) == id(list3)
False
list1 == list3
True
id(list1) == id(list2)
True
list1[0] = -10
print(list1)
print(list2)
print(list3)
[-10, 2, 3, 4] [-10, 2, 3, 4] [1, 2, 3, 4]
Alternatively, we can use the is
operator to check if two variables refer to the same object in memory.
list1 is list2
True
list1 is list3
False
Sometimes, we may want to copy a list to a new variable but sever this link. That is, we might want a new variable (pointing to a new location in memory) that has the same values. To do this, we can use the .copy()
method:
list1 = [1,2,3,4]
list2 = list1.copy()
list2 is list1
False
list2[0] = 99
print(list1)
print(list2)
[1, 2, 3, 4] [99, 2, 3, 4]
Plotting in Python¶
We will the pyplot
submodule from the matplotlib
module for our plotting needs. When importing modules (or submodules), we can assign our own short-hand name using the syntax import <some module or submodule> as <some short-hand name>
.
Typically, we will import the matplotlib.pyplot
module and assign it the short-hand name plt
.
import matplotlib.pyplot as plt
#help(plt)
The pyplot
module has many for data visualization and plotting. The most basic is the plot
function:
#help(plt.plot)
The plot
function is extremely flexible in how it can be called (by making use of default arguments and keyword arguments). One of the most basic ways to use the plot
function is to enter a list of $x$-values and a list of $y$-values as inputs. That is, plt.plot(<list of x-values>, <list of y-values>)
.
x_list = [ i for i in range(10)]
y_list = [ x**2 for x in x_list ]
plt.plot(x_list, y_list)
[<matplotlib.lines.Line2D at 0x1a8b2ad1d10>]
We can call plt.plot
several times within a cell to plot several pieces of data.
x_list = [ x for x in range(10) ]
y1_list = [ x**2 for x in x_list ]
y2_list = [100 - x**2 for x in x_list ]
plt.plot(x_list, y1_list)
plt.plot(x_list, y2_list)
[<matplotlib.lines.Line2D at 0x1a8b3d42d50>]
Options when calling plt.plot
¶
By default, the plot
function will connect the supplied data points with a polygonal line, with the color selected automatically. We can include additional keyword arguments to change this behavior.
Changing the color¶
For example, the keyword argument color
can be used to manually set the color. There are many color characters available:
'r'
: red'g'
: green'b'
: blue'k'
: black'm'
: magenta'c'
: cyan'y'
: yellow'C0'
,'C1'
, ...'C9'
: a sequence of colors thatpyplot
automatically uses when no color is supplied
We can also supply certain color names (see Matplotlib documentation), or an RGB triple of integers (we will discuss this later in the course).
plt.plot(x_list, y1_list, color='m')
plt.plot(x_list, y2_list, color='g')
[<matplotlib.lines.Line2D at 0x1a8b453ce10>]
Changing the marker style¶
The keyword argument marker
can be used to change the marker style. Here are some of the available styles:
'.'
: point','
: pixel'o'
: circle'^'
: triangle'*'
: star's'
: square'+'
: plus'x'
: x
More can be found in the Matplotlib documentation.
plt.plot(x_list, y1_list, color='m', marker='o')
plt.plot(x_list, y2_list, color='g', marker='^')
[<matplotlib.lines.Line2D at 0x1a8b45f3d90>]
Changing the line style¶
The keyword argument linestyle
can be used to change the line style. Here are some of the available styles:
'-'
or'solid'
: solid line'--'
or'dashed'
: dashed line':'
or'dotted'
: dotted line'-.'
or'dashdot
': alternating dots and dashes''
or'None'
: no line
More can be found in the Matplotlib documentation.
plt.plot(x_list, y1_list, color='m', marker='o', linestyle='--')
plt.plot(x_list, y2_list, color='g', marker='^', linestyle='-.')
[<matplotlib.lines.Line2D at 0x1a8b3e0d6d0>]
Using the fmt
string short-hand¶
When calling plt.plot
, we can include a string after our $x$- and $y$-values with color, marker, and line configuration options. For example, the string 'r*--'
tells plt.plot
to plot in red with stars for markers and dashed lines.
plt.plot(x_list, y1_list, 'mo--')
plt.plot(x_list, y2_list, 'g^-.')
[<matplotlib.lines.Line2D at 0x1a8b9d79590>]
Labeling plots¶
When graphing data, we should (almost) always include:
- a title
- axis labels When plotting several curves in a single figure, we should also label the curves and include a legend.
Titles can be added using plt.title
:
plt.plot(x_list, y1_list, 'mo--')
plt.plot(x_list, y2_list, 'g^-.')
plt.title('My first plot')
Text(0.5, 1.0, 'My first plot')
We can use plt.xlabel
and plt.ylabel
to add axis labels. These labels can include LaTeX (as can the title):
plt.plot(x_list, y1_list, 'mo--')
plt.plot(x_list, y2_list, 'g^-.')
plt.title('My first plot')
plt.xlabel('$x$')
plt.ylabel('$y$')
Text(0, 0.5, '$y$')
When calling plt.plot
, we can use the label
keyword argument to give a label to the data being plotted.
We can then use the plt.legend
function to add a legend to the figure.
plt.plot(x_list, y1_list, 'mo--', label='$y = x^2$')
plt.plot(x_list, y2_list, 'g^-.', label='$y = 100 - x^2$')
plt.title('My first plot')
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.legend()
<matplotlib.legend.Legend at 0x1a8b976dbe0>
Further tweaks: axis limits, grid lines¶
We can change the horizontal and vertical limits using the plt.xlim
and plt.ylim
function:
plt.plot(x_list, y1_list, 'mo--', label='$y = x^2$')
plt.plot(x_list, y2_list, 'g^-.', label='$y = 100 - x^2$')
plt.xlim(4,8)
plt.ylim(20, 80)
plt.title('My first plot')
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.legend()
<matplotlib.legend.Legend at 0x1a8ba5834d0>
We can use plt.grid()
to add grid lines to the interior of the plot:
plt.plot(x_list, y1_list, 'mo--', label='$y = x^2$')
plt.plot(x_list, y2_list, 'g^-.', label='$y = 100 - x^2$')
plt.xlim(4,8)
plt.ylim(20, 80)
plt.title('My first plot')
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.legend()
plt.grid()