Exceptions cheatsheet.

Basic

try:
    result = risky()
except ValueError as e:
    log.error("bad value", error=str(e))
except (KeyError, TypeError) as e:
    log.error("type/key error")
except Exception:
    log.exception("unexpected")     # logs with stack trace
    raise
finally:
    cleanup()

try/except/else

try:
    result = risky()
except Error:
    handle()
else:
    # runs if no exception
    use(result)

else clarifies “use(result) only on success.”

Raise

raise ValueError("bad input")
raise ValueError("bad input") from original_exc

from: chains exceptions. Output shows both.

try:
    parse(...)
except ValueError as e:
    raise BusinessError("can't parse user") from e

Suppressing original

raise ValueError("new") from None

Hides the original. Use when you’ve handled it but want a clean stack trace.

Re-raise

try:
    ...
except Exception:
    log.exception("...")
    raise                       # re-raises current

Custom exception hierarchy

class AppError(Exception):
    """Base for all app errors."""

class NotFoundError(AppError): ...
class ValidationError(AppError): ...
class AuthError(AppError): ...
class PermissionError(AuthError): ...

# Usage
try:
    ...
except AppError as e:
    # catches all app errors
    pass
except PermissionError:
    # more specific catch goes first
    pass

Exception arguments

class HttpError(Exception):
    def __init__(self, status: int, message: str):
        self.status = status
        self.message = message
        super().__init__(f"{status}: {message}")

raise HttpError(404, "not found")

ExceptionGroup (3.11+)

try:
    async with asyncio.TaskGroup() as tg:
        tg.create_task(a())
        tg.create_task(b())
except* ValueError as eg:
    log.error("value errors", count=len(eg.exceptions))
except* TypeError as eg:
    log.error("type errors")

except* matches inside the group. Multiple except* can run.

Construct manually

group = ExceptionGroup("multi-error", [ValueError("a"), KeyError("b")])
raise group

with suppress

from contextlib import suppress

with suppress(FileNotFoundError, PermissionError):
    os.remove("file.txt")

Cleaner than try/except for “ignore these errors.”

Assertion (not for prod errors!)

assert x > 0, "x must be positive"

assert is stripped with python -O. Use for internal invariants, NOT input validation.

Common exceptions

Exception
  ArithmeticError
    ZeroDivisionError
  LookupError
    KeyError
    IndexError
  ValueError
  TypeError
  AttributeError
  NameError
  FileNotFoundError
  PermissionError
  OSError
  IOError (alias for OSError)
  TimeoutError
  RuntimeError
  StopIteration
  StopAsyncIteration
  KeyboardInterrupt
  SystemExit

KeyboardInterrupt and SystemExit don’t inherit from Exception — they inherit from BaseException.

except: vs except Exception:

try: ...
except: ...                 # BAD: catches KeyboardInterrupt, SystemExit
except Exception: ...       # GOOD: catches everything app-level

Never bare except: — you’ll block Ctrl+C.

Logging exceptions

try:
    ...
except Exception:
    log.exception("operation failed", extra={"context": "..."})

log.exception automatically captures the current exception’s stack trace. Use only inside except.

Chaining

try:
    a()
except ValueError as e:
    try:
        fallback()
    except KeyError as e2:
        raise RuntimeError("both failed") from e2
        # Shows "while handling KeyError, RuntimeError was raised"

cause vs context

  • raise X from Y sets __cause__ = Y — explicit chain.
  • Exception raised in another except sets __context__ — implicit chain.

Try/finally pattern (no except)

try:
    resource = acquire()
    use(resource)
finally:
    release(resource)

Equivalent to with for resources.

Async exceptions

Same syntax in async functions:

async def handler():
    try:
        await fetch()
    except httpx.TimeoutException:
        ...

CancelledError needs care:

async def worker():
    try:
        await long_op()
    except asyncio.CancelledError:
        await cleanup()
        raise               # re-raise!

Custom str

class ApiError(Exception):
    def __init__(self, status, body):
        self.status, self.body = status, body
    
    def __str__(self):
        return f"API {self.status}: {self.body}"

Common mistakes

  • Bare except: — catches KeyboardInterrupt; user can’t kill process.
  • Swallowing exceptions silently — no log, no re-raise.
  • assert for user input — stripped with -O.
  • raise Exception("...") — too generic; create specific types.
  • Catching too broadly then handling some cases — others silently lost.

Read this next

If you want my exception hierarchy + error envelope library, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .