Python exception types describe what went wrong and determine which except block can handle it. Most application errors inherit from Exception; termination and cancellation signals may instead inherit directly from BaseException. Catch the narrowest type whose outcome you can handle, and let other failures reach a boundary that can report or terminate the work.
The official built-in exception hierarchy is the authoritative reference. This guide highlights the branches developers encounter most often and shows how to handle them without concealing failures.
Read the hierarchy before choosing a handler
Every Python exception derives from BaseException. Its important direct branches include Exception, KeyboardInterrupt, SystemExit, GeneratorExit, and, in Python 3.11+, BaseExceptionGroup. Exception contains ordinary application and library failures; a handler for a parent class also catches its subclasses. That is why except OSError handles FileNotFoundError and PermissionError, and except LookupError handles KeyError and IndexError. It is also why handler order matters: place a specific subclass before its parent.
except Exception is suitable at a deliberate outer boundary that must record a failed job, return a sanitized HTTP error, or clean up before ending work. It is usually too broad around a single file open or conversion. except BaseException and bare except: also catch KeyboardInterrupt and SystemExit, which can obstruct normal shutdown. Python's tutorial on exceptions explains the matching and propagation rules.
One important case lives outside the ordinary Exception branch: asyncio.CancelledError has derived from BaseException since Python 3.8. If an async task catches it for cleanup, it should normally re-raise it so cancellation can propagate. See the asyncio exception reference.
Common Python exception types by problem
Use the concrete type and the operation that raised it as evidence; a type alone is not a root cause. These groups follow the built-in reference:
- Lookup:
KeyErrormeans a mapping key was not found;IndexErrormeans a sequence subscript is outside its range. Both are subclasses ofLookupError. A missing optional key may be handled with a default, while a missing required key should usually remain an error. - Arguments and conversion:
TypeErrorcommonly indicates an unsupported operand or argument type.ValueErrormeans a value has the right type but is inappropriate for the operation;int("abc")raises it. A parser at an input boundary can translate these into a documented validation result. - Attributes and names:
AttributeErrorconcerns an attribute reference or assignment;NameErrorconcerns an unbound name in a scope. Do not turn these into generic “not found” results without checking whether they indicate a programming defect. - Files, permissions, and connections:
OSErrorcovers operating-system failures. Useful subclasses includeFileNotFoundError,PermissionError,FileExistsError,IsADirectoryError,ConnectionRefusedError, andTimeoutError. A retry decision must depend on the operation's safety and the actual failure, not merely on the parent class. - Imports:
ModuleNotFoundErroris a subclass ofImportError. A missing optional dependency might justify a fallback; a required dependency usually means the program cannot start correctly. - Arithmetic:
ZeroDivisionErrorandOverflowErrorderive fromArithmeticError. They often signal invalid inputs or an incorrect assumption, not a condition to ignore. - Runtime and assertions:
RuntimeErrorcovers errors that do not fit a more specific category;NotImplementedErroris its subclass for certain unimplemented methods.AssertionErrorresults from a failedassertand should not be used as the normal validation contract for untrusted input. - Syntax and indentation:
SyntaxErrorandIndentationErrorarise while parsing code. Fix the program or generated source; a runtime handler around unrelated application work cannot repair invalid Python syntax.
Library-defined exceptions also matter. A database or HTTP client may expose a more meaningful hierarchy than any built-in type. Read that library's documented contract and catch its specific exception at the boundary where recovery is possible. Do not assume every timeout, network failure, or server error is safe to retry; a request may have completed despite a lost response.
Catch one type or several types deliberately
Name multiple exception classes in a tuple when they lead to the same handling decision. Use separate handlers when the response differs:
def parse_port(raw: str) -> int:
try:
port = int(raw)
except (TypeError, ValueError) as exc:
raise ValueError("port must be an integer") from exc
if not 1 <= port <= 65535:
raise ValueError("port is outside the valid range")
return port
Both conversion failures become one stable API contract. raise ... from exc preserves the original as an explicit cause for internal diagnosis; do not show the chain or raw exception text to an HTTP client. The exception-chaining documentation describes the __cause__ relationship.
By contrast, a missing optional file and an unreadable required file are not interchangeable:
from pathlib import Path
def load_optional_note(path: Path) -> str | None:
try:
return path.read_text(encoding="utf-8")
except FileNotFoundError:
return None
PermissionError and other OSError subclasses still propagate. That is intentional: returning None for an unreadable file would make a real failure look like an absent optional note. The type annotation uses Python 3.10+ union syntax; use Optional[str] for older supported versions.
Avoid except (KeyError, IndexError, TypeError): pass merely because a function might raise one of them. It can conceal a coding mistake in the try block. Keep the try block as small as practical, and decide what each caught failure means at that operation.
Use else, finally, and context managers for clear control flow
else runs only when the try block completes without an exception. finally runs on normal completion and on exception propagation; it is for cleanup, not for replacing the outcome. A with statement is usually clearer for files, locks, and other resources with context-manager support.
def reciprocal_from_text(raw: str) -> float:
try:
value = float(raw)
except ValueError as exc:
raise ValueError("a numeric value is required") from exc
else:
return 1.0 / value
Here, ZeroDivisionError from the calculation is not caught by the ValueError handler. Putting only the conversion in try keeps that distinction visible. If zero is an expected invalid input, validate it explicitly and return the function's documented error.
When cleanup is required regardless of success, use finally without suppressing the original failure:
def use_resource(resource):
try:
return resource.read()
finally:
resource.close()
In real code, prefer with resource: if the object implements the context-manager protocol. Be aware that cleanup itself can fail and alter what propagates; test that behavior for critical resources. The Python tutorial covers else, finally, and cleanup semantics.
Translate failures without losing their cause
A service boundary may need to translate a low-level error into a domain-specific one. Define a custom exception derived from Exception, give it a stable meaning, and chain the original failure. Avoid multiple inheritance from built-in exception types; the Python reference warns about implementation incompatibilities.
class ConfigurationError(Exception):
"""The required local configuration cannot be read."""
def load_configuration(path):
try:
return path.read_text(encoding="utf-8")
except OSError as exc:
raise ConfigurationError("configuration is unavailable") from exc
This translation makes sense if callers care about a single configuration contract. It would be less useful if they need to distinguish missing, forbidden, and transient I/O errors. A custom class should expose a useful decision, not merely rename Exception.
Inside a handler, a bare raise re-raises the active exception with its traceback. Use it when the current layer cannot recover but must perform a narrow cleanup or add context. Do not log a full traceback at every layer and then re-raise; one failure becomes several duplicate events. Let the boundary that owns the final outcome log it once.
Handle exception groups in Python 3.11 and later
Concurrent work can fail in more than one place. Python 3.11 introduced ExceptionGroup, BaseExceptionGroup, and except*. ExceptionGroup contains only Exception instances and itself derives from Exception; BaseExceptionGroup can contain direct BaseException subclasses. An except* clause matches the relevant subgroup by contained exception type, while unmatched failures continue to propagate. See the official group reference.
def validate_batch():
raise ExceptionGroup(
"batch validation failed",
[ValueError("invalid count"), TypeError("invalid label")],
)
try:
validate_batch()
except* ValueError as group:
print(f"value failures: {len(group.exceptions)}")
except* TypeError as group:
print(f"type failures: {len(group.exceptions)}")
This example handles both subgroups. In production, use the groups to make a deliberate batch decision, not to print and silently accept a partially failed operation. Do not mix except and except* clauses in the same try; use an outer boundary if the whole group also needs handling. Avoid flattening a group to one string when individual failures affect different records or recovery actions.
asyncio.TaskGroup can raise an exception group when child tasks fail, but cancellation has its own semantics. asyncio.CancelledError is not caught by except Exception, and code that catches it for cleanup should normally re-raise it. This protects structured-concurrency shutdown behavior rather than turning cancellation into a successful result.
Log failures once, with safe context
Python's logging module emits records through handlers and formatters. logger.exception() adds exception information at ERROR level and is intended for use inside an exception handler. logger.error(..., exc_info=True) also logs at ERROR with exception information; use logger.log(level, ..., exc_info=True) if a different level is appropriate. The logging API reference documents the behavior.
import logging
logger = logging.getLogger(__name__)
def process_job(job):
try:
job.run()
except Exception:
logger.exception("job failed", extra={"job_kind": "import"})
raise
Use this pattern only when this function is the boundary responsible for the failure record; otherwise let its caller log once. The example's job_kind is a fixed, non-sensitive category. Real jobs may require a safe pseudonymous run ID and an allowlist of fields. Exception messages, arguments, tracebacks, file paths, and extra values can contain credentials or personal information; review what the formatter emits and restrict access and retention. Do not log request bodies, tokens, raw environment variables, or user identifiers by default.
A log line does not prove a failure was recovered, and an absent error line does not prove success. Test the expected outcome and the logging path separately. For a handled validation error, assert the public result and that no sensitive input is emitted. For an unexpected work-unit failure, assert that it propagates or produces the intended failure status and one owned log record. For finally, test both success and failure cleanup paths; for grouped failures, test that unmatched subgroups still propagate.
If Python logs are already emitted to stdout or another supported source, a collector can deliver them to a logs-focused backend. Fluxtail's paid Starter and Pro plans can help teams search and filter received logs and inspect recent records with Live Tail. It does not catch Python exceptions by itself or replace application error handling, tracing, or APM. Verify the collector's field mapping and delivery with a safe test marker before relying on a filter during an incident.