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:

InĀ [18]:
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.

InĀ [19]:
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.

InĀ [20]:
my_list = ['a','b','c','d','e','f','g','h']
InĀ [21]:
my_list[0]
Out[21]:
'a'
InĀ [22]:
my_list[3]
Out[22]:
'd'
InĀ [23]:
my_list[-1]
Out[23]:
'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.

InĀ [24]:
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 at start_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 than stop_index.
  • my_list[start_index:stop_index] will start the slice at start_index and proceed until one less than stop_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.

InĀ [28]:
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 of skip.
  • my_list[start_index:stop_index:skip] will start at start_index, proceed in steps of skip, and will stop when the index is greater than or equal to stop_index.
InĀ [29]:
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.

InĀ [54]:
squares = []
for i in range(1,11):
    squares.append(i**2)

print(squares)
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
InĀ [32]:
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.

InĀ [55]:
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?

InĀ [56]:
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.

InĀ [57]:
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
InĀ [60]:
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:

InĀ [61]:
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?

InĀ [62]:
n = n + 5

print('n = {}'.format(n))
print('m = {}'.format(m))
n = 8
m = 3

Experiment #2:

InĀ [63]:
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?

InĀ [64]:
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.

InĀ [65]:
id(list1)
Out[65]:
1824045649280
InĀ [66]:
id(list2)
Out[66]:
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).

InĀ [72]:
list1 = [1,2,3,4]
list2 = list1
list3 = [1,2,3,4]
InĀ [75]:
id(list1) == id(list3)
Out[75]:
False
InĀ [73]:
list1 == list3
Out[73]:
True
InĀ [74]:
id(list1) == id(list2)
Out[74]:
True
InĀ [77]:
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.

InĀ [78]:
list1 is list2
Out[78]:
True
InĀ [79]:
list1 is list3
Out[79]:
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:

InĀ [80]:
list1 = [1,2,3,4]
list2 = list1.copy()

list2 is list1
Out[80]:
False
InĀ [81]:
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.

InĀ [82]:
import matplotlib.pyplot as plt
InĀ [84]:
#help(plt)

The pyplot module has many for data visualization and plotting. The most basic is the plot function:

InĀ [86]:
#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>).

InĀ [87]:
x_list = [ i for i in range(10)]
y_list = [ x**2 for x in x_list ]

plt.plot(x_list, y_list)
Out[87]:
[<matplotlib.lines.Line2D at 0x1a8b2ad1d10>]
No description has been provided for this image

We can call plt.plot several times within a cell to plot several pieces of data.

InĀ [88]:
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)
Out[88]:
[<matplotlib.lines.Line2D at 0x1a8b3d42d50>]
No description has been provided for this image

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 that pyplot 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).

InĀ [89]:
plt.plot(x_list, y1_list, color='m')
plt.plot(x_list, y2_list, color='g')
Out[89]:
[<matplotlib.lines.Line2D at 0x1a8b453ce10>]
No description has been provided for this image

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.

InĀ [91]:
plt.plot(x_list, y1_list, color='m', marker='o')
plt.plot(x_list, y2_list, color='g', marker='^')
Out[91]:
[<matplotlib.lines.Line2D at 0x1a8b45f3d90>]
No description has been provided for this image

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.

InĀ [92]:
plt.plot(x_list, y1_list, color='m', marker='o', linestyle='--')
plt.plot(x_list, y2_list, color='g', marker='^', linestyle='-.')
Out[92]:
[<matplotlib.lines.Line2D at 0x1a8b3e0d6d0>]
No description has been provided for this image

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.

InĀ [95]:
plt.plot(x_list, y1_list, 'mo--')
plt.plot(x_list, y2_list, 'g^-.')
Out[95]:
[<matplotlib.lines.Line2D at 0x1a8b9d79590>]
No description has been provided for this image

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:

InĀ [96]:
plt.plot(x_list, y1_list, 'mo--')
plt.plot(x_list, y2_list, 'g^-.')

plt.title('My first plot')
Out[96]:
Text(0.5, 1.0, 'My first plot')
No description has been provided for this image

We can use plt.xlabel and plt.ylabel to add axis labels. These labels can include LaTeX (as can the title):

InĀ [98]:
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$')
Out[98]:
Text(0, 0.5, '$y$')
No description has been provided for this image

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.

InĀ [100]:
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()
Out[100]:
<matplotlib.legend.Legend at 0x1a8b976dbe0>
No description has been provided for this image

Further tweaks: axis limits, grid lines¶

We can change the horizontal and vertical limits using the plt.xlim and plt.ylim function:

InĀ [103]:
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()
Out[103]:
<matplotlib.legend.Legend at 0x1a8ba5834d0>
No description has been provided for this image

We can use plt.grid() to add grid lines to the interior of the plot:

InĀ [104]:
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()
No description has been provided for this image