← Back to Python series
⚙️
Intermediate
datetime · pathlib · collections · itertools · random · try · except · else · finally · raise · Custom exceptions · open · read · write · with · CSV · JSON · pathlib

Weeks 4–6 — Standard Library · Exception Handling · File I/O

The Python standard library has batteries included. Learn the most useful modules: datetime for time, pathlib for files, collections for advanced data structures, and itertools for efficient looping. Write robust programs by handling errors gracefully. Learn the full try/except/else/finally pattern, re-raise exceptions, and define custom exception classes. Persist data to disk with Python's file I/O. Read and write plain text, CSV, and JSON files using context managers, the csv module, and json module.

datetimepathlibcollectionsitertoolsstdlibexceptiontryexceptraisecustom exceptionfileopencsvjsoncontext manager
Duration
⏱ 2.5 hours + 2 hours + 2.5 hours
Level
📊 Intermediate
Prerequisite
🎯 Week 3
OUTCOME
Build a file report tool using pathlib and collections.Counter / Wrap a file-reading function with proper error handling and a custom exception / Build a student grade manager that loads/saves CSV and exports JSON reports

What you'll learn

  • 1Parse and format dates with datetime
  • 2Navigate the filesystem with pathlib.Path
  • 3Use Counter, defaultdict, and deque from collections
  • 4Apply itertools.chain, groupby, combinations, permutations
  • 5Generate random data with the random module
  • 6Catch specific exceptions with except ExceptionType
  • 7Use else (runs when no exception) and finally (always runs)
  • 8Re-raise exceptions with raise
  • 9Create custom exception classes
  • 10Log exceptions and provide user-friendly error messages
  • 11Open, read, write, and append files with open()
  • 12Use context managers (with statement) for safe file handling
  • 13Parse and write CSV files with the csv module
  • 14Serialize and deserialize JSON with the json module
  • 15Navigate paths with pathlib

Week 4 — Standard Library

The Python standard library has batteries included. Learn the most useful modules: datetime for time, pathlib for files, collections for advanced data structures, and itertools for efficient looping.

1. datetime

python
from datetime import datetime, timedelta, date

now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M:%S"))

bday = date(1990, 6, 15)
today = date.today()
age = (today - bday).days // 365
print(f"Age: {age} years")

next_week = now + timedelta(days=7)
print(next_week.date())

2. pathlib

python
from pathlib import Path

p = Path(".")
for f in p.glob("*.py"):
    print(f.name, f.stat().st_size, "bytes")

data = Path("data.txt")
data.write_text("Hello, pathlib!")
print(data.read_text())
data.unlink()  # delete

3. collections

python
from collections import Counter, defaultdict, deque

# Counter: count occurrences
words = "the cat sat on the mat".split()
print(Counter(words).most_common(3))

# defaultdict: no KeyError on missing key
dd = defaultdict(list)
dd["fruits"].append("apple")

# deque: efficient append/pop at both ends
q = deque([1, 2, 3])
q.appendleft(0)
q.append(4)
print(list(q))   # [0, 1, 2, 3, 4]

4. itertools

python
from itertools import chain, combinations, permutations, groupby

print(list(chain([1,2], [3,4], [5])))
print(list(combinations("ABC", 2)))
print(list(permutations([1,2,3], 2)))

data = [("a",1),("a",2),("b",3),("b",4)]
for key, group in groupby(data, key=lambda x: x[0]):
    print(key, list(group))

When to reach for what (and the gotchas)

These modules overlap with things you could hand-roll, but the standard library version is faster, tested, and clearer. Here's the quick decision table and the trap each one hides.

NeedReach forCommon gotcha
Calendar dates / deadlinesdatetime.dateSubtracting dates gives a timedelta — read .days, don't compare strings
Date + time of daydatetime.datetimeNaive vs timezone-aware: don't mix them in subtraction
Filesystem pathspathlib.PathPath objects, not strings — wrap with str(p) only at the boundary
Counting occurrencescollections.CounterTies keep insertion order, not alphabetical
Missing-key defaultscollections.defaultdictReading a missing key CREATES it
Fast both-end queuecollections.dequeRandom indexing (q[5000]) is O(n), unlike a list
Grouping rowsitertools.groupbyOnly groups CONSECUTIVE keys — sort first
💡

Rule of thumb: if you're about to write a loop that counts, buckets, or pairs things up, there's usually a one-liner in collections or itertools that does it faster and reads better.

Common Mistakes (FAQ)

Q. datetime or date — which should I use?

Use date for calendar days (birthdays, deadlines, 'days until'); use datetime when the time of day matters. Subtracting either gives a timedelta — read .days or .total_seconds(), never compare formatted strings.

