Module 3
Spring 2026
The following materials are meant to supplement the lectures and exercises for Module 3. They cover additional topics and examples related to Python Essentials. Feel free to explore these resources to deepen your understanding of the concepts we are learning in class. If you have any questions or need further clarification on any of the topics, please don’t hesitate to ask!
Week 10
A list in Python is an ordered, mutable collection of items. Lists can hold elements of any type and support indexing, slicing, and a wide range of built-in methods.
my_list = [10, "hello", 3.14, True]
my_list.append("new") # add to end
my_list.pop() # remove last item
my_list.sort() # sort in place
my_list[1] # access by index
my_list[-1] # last item
my_list[1:3] # slice from index 1 up to (not including) 3Python makes it easy to work with files using the with open(...) pattern, which automatically closes the file when done:
# Reading a file
with open("data.txt", "r") as f:
content = f.read()
# Writing a file
with open("output.txt", "w") as f:
f.write("Hello, file!")Common Methods
Python lists come with many built-in methods. Here is a quick reference for the ones you will use most often.
| Method | What it does |
|---|---|
append(item) |
Adds item to the end of the list |
insert(i, item) |
Inserts item at index i, shifting the rest right |
remove(item) |
Removes the first occurrence of item (raises ValueError if missing) |
pop(i=-1) |
Removes and returns the item at index i (default: last item) |
sort() |
Sorts the list in place (ascending by default) |
reverse() |
Reverses the list in place |
index(item) |
Returns the index of the first occurrence of item |
count(item) |
Returns how many times item appears in the list |
extend(other) |
Appends all items from other to the end of the list |
clear() |
Removes all items, leaving an empty list |
colors = ["red", "green", "blue"]
colors.append("yellow") # ["red", "green", "blue", "yellow"]
colors.insert(1, "orange") # ["red", "orange", "green", "blue", "yellow"]
colors.remove("green") # ["red", "orange", "blue", "yellow"]
popped = colors.pop() # popped = "yellow", list is now ["red", "orange", "blue"]
colors.sort() # ["blue", "orange", "red"]
print(colors.index("orange")) # 1
print(colors.count("blue")) # 1
colors.extend(["pink", "white"]) # ["blue", "orange", "red", "pink", "white"]
colors.clear() # []Additional Methods
When you need both the position and the value while looping over a list, use enumerate(). When you need to pair items from two lists side by side, use zip().
# enumerate() gives (index, value) pairs
fruits = ["apple", "banana", "cherry"]
for i, fruit in enumerate(fruits):
print(i, fruit)
# 0 apple
# 1 banana
# 2 cherry
# Start counting from 1 instead of 0
for i, fruit in enumerate(fruits, start=1):
print(f"{i}. {fruit}")
# 1. apple
# 2. banana
# 3. cherry
# zip() pairs two lists together, stopping at the shorter one
names = ["Alice", "Bob", "Carol"]
scores = [88, 92, 75]
for name, score in zip(names, scores):
print(f"{name}: {score}")
# Alice: 88
# Bob: 92
# Carol: 75List Comprehensions
A list comprehension creates a new list by applying an expression to every item in an iterable, with an optional filter.
numbers = [1, 2, 3, 4, 5, 6]
# Example 1: square every number
squares = [n ** 2 for n in numbers]
print(squares) # [1, 4, 9, 16, 25, 36]
# Example 2: keep only even numbers
evens = [n for n in numbers if n % 2 == 0]
print(evens) # [2, 4, 6]
# Compare to the equivalent for-loop approach
result = []
for n in numbers:
if n % 2 == 0:
result.append(n) # same as the comprehension aboveFile Modes
When you open a file with open(), the second argument is the mode. If you omit the mode, Python defaults to "r" (read). The with open(...) pattern is preferred because it closes the file automatically, even if an error occurs.
| Mode | Meaning |
|---|---|
"r" |
Read only. File must exist. |
"w" |
Write (creates a new file or overwrites an existing one). |
"a" |
Append (writes to the end without erasing existing content). |
"r+" |
Read and write. File must exist. |
# Reading a file line by line
with open("data.txt", "r") as f:
for line in f:
print(line.strip()) # strip() removes the newline character
# Overwriting a file completely
with open("output.txt", "w") as f:
f.write("First line\n")
f.write("Second line\n")
# Appending to an existing file (does not erase old content)
with open("log.txt", "a") as f:
f.write("New log entry\n")Interactive Lists and Files Demo
Week 11
Conditional statements in Python let your program make decisions. The if, elif, and else keywords form a chain where Python evaluates each condition in order and executes the first matching branch.
age = 25
if age >= 65:
print("senior")
elif age >= 18:
print("adult")
else:
print("minor")Python supports six comparison operators for building conditions:
x == y # equal
x != y # not equal
x < y # less than
x <= y # less than or equal
x > y # greater than
x >= y # greater than or equalThe logical operators and, or, and not combine or negate boolean expressions:
if score >= 60 and score < 90:
print("passing")
if temperature < 0 or temperature > 40:
print("extreme weather")
if not is_locked:
print("door is open")True and False Values
Python evaluates any value as either truthy or falsy inside an if condition. You do not always need an explicit comparison; Python checks whether the value itself is “empty” or “zero.”
The following values are falsy (they evaluate to False):
False0and0.0""(empty string)[](empty list),{}(empty dict),()(empty tuple)None
Everything else is truthy. This lets you write shorter, more readable conditions:
my_list = [1, 2, 3]
# Verbose style — works but unnecessary
if len(my_list) > 0:
print("list has items")
# Pythonic style — same meaning, cleaner
if my_list:
print("list has items")
username = ""
if not username: # empty string is falsy
print("please enter a username")Additional Operator
The in operator tests whether a value is a member of a sequence or collection. It works with strings, lists, and dictionary keys.
# Check membership in a string
vowels = "aeiou"
if "e" in vowels:
print("found a vowel") # prints
# Check membership in a list
fruits = ["apple", "banana", "cherry"]
if "banana" in fruits:
print("banana is available") # prints
# Check if a key exists in a dictionary
student = {"name": "Alice", "grade": 90}
if "grade" in student:
print(student["grade"]) # 90
# Use `not in` to check absence
if "mango" not in fruits:
print("mango not on the list") # printsNested Conditions
You can place an if statement inside another if block to handle decisions that depend on earlier checks. Keep nesting shallow (two levels at most) to avoid code that is hard to follow.
score = 85
submitted = True
if submitted:
if score >= 60:
print("passed")
else:
print("failed")
else:
print("assignment not submitted")When nesting grows deep, consider combining conditions with and/or or extracting logic into a function instead.
Ternary Expression
Python supports a one-line conditional expression that assigns one of two values based on a condition.
age = 20
# Classic if-else (three lines)
if age >= 18:
label = "adult"
else:
label = "minor"
# Ternary expression (one line, same result)
label = "adult" if age >= 18 else "minor"
print(label) # adult
# Useful inside print or function calls
score = 73
print("pass" if score >= 60 else "fail") # passInteractive Conditions Demo
Week 12
A while loop in Python repeats a block of code as long as a condition remains True. Unlike a for loop, a while loop requires you to manually update the loop variable, and forgetting to do so creates an infinite loop.
i = 0
while i < 5:
print(i)
i += 1 # must update i, or the loop runs foreverUse break to exit a loop early and continue to skip the rest of the current iteration:
while True:
val = int(input("Enter a number: "))
if val == 0:
break # stop the loop
if val < 0:
continue # skip negative numbers
print(val)while - else
Interestingly, Python’s while loop has an optional else clause. The else block runs once after the condition becomes False. Importantly, it does not run if the loop exits via break.
n = 5
while n > 0:
print(n)
n -= 1
else:
print("countdown complete") # runs after loop finishes normally
# The else is skipped when break fires
n = 5
while n > 0:
if n == 3:
break # exits without running else
print(n)
n -= 1
else:
print("this will not print")Common Patterns
A few patterns come up over and over in beginner programs. Recognizing them makes it easier to write loops quickly.
# Pattern 1: Input validation loop
# Keep asking until the user gives a valid answer
while True:
user_input = input("Enter a positive number: ")
if user_input.isdigit() and int(user_input) > 0:
value = int(user_input)
break # valid input received, exit loop
print("Invalid. Try again.")
# Pattern 2: Countdown
count = 10
while count >= 0:
print(count)
count -= 1 # decrement toward the stopping condition
# Pattern 3: Accumulator
# Sum numbers entered by the user until they enter 0
total = 0
while True:
num = int(input("Enter a number (0 to stop): "))
if num == 0:
break
total += num # accumulate the running total
print("Total:", total)Functions
Functions help organize code and reduce repetition to read and test. Variables defined inside a function are local and cannot be accessed outside it. A function in Python is a reusable block of code defined with def. Functions accept parameters and return values with return:
def add(a, b):
return a + b
result = add(3, 4) # result = 7Defining and Calling Functions
The basic structure of a function is the def keyword, a name, parentheses with zero or more parameters, a colon, and an indented body. Use return to send a value back to the caller. If there is no return, Python returns None automatically.
# A function with two parameters
def calculate_area(width, height):
area = width * height # compute the result
return area # send it back to the caller
# Calling the function and using its return value
room_area = calculate_area(5, 8)
print(room_area) # 40
# A function can call other functions
def calculate_perimeter(width, height):
return 2 * (width + height)
print(calculate_perimeter(5, 8)) # 26Default Parameters
You can give a parameter a default value so callers do not need to supply every argument. Default parameters must come after any required ones.
def greet(name, greeting="Hello"):
print(f"{greeting}, {name}!")
greet("Alice") # Hello, Alice! — uses default
greet("Bob", "Good morning") # Good morning, Bob! — overrides default
greet(name="Carol", greeting="Hi") # Hi, Carol! — keyword styleInteractive While Loops and Functions Demo
Week 14
Dictionary is a fundamental data structure in Python, and it allows you to store and manage data in key-value pairs. A dictionary is defined using curly braces {}, with each key-value pair separated by a colon :. Here is an example of a dictionary in Python:
example = {'name': 'Seth', 'age': 30, 'occupation': 'Engineer', 'city': 'New York'}What Is a Dictionary?
A dictionary maps keys to values, much like a real-world dictionary maps a word to its definition. You look up a key and instantly retrieve the associated value. Key properties of Python dictionaries:
- Key-value pairs: every entry has a key and a corresponding value.
- Mutable: you can add, update, or remove entries after the dictionary is created.
- Keys must be unique: if you assign a value to the same key twice, the second assignment overwrites the first.
- Keys must be immutable: strings, numbers, and tuples can be keys; lists cannot.
Use a dictionary when you need fast lookups by a meaningful label rather than by position. Examples include storing a student’s course grades, counting word frequencies in a document, or mapping product IDs to prices.
Creating Dictionaries
There are two common ways to create a dictionary.
Literal syntax (most common):
# Curly braces with key: value pairs
student = {
'name': 'Alice',
'major': 'Computer Science',
'gpa': 3.8
}dict() constructor:
# Using the dict() function with keyword arguments
student = dict(name='Alice', major='Computer Science', gpa=3.8)An empty dictionary is just {} or dict().
Accessing Values
Bracket notation retrieves the value for a key. If the key does not exist, Python raises a KeyError:
print(student['name']) # Alice
print(student['gpa']) # 3.8.get() with a default is safer, returning None (or a value you choose) when the key is missing:
print(student.get('age')) # None (key missing, no error)
print(student.get('age', 'N/A')) # N/A (custom default)
print(student.get('name', 'N/A')) # Alice (key exists, default ignored)Modifying Dictionaries
Adding a new key or updating an existing key use the same bracket assignment syntax:
student['age'] = 20 # add a new key
student['gpa'] = 3.9 # update an existing keyDeleting a key can be done with del or .pop(). Use .pop() when you also need the removed value:
del student['age'] # remove the key (no return value)
gpa = student.pop('gpa') # remove and return the value
print(gpa) # 3.9Iterating Over Dictionaries
Three methods let you loop over different parts of a dictionary:
| Method | Returns | Example use |
|---|---|---|
.keys() |
All keys | Check which fields exist |
.values() |
All values | Sum or compare values |
.items() |
Key-value pairs as tuples | Process both together |
grades = {'math': 92, 'english': 85, 'history': 78}
for subject in grades.keys():
print(subject) # math, english, history
for score in grades.values():
print(score) # 92, 85, 78
for subject, score in grades.items():
print(f"{subject}: {score}") # math: 92, english: 85, ...Common Use Cases
Dictionaries shine in three recurring patterns:
Counting occurrences (frequency table):
words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
counts = {} # start with an empty dictionary
for word in words:
if word in counts:
counts[word] += 1 # key already exists, increment
else:
counts[word] = 1 # first time seeing this word
print(counts) # {'apple': 3, 'banana': 2, 'cherry': 1}Lookup table (map one value to another):
# Map letter grades to GPA points
grade_points = {
'A': 4.0,
'B': 3.0,
'C': 2.0,
'D': 1.0,
'F': 0.0
}
letter = 'B'
print(f"A {letter} is worth {grade_points[letter]} GPA points.")
# A B is worth 3.0 GPA points.Grouping items (collect related values under a shared key):
# Group students by their major
roster = [('Alice', 'CS'), ('Bob', 'Math'), ('Carol', 'CS'), ('Dave', 'Math')]
by_major = {}
for name, major in roster:
if major not in by_major:
by_major[major] = [] # create an empty list for new major
by_major[major].append(name) # add student to the right group
print(by_major)
# {'CS': ['Alice', 'Carol'], 'Math': ['Bob', 'Dave']}Interactive Dictionary Demo
Week 15
Sorting is a fundamental operation in computer science, and there are many different algorithms that can be used to sort data. Here, we will explore some of the most common sorting algorithms, including Bubble Sort and Selection Sort. Please refer to the provided interactive plot at the bottom for a fun demonstration of these sorting algorithms.
Why Sorting Matters
Sorting arranges a collection of items into a defined order, such as smallest to largest or alphabetically. Nearly every real-world data task relies on sorting: ranking search results, displaying leaderboards, organizing a file list, or preparing data before further analysis. Faster sorting algorithms allow programs to handle larger datasets without slowing down.
Python’s Built-in Sorting
Python gives you two tools for sorting: sorted() and .sort(). Knowing the difference helps you choose the right one.
| Feature | sorted() |
.sort() |
|---|---|---|
| Works on | Any iterable | Lists only |
| Returns | A new sorted list | None (sorts in place) |
| Original data | Unchanged | Modified |
| When to use | You need the original order preserved | You are done with the original order |
scores = [45, 92, 67, 38, 81]
# sorted() returns a new list; scores is unchanged
ranked = sorted(scores)
print(ranked) # [38, 45, 67, 81, 92]
print(scores) # [45, 92, 67, 38, 81] (original intact)
# .sort() modifies the list in place; nothing is returned
scores.sort()
print(scores) # [38, 45, 67, 81, 92]Both sorted() and .sort() accept a key= argument. You pass a function, and Python uses its return value to determine the sort order (the original items are still what gets placed in the result):
names = ['Charlie', 'alice', 'Bob']
# Default sort is case-sensitive; uppercase letters sort before lowercase
print(sorted(names)) # ['Bob', 'Charlie', 'alice']
# key=str.lower makes the comparison case-insensitive
print(sorted(names, key=str.lower)) # ['alice', 'Bob', 'Charlie']
# Sort a list of tuples by the second element (score)
students = [('Alice', 88), ('Bob', 72), ('Carol', 95)]
print(sorted(students, key=lambda s: s[1]))
# [('Bob', 72), ('Alice', 88), ('Carol', 95)]Use reverse=True to sort in descending order:
print(sorted(scores, reverse=True)) # [92, 81, 67, 45, 38]Bubble Sort
Bubble Sort is one of the simplest sorting algorithms to understand. The idea is to repeatedly walk through the list and compare each pair of neighboring elements. If the left neighbor is larger than the right neighbor, swap them. After each full pass, the largest unsorted value has “bubbled up” to its correct position at the end.
Steps for one pass:
- Compare element at index 0 with element at index 1. Swap if out of order.
- Move to index 1 and index 2. Swap if out of order.
- Continue until the end of the unsorted portion.
- Repeat until no swaps occur in an entire pass.
def bubble_sort(arr):
n = len(arr)
for i in range(n): # repeat n times (at most)
for j in range(n - i - 1): # shrink the window each pass
if arr[j] > arr[j + 1]: # neighbors out of order?
arr[j], arr[j + 1] = arr[j + 1], arr[j] # swap them
return arr
data = [5, 2, 8, 1, 9]
print(bubble_sort(data)) # [1, 2, 5, 8, 9]Selection Sort
Selection Sort takes a different approach. On each pass it scans the unsorted portion of the list to find the smallest element, then places that element at the front of the unsorted portion. The sorted section grows by one item after every pass.
Steps for one pass:
- Find the minimum value in the entire list.
- Swap it with the element at index 0.
- Find the minimum value in the remaining list (index 1 onward).
- Swap it with the element at index 1.
- Continue until the whole list is sorted.
def selection_sort(arr):
n = len(arr)
for i in range(n): # position to fill next
min_index = i # assume current position is minimum
for j in range(i + 1, n): # scan the rest of the list
if arr[j] < arr[min_index]: # found something smaller?
min_index = j # update the minimum index
arr[i], arr[min_index] = arr[min_index], arr[i] # place minimum
return arr
data = [5, 2, 8, 1, 9]
print(selection_sort(data)) # [1, 2, 5, 8, 9]Interactive Sorting Demo
Try to predict how the algorithms will sort the list before watching the animation. Notice how Bubble Sort makes many small swaps, while Selection Sort makes fewer but larger jumps.