Practice every exception type and set up a dual-handler logger that writes to both file and console
Day 47 of 80
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)
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.
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.
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.
# 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}")
as e part captures the exception object, which you can convert to a string to get the message.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.
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}")
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.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.
# 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
PromptVaultError first, it would catch InvalidPlatformError too, because InvalidPlatformError IS a PromptVaultError.Set up a logger with the standard format and log something at every level. Watch what appears vs. what gets filtered out.
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")
%(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.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.
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")
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.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.