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.
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
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
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() # delete3. collections
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
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.
| Need | Reach for | Common gotcha |
|---|---|---|
| Calendar dates / deadlines | datetime.date | Subtracting dates gives a timedelta — read .days, don't compare strings |
| Date + time of day | datetime.datetime | Naive vs timezone-aware: don't mix them in subtraction |
| Filesystem paths | pathlib.Path | Path objects, not strings — wrap with str(p) only at the boundary |
| Counting occurrences | collections.Counter | Ties keep insertion order, not alphabetical |
| Missing-key defaults | collections.defaultdict | Reading a missing key CREATES it |
| Fast both-end queue | collections.deque | Random indexing (q[5000]) is O(n), unlike a list |
| Grouping rows | itertools.groupby | Only 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
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
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
- Bare except: (no type) catches everything including KeyboardInterrupt and SystemExit. Always specify a type.
- Swallowing exceptions silently: except Exception: pass hides bugs. At minimum log them.
- 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
# 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
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
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
- Opening without "with" — file may not be closed on error. Always use "with open(...) as f:".
- Writing CSV without newline="" → blank rows on Windows.
- json.dumps() returns a string; json.dump() writes to a file. Easy to mix up.
💻 Examples
Run these examples and check the output yourself.
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}")
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}")
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}")
10/2 = 5.0
10/0 → Error: Cannot divide by zero
10/x → Error: Type error: unsupported operand type(s) for /: 'int' and 'str'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}")
Connected to db.example.com
Network error: Host cannot be empty
Network error: Cannot reach bad-hostfrom 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())
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.
Word Frequency Analyzer
Goal: Read a text file and produce a frequency report using Counter.
- Read file with pathlib
- Tokenize into words (lowercase, strip punctuation)
- Print top-10 words with counts and a bar chart (ASCII)
▶Toggle 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}")
Robust Input Loop
Goal: Write a function get_positive_int(prompt) that keeps asking until valid.
- Catches ValueError for non-integers
- Validates that the value is positive
- Returns the valid integer
▶Toggle 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}")
Config File Manager
Goal: Read and write a JSON config file with default fallback.
- 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
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"))
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