Q. defaultdict created a key I only meant to read — why?

Indexing a missing key on a defaultdict runs the factory and INSERTS it. If you only want to check, use d.get(key) or `key in d` instead of d[key].

Q. itertools.groupby returned weird/empty groups.

groupby only groups CONSECUTIVE items with the same key. Sort the data by that same key first (sorted(data, key=...)), otherwise identical keys scattered through the list become separate groups.

Q. Counter.most_common() ties aren't alphabetical — is that a bug?

No. Equal counts keep their first-seen (insertion) order in Python 3.7+. If you need a deterministic tie-break, sort with a secondary key: sorted(c.items(), key=lambda kv: (-kv[1], kv[0])).

Q. pathlib or os.path?

Prefer pathlib — it's the modern, object-oriented API (p / 'sub' / 'file.txt', p.read_text(), p.glob(...)). Reach for os.path only when an older library hands you string paths.

Week 5 — Exception Handling

Write robust programs by handling errors gracefully. Learn the full try/except/else/finally pattern, re-raise exceptions, and define custom exception classes.

1. try / except / else / finally

python
def read_int(prompt):
    try:
        return int(input(prompt))
    except ValueError as e:
        print(f"Invalid input: {e}")
        return None

try:
    with open("data.txt") as f:
        data = f.read()
except FileNotFoundError:
    print("File not found")
except PermissionError:
    print("Permission denied")
else:
    print("File read successfully")
finally:
    print("Done (always runs)")

2. Custom Exceptions

python
class ValidationError(ValueError):
    """Raised when user input fails validation."""
    def __init__(self, field, message):
        super().__init__(f"{field}: {message}")
        self.field = field

def validate_age(age):
    if not isinstance(age, int):
        raise ValidationError("age", "must be an integer")
    if age < 0 or age > 150:
        raise ValidationError("age", "must be between 0 and 150")

try:
    validate_age(-5)
except ValidationError as e:
    print(f"Validation failed — {e}")

3. Exception Hierarchy

ℹ️

BaseException → Exception → ValueError, TypeError, OSError (FileNotFoundError, PermissionError), IndexError, KeyError, etc. Catch the most specific exception you can handle.

4. Common Mistakes

  1. Bare except: (no type) catches everything including KeyboardInterrupt and SystemExit. Always specify a type.
  2. Swallowing exceptions silently: except Exception: pass hides bugs. At minimum log them.
  3. Using exceptions for normal control flow — e.g., catching ValueError to detect end of input instead of checking first.

Week 6 — File I/O

Persist data to disk with Python's file I/O. Read and write plain text, CSV, and JSON files using context managers, the csv module, and json module.

1. Text Files

python
# Write
with open("notes.txt", "w", encoding="utf-8") as f:
    f.write("Line 1\n")
    f.write("Line 2\n")

# Read all at once
with open("notes.txt", encoding="utf-8") as f:
    text = f.read()

# Read line by line (memory-efficient)
with open("notes.txt", encoding="utf-8") as f:
    for line in f:
        print(line.rstrip())
💡

Always specify encoding="utf-8" to avoid platform-dependent encoding surprises.

2. CSV Files

python
import csv

