Week 10 of 16

Read + Jupyter: Exception Hierarchy & Logging

Practice every exception type and set up a dual-handler logger that writes to both file and console

Day 47 60 minutes Read + Experiment

Day 47 of 80

Read First (15 minutes)

Before opening Jupyter, read the Real Python logging guide through the section titled "Formatting the Output." This gives you the vocabulary you need for the cells below.

Real Python: Logging in Python — read through "Formatting the Output" (~15 min)

What to Look For

Pay attention to the %(asctime)s, %(levelname)s, and %(message)s format strings. These are the building blocks for log output. You'll use them in Cell 4 and Cell 5 today.

Jupyter Practice (45 minutes)

Open a new Jupyter notebook. Create each cell below, run it, and read the output carefully. Don't just copy and run — understand what each line does before moving on.

Cell 1 — Three Common Exception Types

Python has a hierarchy of built-in exceptions. These three come up constantly in real code. Notice how each except clause catches only its specific type.

Cell 1 Python
# ValueError — you gave a function a value of the right type but wrong value
try:
    number = int("not a number")
except ValueError as e:
    print(f"ValueError: {e}")

# KeyError — you asked for a dict key that doesn't exist
try:
    data = {"platform": "Kling"}
    print(data["mood"])
except KeyError as e:
    print(f"KeyError: {e}")

# TypeError — you used the wrong type entirely
try:
    result = "hello" + 5
except TypeError as e:
    print(f"TypeError: {e}")
Each try/except here is independent. Notice Python's error messages are actually quite readable — they tell you exactly what went wrong. The as e part captures the exception object, which you can convert to a string to get the message.

Cell 2 — Full try/except/else/finally with File Loading

This is a realistic pattern you'll use constantly — safely loading a JSON file with full error handling. Run it twice: once with a file that doesn't exist (just use any path), and once pointing it at a real JSON file.

Cell 2 Python
import json

def safe_load(path):
    try:
        with open(path, "r") as f:
            data = json.load(f)
    except FileNotFoundError:
        print(f"  File not found: {path}")
        return []
    except json.JSONDecodeError as e:
        print(f"  Corrupt JSON: {e}")
        return []
    else:
        print(f"  Loaded {len(data)} items")
        return data
    finally:
        print(f"  (finished attempt)")

# Test with a file that doesn't exist
result = safe_load("ghost.json")
print(f"Result: {result}")
The finally block always runs — you'll see "(finished attempt)" printed even when the file isn't found. The else block only runs when json.load() succeeds. Two different except clauses handle two different failure modes — a missing file vs. a corrupted file.

Cell 3 — Custom Exception Hierarchy

You can create your own exception classes that inherit from the built-in Exception. This lets calling code be precise about what kind of error occurred. Preview of what you'll build tomorrow.

Cell 3 Python
# Base class — all Prompt Vault errors inherit from this
class PromptVaultError(Exception):
    pass

# Specific error — inherits from PromptVaultError
class InvalidPlatformError(PromptVaultError):
    pass

# Python catches the most specific matching type
try:
    raise InvalidPlatformError("Pika is not supported")
except InvalidPlatformError as e:
    print(f"Platform error: {e}")   # this one runs
except PromptVaultError as e:
    print(f"Vault error: {e}")      # this would run for other PromptVaultErrors

# Test: raise the base class instead
try:
    raise PromptVaultError("Something generic went wrong")
except InvalidPlatformError as e:
    print(f"Platform: {e}")          # won't run
except PromptVaultError as e:
    print(f"Vault: {e}")             # this runs
Python checks except clauses from top to bottom and runs the first matching one. Put specific exceptions before general ones — if you put PromptVaultError first, it would catch InvalidPlatformError too, because InvalidPlatformError IS a PromptVaultError.

Cell 4 — Logging Basics

Set up a logger with the standard format and log something at every level. Watch what appears vs. what gets filtered out.

Cell 4 Python
import logging

# basicConfig sets up the root logger — call it once at startup
logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%H:%M:%S"
)

# Get a named logger — use __name__ in real modules
logger = logging.getLogger("prompt_vault")

logger.debug("Loading prompts from disk")
logger.info("Loaded 12 prompts")
logger.warning("Platform 'Pika' not recognized")
logger.error("Failed to save prompts.json")
logger.critical("Cannot find config — shutting down")
The format string uses %(asctime)s for timestamp, %(levelname)s for the level like DEBUG or WARNING, and %(message)s for your actual message. Named loggers let you trace which module a message came from in a multi-file app.

Cell 5 — Logging to File and Console Simultaneously

The real pattern: DEBUG and above go to a log file (for later analysis), WARNING and above appear in the console (for immediate attention). Two handlers, different levels.

Cell 5 Python
import logging

logger = logging.getLogger("vault_file")
logger.setLevel(logging.DEBUG)          # logger accepts everything

# File handler — writes DEBUG and above to vault.log
file_handler = logging.FileHandler("vault.log")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
    "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
))

# Console handler — prints WARNING and above to terminal
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING)
console_handler.setFormatter(logging.Formatter(
    "[%(levelname)s] %(message)s"
))

logger.addHandler(file_handler)
logger.addHandler(console_handler)

logger.debug("File only — too verbose for console")
logger.info("File only — normal operation")
logger.warning("Both file and console — needs attention")
logger.error("Both file and console — something broke")
After running this cell, open vault.log in your directory — you'll see all four messages including the debug and info ones. The console only shows warning and error. This is the pattern you'll use in your real app starting Day 49.

End of Day Checklist

Tomorrow

Day 48 is a Build day. You'll create exceptions.py with a custom exception hierarchy for the Prompt Vault, then update models.py to raise those specific exceptions instead of generic ValueError. Your app will speak its own error language.