Python Exceptions#

Core Hierarchy#

The relevant hierarchy is:

BaseException
├── KeyboardInterrupt
├── SystemExit
├── GeneratorExit
└── Exception
    ├── ValueError
    ├── TypeError
    ├── KeyError
    ├── RuntimeError
    └── ...

BaseException is the root of Python’s exception hierarchy.

Exception is the base class for ordinary application errors.

Why Application Code Catches Exception#

Normal application code should catch a specific subclass of Exception:

try:
    quantity = int(raw_quantity)
except ValueError as exc:
    raise ValueError("quantity must be an integer") from exc

When several expected failures share the same recovery behaviour:

try:
    quantity = int(raw_quantity)
except (TypeError, ValueError) as exc:
    raise ValueError("invalid quantity") from exc

A broad Exception handler may be appropriate at a controlled application boundary:

try:
    run_trade_job()
except Exception:
    logger.exception("Trade job failed")
    return 1

This catches ordinary application failures while allowing shutdown and user interruption to propagate.

Why Not Catch BaseException?#

KeyboardInterrupt and SystemExit inherit directly from BaseException, not from Exception.

Therefore:

try:
    run_service()
except BaseException:
    logger.exception("Failure")

also catches:

  • Ctrl+C through KeyboardInterrupt;

  • sys.exit() through SystemExit;

  • generator shutdown through GeneratorExit.

Suppressing these exceptions can prevent a process from terminating normally.

A bare handler has similarly broad behaviour:

try:
    run_service()
except:
    ...

Avoid both forms in normal application code.

Cleanup#

Do not catch BaseException merely to release a resource.

Use finally:

connection = open_connection()

try:
    process_trades(connection)
finally:
    connection.close()

Or preferably use a context manager:

with open_connection() as connection:
    process_trades(connection)

The cleanup runs while the original exception continues to propagate.

Rare BaseException Use#

Low-level infrastructure may occasionally catch BaseException to perform mandatory cleanup.

It should immediately re-raise:

try:
    run_worker()
except BaseException:
    release_native_resources()
    raise

Suppressing it would interfere with shutdown and interruption.

Exception Chaining#

Translate a low-level exception into a domain exception while preserving the cause:

class TradeNotFoundError(Exception):
    pass


def get_trade(trade_id: str) -> Trade:
    try:
        return trades[trade_id]
    except KeyError as exc:
        raise TradeNotFoundError(
            f"Unknown trade_id: {trade_id}"
        ) from exc

This gives callers a stable domain-level exception while preserving the original traceback.

Custom Exceptions#

Normal application exceptions should inherit from Exception:

class TradeError(Exception):
    pass


class DuplicateTradeError(TradeError):
    pass

Do not inherit ordinary application errors directly from BaseException.

Interview Answer#

A concise interview answer is:

BaseException is the root of all Python exceptions. Exception is the base class for ordinary application failures. Application code should normally catch a specific Exception subclass, or occasionally Exception at a system boundary. It should not normally catch BaseException because that would also intercept KeyboardInterrupt, SystemExit, and GeneratorExit. Use finally or context managers for cleanup, and use raise ... from exc when translating exceptions.