[Python Series 9] Exception Handling and Logging Strategies

한국어 버전

Even the best code runs into unexpected errors. Once your logic is structured with classes, it is time to learn how to handle those failures gracefully. Start with simply knowing "what happened," then move from print statements to deliberate exception handling, topped off with structured logging. We'll progress from core try/except patterns to custom exceptions and the Python logging module.

Key terms

  1. Exception: A runtime signal that something went wrong; handled via try/except.
  2. Traceback: The ordered record of function calls when an exception occurs.
  3. Logger: The entity in the logging module responsible for emitting messages with names and levels.
  4. Handler: A component that routes logs to destinations such as the console, files, or remote services.

Core ideas

Study memo

  • Time: 60 minutes
  • Prereqs: Experience with requests plus familiarity with functions/classes
  • Goal: Configure custom exceptions and loggers that capture precise failure context

Follow this arc to raise the difficulty naturally:

  1. Core flow: Wrap sensitive blocks in try/except.
  2. Reinforce: Add custom exceptions to keep context.
  3. Expand: Record the whole flow with logging.

When exception handling and logging align, failure points become obvious.

Code examples

Try/except basics (Core)

Try/except formalizes the "expect failure" mindset.

from requests import RequestException

try:
    response = requests.get("https://status.mathbong.com", timeout=3)
    response.raise_for_status()
except (RequestException, TimeoutError) as exc:
    print("Status check failed", exc)
else:
    print("OK", response.status_code)
finally:
    print("Clean up resources")

else runs only when no exception occurs. finally always runs, which is perfect for releases or cleanup steps.

Add context with custom exceptions (Core → Plus)

Built-in exceptions rarely describe the business context. Create named exceptions so the message alone tells a story.

class MealFetchError(RuntimeError):
    def __init__(self, school_code: str, original: Exception):
        super().__init__(f"Meal fetch failed: {school_code}")
        self.original = original


def fetch_meal(school_code: str) -> dict:
    try:
        response = requests.get(f"https://api.school/{school_code}", timeout=4)
        response.raise_for_status()
        return response.json()
    except Exception as exc:
        raise MealFetchError(school_code, exc) from exc

The from exc chain retains the original stack trace so you do not lose the root cause.

Set up logging (Core)

Replace ad-hoc prints with logs for consistent formatting and filtering.


logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s %(message)s",
)

logger = logging.getLogger("mealbot")
logger.info("Service started")

Call basicConfig once. Start with minimal metadata such as timestamps, levels, and logger names.

Structured logs and handlers (Optional)

Add structure only when you need it. JSON logs are easier to parse, and dedicated handlers send logs to multiple destinations.


class JsonFormatter(logging.Formatter):
    def format(self, record):
        payload = {
            "level": record.levelname,
            "message": record.getMessage(),
            "module": record.name,
            "extras": getattr(record, "extra", {}),
        }
        return json.dumps(payload, ensure_ascii=False)


handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())

logger = logging.getLogger("mealbot")
logger.setLevel(logging.INFO)
logger.addHandler(handler)

Separate handlers so, for example, the console shows summaries while files hold detailed traces. Skip this section until you need more destinations.

Exception logging pattern (Core)

Always log after you catch. The muscle memory is try -> except -> logger.

try:
    meal = fetch_meal("demo-high")
except MealFetchError:
    logger.exception("Failed to fetch meals", extra={"school": "demo-high"})
else:
    logger.info("Meal data received", extra={"menu_count": len(meal["menu"])})

logger.exception attaches the active stack trace, so only call it inside except blocks.

Operational tips

  • In uvicorn or gunicorn, namespace loggers per service to cut noise.
  • Filter sensitive keys (API tokens, personal data) before passing them via extra.
  • Long-running jobs should use logging.handlers.RotatingFileHandler to cap file sizes.

Why it matters

Automation scripts can fail at any point. Logging contextual data and wrapping exceptions reduce recovery time and make it easier to collaborate on fixes.

Practice

  • Follow along: Reproduce the MealFetchError chain and observe how from exc preserves the traceback.
  • Extend: Configure the JSON formatter with both console and file handlers using different formats.
  • Debug: Force a 404, catch it, and attach hints via extra inside logger.exception.
  • Done when: You can demonstrate the full flow of try/except → custom exception → structured log inside a repeatable test.

Wrap up

Solid exception handling and logging drastically cut the time needed to reproduce and fix outages. Next we will protect the execution environment with virtual environments, pyproject.toml, and uv so results stay consistent everywhere.

💬 댓글

이 글에 대한 의견을 남겨주세요