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) 3

Python 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: 75

List 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 above

File 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 equal

The 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):

  • False
  • 0 and 0.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") # prints

Nested 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")  # pass

Interactive 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 forever

Use 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 = 7

Defining 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))  # 26

Default 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 style

Interactive 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 key

Deleting 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.9

Iterating 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:

  1. Compare element at index 0 with element at index 1. Swap if out of order.
  2. Move to index 1 and index 2. Swap if out of order.
  3. Continue until the end of the unsorted portion.
  4. 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:

  1. Find the minimum value in the entire list.
  2. Swap it with the element at index 0.
  3. Find the minimum value in the remaining list (index 1 onward).
  4. Swap it with the element at index 1.
  5. 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.