Redirect Chapter 4: Modules, Files & Error Handling | AI Fundamentals
← Back to Tutorials Chapter 4

Modules, Files & Error Handling

Real programs are never one giant file and never crash silently. This chapter covers three survival skills: splitting code into reusable modules, reading and writing files, and handling errors gracefully.

Importing Modules and Packages

A module is a single Python file holding related functions. A package is a folder of modules. Importing lets you reuse other people's code (and your own) without rewriting it.

Import styles

import math                      # whole module
from math import sqrt, pi        # specific names
import numpy as np               # alias for short usage

Use import module when you want several names or clarity about the source, from module import name when you use one thing constantly, and aliases for long names you type often.

What is __name__?

Every module gets a built-in variable called __name__. When you run a file directly, it is "__main__"; when another file imports it, it is the module's own name. This lets one file act as both a library and a runnable script:

def double(x):
    return x * 2

if __name__ == "__main__":
    print(double(21))   # only runs when this file is executed directly
Why this matters: importing a module executes its top-level code. Guarding test code behind if __name__ == "__main__" prevents side effects when other files import it.

Standard Library Tour

The standard library ships with Python. These are the modules you will reach for constantly:

import os, sys, math, random
from collections import Counter

print(math.sqrt(144))              # 12.0
print(random.choice(["a", "b", "c"]))  # random pick
print(Counter("aabbbc"))           # Counter({'a': 2, 'b': 3, 'c': 1})

Package practice

Build your own tiny package. Create a folder shapes/ containing area.py with two functions, then import them elsewhere:

# shapes/area.py
def circle(r):
    return 3.14159 * r * r

def square(side):
    return side * side
# main.py - run from the folder above shapes/
from shapes.area import circle, square
print(circle(2))    # 12.56636
print(square(3))    # 9

File Operations in Python

Reading and writing text files is the bridge between your program and data on disk.

Opening files

f = open("notes.txt", "w")   # write mode
f.write("first line\n")
f.close()

Common modes: "r" read, "w" write (overwrites), "a" append, "r+" read and write.

The with statement

Forgetting close() can corrupt data. The with block closes the file automatically when the block ends:

with open("notes.txt", "a") as f:
    f.write("more text\n")

with open("notes.txt") as f:
    for line in f:
        print(line.strip())

Reading, writing, appending

with open("data.txt", "w") as f:
    f.write("1,2,3\n4,5,6\n")

with open("data.txt") as f:
    content = f.read()            # whole file as one string
print(content)
Iterating lines: for line in f reads one line at a time without loading the entire file into memory — essential when files are large.

Working with File Paths

os.path

os.path builds paths correctly on any operating system instead of relying on hard-coded slashes:

import os
path = os.path.join("data", "raw", "set.csv")
print(path)                 # data/raw/set.csv (or with backslashes on Windows)
print(os.path.exists(path)) # False - no such file yet

pathlib

The modern pathlib module treats paths as objects with useful methods:

from pathlib import Path
p = Path("data") / "raw" / "set.csv"
print(p.name)        # set.csv
print(p.parent)      # data\raw
print(p.suffix)      # .csv

File handling assignments

  1. Write a program that reads a text file and prints the number of lines, words, and characters it contains.
  2. Append today's date to the end of a log file using datetime and the append mode.
  3. Read a CSV-style text file and print the sum of all the numbers in its last column.

Exception Handling: try, except, else, finally

Errors are inevitable — files vanish, users type numbers as text, networks drop. Exception handling lets a program detect a problem and react instead of stopping abruptly.

try:
    number = int(input("Enter a number: "))
    print(100 / number)
except ValueError:
    print("That was not a valid number.")
except ZeroDivisionError:
    print("Cannot divide by zero.")
else:
    print("The calculation succeeded.")
finally:
    print("Cleanup runs no matter what.")
Be specific: catch ValueError or ZeroDivisionError rather than a bare except:. A blanket handler hides bugs and makes debugging painful.

Exception practice

  1. Write a function that reads a file and returns "missing" when FileNotFoundError is raised, instead of crashing.
  2. Wrap a JSON-loading call in try/except so a malformed json.JSONDecodeError produces a friendly message.
  3. Use finally to ensure a file handle is closed even when a write inside the try block fails.
Exercise: Build a mini journal program. It should append a timestamped line to journal.txt using with and datetime, then read the whole file back and display it. Wrap the input handling in try/except so an empty entry is politely rejected instead of crashing the program.