Wednesday, February 18th, 2026¶
On Monday, we discussed documenting code using both Markdown and code comments.
Documenting code in your report¶
When writing your report, every code cell must be accompanied by a preceding Markdown cell above which explains:
- What the code cell does,
- How the code works,
- How it fits into the narrative of the report (i.e. why are we doing this?).
The code cell must then include code comments that tie into the Markdown explanation of how the code works for further clarification.
Exercise: Write a function is_prime_like that takes in an integer n and returns True if n is prime-like and False otherwise. Include a preceding Markdown cell that explains what the function does and how it works, and include code comments that connect to the Markdown explanation for clarification.
Exercise: Write a function get_prime_decomp that takes in an integer n and returns a list containing the prime decomposition of n. Include a preceding Markdown cell that explains what the function does and how it works, and include code comments that connect to the Markdown explanation for clarification.
Modules in Python¶
When starting a Jupyter notebook, Python loads in a base set of commands that are made available (e.g. print, list, int, etc.). However, there are many other commands that are not loaded by default that we sometimes want to make use of. We can import various modules that contain all kinds of additional functionality.
For example, suppose we want to consider the efficiency of our get_primes function. The time module contains tools relating to time, which we can use to time the get_primes function.
Importing a module¶
We can use the syntax import <some module> to make that module available to us. After doing so, we can use the syntax <some module>.<some function or object> to call upon variables functions or objects contained in that module.
import time
The time module contains many functions. We can use the help function to look at the documentation and see some of the available functions.
#help(time)
For example, the asctime function from the time module gives a string specifying the current time.
time.asctime()
'Wed Feb 18 15:38:39 2026'
Another is the time function from the time module. This function returns the number of seconds (as a float) since the Epoch. We can use this function to time how long it takes for some code to run.
time.time()
1771447158.406582
time.time()
1771447201.757252
t0 = time.time()
t1 = time.time()
print(t1-t0, 'seconds have elapsed.')
14.405227184295654 seconds have elapsed.
Importing from a module¶
Sometimes we just want to make use of a single function or object from within a module. We can use the syntax from <some module> import <some function or object> to gain direct access to the function or object. After doing so, we can simply use <some function or object> directly rather than looking back inside the module (that is, we do not need to include <some module>. before calling upon the function/object).
For example, if we try to directly call on the asctime function, we will get an error.
time.asctime()
'Wed Feb 18 15:42:54 2026'
asctime()
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[14], line 1 ----> 1 asctime() NameError: name 'asctime' is not defined
If we import the asctime function from the time module, then we can directly call upon asctime.
from time import asctime
asctime()
'Wed Feb 18 15:43:50 2026'
Exercise: Time how long it takes the is_prime function to test whether $n = 100,000,007$ is prime.
def is_prime(n):
for d in range(2,n):
if n % d == 0:
return False
return True
n = 100_000_007
t0 = time.time()
print(is_prime(n))
t1 = time.time()
print(t1 - t0)
True 18.63295340538025
is_prime(131)
True
Optimizing the is_prime function¶
For the first project, we will need to call upon the is_prime function many times. With that in mind, it will be good to try to make the is_prime function more efficient so that our code can run in a reasonable amount of time.
def old_is_prime(n):
for d in range(2,n):
if n % d == 0:
return False
return True
Exercise: Rewrite the is_prime function to take advantage of the $\sqrt{n}$ optimization discussed in class. We can use the sqrt function from the math module to compute square roots.
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
is_prime(131)
True
Exercise: Compare the runtime for the old is_prime function (without optimization) to the newly optimized is_prime function when checking whether $n = 100,000,007$ is prime.
n = 100_000_007
t0 = time.time()
print(old_is_prime(n))
t1 = time.time()
print(t1 - t0)
True 20.87965989112854
t0 = time.time()
print(is_prime(n))
t1 = time.time()
print(t1 - t0)
True 0.003634214401245117
The zip function¶
It often happens that we want to simultaneously iterate through two or more lists. For example, suppose we have two lists first_names and last_names that separately hold the first and last names of some individuals (in corresponding order).
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>, ...):
(do something...)
With this setup, the for loop will simultaneously grab all of the first elements from each list and name them <item 1>, <item 2>, ..., then perform the operations inside the loop, then grab the second elements from each list, etc.
Consider the following examples that illustrate this point. We compare iterating through both lists using nested for loops versus using the zip function to simultaneously iterate through both lists.
first_names = ['Seamus', 'Jordan', 'James']
last_names = ['Coleman', 'Pickford', 'Garner']
for first_name in first_names:
for last_name in last_names:
print(first_name, last_name)
Seamus Coleman Seamus Pickford Seamus Garner Jordan Coleman Jordan Pickford Jordan Garner James Coleman James Pickford James Garner
With nested for loops, we get every possible choice of first_name paired with every possible choice of last_name. Instead, we can use a single for loop to iterate through the two lists zipped together.
for first_name, last_name in zip(first_names, last_names):
print(first_name, last_name)
Seamus Coleman Jordan Pickford James Garner
With the zip function, we pair off each first_name with the corresponding last_name, then iterate through each pair.
For another example, suppose we have a list false_primes of false primes and a list prime_decomps that contains the prime decompositions for each of the false primes. We may then with to iterate through each false prime and its corresponding prime decomposition in order to display this data to the reader.
false_primes = [561, 701, 1001, 2003]
prime_decomps = [[5, 7, 11], [13, 17], [2, 5, 131]]
for false_prime, prime_decomp in zip(false_primes, prime_decomps):
print(false_prime, prime_decomp)
561 [5, 7, 11] 701 [13, 17] 1001 [2, 5, 131]
List slicing¶
So far, when working with lists we've discussed how to access specific elements by index using square brackets. That is my_list[i] will get the element in the ith index of my_list.
my_list = ['a','b','c','d','e','f','g','h']
print(my_list[0])
print(my_list[3])
print(my_list[-1])
a d 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(len(my_list)):
if i % 2 == 0:
every_other.append(my_list[i])
print(every_other)
['a', 'c', 'e', 'g']
every_other = []
for i in range(0,len(my_list),2):
every_other.append(my_list[i])
print(every_other)
['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_indexand 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_indexand proceed until one less thanstop_index.
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[:4])
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] ['a', 'b', 'c', 'd']
print(my_list)
print(my_list[3:])
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] ['d', 'e', 'f', 'g', 'h']
If we compare the use of list slicing with the use of the range function, we can see that they share a lot in common.
Instead of using commas to separate the start and stop indices, we use colons.
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.
my_list[::2]
['a', 'c', 'e', 'g']
Exercise: Generate a list cubes that stores the cubes of the first $40$ positive integers. Then print out the third through tenth of these cubes (i.e. index-2 through index-9).
Exercise: Print out the last ten cubes from the cubes list.
Exercise: Print out every third cube from the cubes list, starting with the second cube (i.e. the index-1 entry).