# Write CSV
with open("students.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["name","score"])
    writer.writeheader()
    writer.writerows([{"name":"Alice","score":90},{"name":"Bob","score":82}])

# Read CSV
with open("students.csv", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(row["name"], row["score"])

3. JSON Files

python
import json

data = {"name": "Alice", "scores": [90, 85, 92], "active": True}

# Serialize to file
with open("data.json", "w") as f:
    json.dump(data, f, indent=2)

# Deserialize from file
with open("data.json") as f:
    loaded = json.load(f)
print(loaded["scores"])  # [90, 85, 92]

4. Common Mistakes

  1. Opening without "with" — file may not be closed on error. Always use "with open(...) as f:".
  2. Writing CSV without newline="" → blank rows on Windows.
  3. json.dumps() returns a string; json.dump() writes to a file. Easy to mix up.

💻 Examples

Run these examples and check the output yourself.

01_file_report.py— File type report using pathlib + Counter
CODE
from pathlib import Path
from collections import Counter

path = Path(".").resolve()
all_files = [f for f in path.rglob("*") if f.is_file()]
exts = Counter(f.suffix or "(no ext)" for f in all_files)
total_size = sum(f.stat().st_size for f in all_files)

print(f"Total files: {len(all_files)}")
print(f"Total size:  {total_size/1024:.1f} KB")
print("\nTop file types:")
for ext, cnt in exts.most_common(5):
    print(f"  {ext:<12} {cnt}")
02_date_calc.py— Date arithmetic with datetime
CODE
from datetime import date, timedelta

today = date.today()
print(f"Today:      {today}")
print(f"100 days later: {today + timedelta(days=100)}")

# Days until New Year
new_year = date(today.year + 1, 1, 1)
print(f"Days to New Year: {(new_year - today).days}")
01_safe_divide.py— Division with multiple exception handling
CODE
def safe_divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        return None, "Cannot divide by zero"
    except TypeError as e:
        return None, f"Type error: {e}"
    else:
        return result, None

for a, b in [(10, 2), (10, 0), (10, "x")]:
    val, err = safe_divide(a, b)
    if err:
        print(f"{a}/{b} → Error: {err}")
    else:
        print(f"{a}/{b} = {val}")
▶ Output
10/2 = 5.0
10/0 → Error: Cannot divide by zero
10/x → Error: Type error: unsupported operand type(s) for /: 'int' and 'str'
02_custom_exc.py— Custom exception hierarchy
CODE
class AppError(Exception): pass
class DatabaseError(AppError): pass
class NetworkError(AppError): pass

def connect(host):
    if not host:
        raise NetworkError("Host cannot be empty")
    if host == "bad-host":
        raise NetworkError(f"Cannot reach {host}")
    return f"Connected to {host}"

for host in ["db.example.com", "", "bad-host"]:
    try:
        print(connect(host))
    except NetworkError as e:
        print(f"Network error: {e}")
▶ Output
Connected to db.example.com
Network error: Host cannot be empty
Network error: Cannot reach bad-host
01_text_rw.py— Read and write a log file
CODE
from datetime import datetime
LOGFILE = "app.log"

def log(msg):
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with open(LOGFILE, "a", encoding="utf-8") as f:
        f.write(f"[{ts}] {msg}\n")

def read_log():
    with open(LOGFILE, encoding="utf-8") as f:
        return f.read()

log("Application started")
log("User logged in: alice")
print(read_log())
02_grade_csv.py— Grade manager with CSV load/save
CODE
import csv
from pathlib import Path

FILE = Path("grades.csv")

def load():
    if not FILE.exists(): return []
    with FILE.open(encoding="utf-8") as f:
        return list(csv.DictReader(f))

def save(records):
    with FILE.open("w", newline="", encoding="utf-8") as f:
        w = csv.DictWriter(f, fieldnames=["name","score"])
        w.writeheader(); w.writerows(records)

records = load()
records.append({"name": "Diana", "score": "95"})
save(records)
print(load())

📝 Exercises

Try them yourself first, then open the solution to compare.

Exercise 1

Word Frequency Analyzer

Goal: Read a text file and produce a frequency report using Counter.

Requirements
  • Read file with pathlib
  • Tokenize into words (lowercase, strip punctuation)
  • Print top-10 words with counts and a bar chart (ASCII)
▶Toggle solution
SOLUTION
from pathlib import Path
from collections import Counter
import re

text = Path("sample.txt").read_text(errors="ignore").lower()
words = re.findall(r"[a-z]+", text)
top = Counter(words).most_common(10)
max_c = top[0][1]
for word, count in top:
    bar = "█" * (count * 20 // max_c)
    print(f"{word:<15} {count:5d} {bar}")
Exercise 2

Robust Input Loop

Goal: Write a function get_positive_int(prompt) that keeps asking until valid.

Requirements
  • Catches ValueError for non-integers
  • Validates that the value is positive
  • Returns the valid integer
▶Toggle solution
SOLUTION
def get_positive_int(prompt):
    while True:
        try:
            n = int(input(prompt))
            if n <= 0:
                raise ValueError("must be positive")
            return n
        except ValueError as e:
            print(f"  Invalid: {e}. Try again.")

n = get_positive_int("Enter a positive integer: ")
print(f"Got: {n}")
Exercise 3

Config File Manager

Goal: Read and write a JSON config file with default fallback.

Requirements
  • load_config(path) → dict (returns {} if file missing)
  • save_config(path, data) → writes JSON with indent=2
  • update_config(path, key, value) → load, update key, save
▶Toggle solution
SOLUTION
import json
from pathlib import Path

def load_config(path):
    p = Path(path)
    if not p.exists(): return {}
    return json.loads(p.read_text())

def save_config(path, data):
    Path(path).write_text(json.dumps(data, indent=2))

def update_config(path, key, value):
    cfg = load_config(path)
    cfg[key] = value
    save_config(path, cfg)

update_config("config.json", "theme", "dark")
print(load_config("config.json"))
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗

Comments 0

No comments yet. Be the first.

Only members can comment.

Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy