Error Handling and Logging
Goal: Learn how to make your Python programs safer and easier to debug using exceptions (error handling) and the logging module.
What you'll be able to do:
- Predict and handle common errors without crashing your app.
- Create and raise your own exceptions with helpful messages.
- Record what your program is doing using logs (with timestamps, levels, and files).
- Build a small data-processing tool that handles bad input gracefully and logs what happened.
1) Errors and Exceptions (What and Why)
- Syntax error: Python can't read your code (e.g., missing colon).
- Runtime error (Exception): Code is syntactically correct, but something goes wrong during execution (e.g., dividing by zero, bad file path).
- Logic error: Code runs, but does the wrong thing (harder to detect).
We handle runtime errors with try/except so our program can recover or fail nicely.
Example: Safe integer conversion
def to_int(s):
try:
return int(s)
except ValueError:
# int() fails for strings like "hello"
return None
print(to_int("42")) # 42
print(to_int("hi")) # None (no crash)2) try, except, else, finally
try: Code that might fail.except: What to do if it fails.else: Runs only if no exception happened.finally: Runs no matter what (great for cleanup).
def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError as e:
print("Can't divide by zero:", e)
return None
else:
print("Division successful")
return result
finally:
print("Done dividing (cleanup happens here).")
safe_divide(10, 2)
safe_divide(10, 0)3) Raising and Creating Exceptions
Sometimes you should signal that something is wrong.
Raise a built-in exception:
def sqrt(n):
if n < 0:
raise ValueError("n must be non-negative")
return n ** 0.5Custom exception:
class InvalidScoreError(Exception):
"""Raised when a score is not a number or out of range."""
pass
def parse_score(s):
try:
score = float(s)
except ValueError as e:
# Keep the original cause using "from e"
raise InvalidScoreError(f"Score {s!r} is not a number") from e
if not 0 <= score <= 100:
raise InvalidScoreError(f"Score {score} must be between 0 and 100")
return score4) Logging (Your Program's Diary)
Why not just print?
- print disappears unless you're watching the terminal.
- Logging gives timestamps, levels (how important), and can write to files automatically.
Common levels:
- DEBUG: Things that help you debug.
- INFO: High-level steps ("Starting app…").
- WARNING: Something odd but not fatal.
- ERROR: Something failed; app may continue.
- CRITICAL: The app is in trouble.
Basic logging
import logging
logging.basicConfig(
level=logging.INFO, # show INFO and above
format="%(asctime)s | %(levelname)s | %(message)s"
)
logging.debug("This is hidden unless level <= DEBUG")
logging.info("Program started")
logging.warning("Low disk space")
logging.error("Something went wrong")Per-module logger (recommended)
import logging
logger = logging.getLogger(__name__) # __name__ = module name
def load_config(path):
try:
with open(path, "r", encoding="utf-8") as f:
data = f.read()
logger.info("Loaded config from %s", path)
return data
except FileNotFoundError:
logger.error("Config file not found: %s", path)
return None
except Exception:
# Includes full stack trace automatically
logger.exception("Unexpected error while reading %s", path)
return NoneLogging to a file and the console
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger("app")
logger.setLevel(logging.DEBUG) # logger's level
# Console handler
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# File handler with rotation (keeps last 3 files, 100 KB each)
fh = RotatingFileHandler("app.log", maxBytes=100_000, backupCount=3, encoding="utf-8")
fh.setLevel(logging.DEBUG)
# Formatter (controls how logs look)
fmt = logging.Formatter("%(asctime)s | %(levelname)s | %(name)s | %(message)s")
ch.setFormatter(fmt)
fh.setFormatter(fmt)
# Avoid adding handlers twice if this code runs multiple times (e.g., in notebooks)
if not logger.handlers:
logger.addHandler(ch)
logger.addHandler(fh)
logger.info("Logger is ready.")5) Putting It Together: Logging + Exceptions
import logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
if not logger.handlers:
logger.addHandler(logging.StreamHandler())
def read_number(s):
try:
return float(s)
except ValueError:
logger.warning("Could not parse number from %r", s)
return None
def read_file(path):
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
logger.error("File not found: %s", path)
return None
except Exception:
logger.exception("Unexpected error reading %s", path)
return NonePractical Project: GradeBook Analyzer
Goal: Read a CSV file of student scores, compute stats, handle bad lines without crashing, and log everything to both console and a rotating log file.
1) Prepare a CSV file named grades.csv with some "messy" data:
Alice,95.5
Bob,hello
Carla,102
,88
David,90
Eve,77.5
Frank,-3
Grace,892) Build the script gradebook.py
import csv
import logging
from logging.handlers import RotatingFileHandler
from dataclasses import dataclass
from typing import List, Optional
# ---------- Logging setup ----------
logger = logging.getLogger("gradebook")
logger.setLevel(logging.DEBUG)
fmt = logging.Formatter("%(asctime)s | %(levelname)s | %(name)s | %(message)s")
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
ch.setFormatter(fmt)
fh = RotatingFileHandler("gradebook.log", maxBytes=100_000, backupCount=3, encoding="utf-8")
fh.setLevel(logging.DEBUG)
fh.setFormatter(fmt)
if not logger.handlers:
logger.addHandler(ch)
logger.addHandler(fh)
# ---------- Exceptions ----------
class InvalidScoreError(Exception):
pass
# ---------- Data model ----------
@dataclass
class Record:
name: str
score: float
# ---------- Core functions ----------
def parse_score(raw: str) -> float:
try:
score = float(raw)
except ValueError as e:
raise InvalidScoreError(f"Score {raw!r} is not a number") from e
if not 0 <= score <= 100:
raise InvalidScoreError(f"Score {score} is out of range [0, 100]")
return score
def load_records(csv_path: str) -> List[Record]:
records: List[Record] = []
try:
with open(csv_path, newline="", encoding="utf-8") as f:
reader = csv.reader(f)
for line_no, row in enumerate(reader, start=1):
if len(row) != 2:
logger.warning("Line %d: expected 2 columns, got %d -> %r", line_no, len(row), row)
continue
name, raw_score = row[0].strip(), row[1].strip()
if not name:
logger.warning("Line %d: missing student name -> %r", line_no, row)
continue
try:
score = parse_score(raw_score)
except InvalidScoreError as e:
logger.warning("Line %d: %s", line_no, e)
continue
records.append(Record(name=name, score=score))
logger.info("Loaded %d valid records from %s", len(records), csv_path)
return records
except FileNotFoundError:
logger.error("CSV file not found: %s", csv_path)
return []
except Exception:
logger.exception("Unexpected error while loading %s", csv_path)
return []
def compute_stats(records: List[Record]) -> Optional[dict]:
if not records:
logger.warning("No records to compute stats.")
return None
scores = [r.score for r in records]
avg = sum(scores) / len(scores)
high = max(records, key=lambda r: r.score)
low = min(records, key=lambda r: r.score)
stats = {
"count": len(records),
"average": round(avg, 2),
"highest": (high.name, high.score),
"lowest": (low.name, low.score),
}
logger.info(
"Stats -> count=%d, average=%.2f, highest=%s(%.1f), lowest=%s(%.1f)",
stats["count"], stats["average"], high.name, high.score, low.name, low.score
)
logger.debug("All scores: %s", scores)
return stats
def main():
csv_path = "grades.csv"
logger.info("Starting GradeBook Analyzer for %s", csv_path)
try:
records = load_records(csv_path)
stats = compute_stats(records)
if stats:
print("Summary")
print(f"- Count: {stats['count']}")
print(f"- Average: {stats['average']}")
print(f"- Highest: {stats['highest'][0]} ({stats['highest'][1]})")
print(f"- Lowest: {stats['lowest'][0]} ({stats['lowest'][1]})")
else:
print("No valid data to show. Check warnings in the logs.")
except Exception:
# Catch any truly unexpected error at the top level
logger.exception("Fatal error in main()")
if __name__ == "__main__":
main()3) Run it
python gradebook.py- Check the console output and the file gradebook.log.
- Edit grades.csv to add new issues (blank lines, extra commas, negative scores) and re-run. Watch how the logs explain what happened and where.
4) Optional extensions
- Write the clean records back to a new CSV (clean_grades.csv).
- Add a command-line argument for the CSV file path.
- Sort and print the top 3 students.
- Add unit tests for parse_score using pytest.
Best Practices and Common Mistakes
- Be specific in
except: catchValueErrororFileNotFoundError, not a bareexceptthat hides everything. - Don't swallow errors silently. At least log them with context.
- Use meaningful messages when raising exceptions.
- Use
finally(orwith) to clean up resources like files. - Prefer logging over print for real programs; use levels correctly (DEBUG for detailed internals, INFO for high-level events, WARNING/ERROR when things go wrong).
- Avoid adding duplicate logging handlers; check
if not logger.handlersfirst.
Summary
- Exceptions help your programs fail safely and clearly.
try/except/else/finallygive you precise control over error handling.- You can raise your own exceptions to make rules clear.
- Logging records what your program is doing, with levels, timestamps, and files, which makes debugging and maintenance much easier.
- The GradeBook Analyzer showed how to combine both: cleanly handle bad data and keep a useful log trail.