🎉 Welcome to PyVerse! Start Learning Today

Error Handling and Logging

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.5

Custom 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 score

4) 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 None

Logging 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 None

Practical 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,89

2) 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: catch ValueError or FileNotFoundError, not a bare except that hides everything.
  • Don't swallow errors silently. At least log them with context.
  • Use meaningful messages when raising exceptions.
  • Use finally (or with) 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.handlers first.

Summary

  • Exceptions help your programs fail safely and clearly.
  • try/except/else/finally give 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.

Loading quizzes...