Exception Testing#

Expected Exceptions#

Use pytest.raises when an exception is the expected result of an operation:

import pytest


with pytest.raises(ValueError):
    validate_quantity(0)

The test fails if the expected exception is not raised.

Use the narrowest appropriate exception type. Testing against a broad Exception can allow an unrelated failure to satisfy the test.

Inspecting the Exception#

The context-manager form can expose the captured exception:

with pytest.raises(OverfillError) as exc_info:
    apply_fill(order, fill)

assert exc_info.value.order_id == "ORD-001"

exc_info is a pytest ExceptionInfo object rather than the exception itself.

The actual exception object is available through:

exc_info.value

For several assertions, assigning the exception to a descriptive local name is often clearer:

with pytest.raises(OverfillError) as exc_info:
    apply_fill(order, fill)

error = exc_info.value

assert error.order_id == "ORD-001"
assert error.filled_quantity == 0
assert error.incoming_quantity == 200
assert error.original_quantity == 100

The assignment is only for readability. Accessing exc_info.value directly has the same meaning.

Control Flow#

Normal exception and context-manager control flow still applies inside pytest.raises.

Consider:

with pytest.raises(OverfillError) as exc_info:
    apply_fill(order, fill)

assert exc_info.value.order_id == "ORD-001"

When apply_fill raises the expected exception:

  1. normal execution of the with suite stops immediately;

  2. pytest.raises handles the expected exception;

  3. the with statement completes;

  4. execution continues after the with statement.

Code after the raising operation inside the same with suite is therefore not executed:

with pytest.raises(OverfillError) as exc_info:
    apply_fill(order, fill)

    # Unreachable when apply_fill() raises.
    assert exc_info.value.order_id == "ORD-001"

Assertions about the captured exception should normally be placed after the with block.

Structured Exception Assertions#

Domain exceptions can carry structured diagnostic information:

with pytest.raises(OverfillError) as exc_info:
    apply_fill(order, fill)

error = exc_info.value

assert error.order_id == "ORD-001"
assert error.filled_quantity == 0
assert error.incoming_quantity == 200
assert error.original_quantity == 100

Prefer asserting stable domain data when those attributes are part of the exception contract.

The message can also be tested:

assert "would overfill order" in str(error)

When only the message needs to be matched, pytest.raises supports match:

with pytest.raises(
    OverfillError,
    match="would overfill order",
):
    apply_fill(order, fill)

Exception Type Matching#

pytest.raises accepts the specified exception type and its subclasses.

For example:

class TradeError(Exception):
    pass


class OverfillError(TradeError):
    pass


with pytest.raises(TradeError):
    raise OverfillError()

This passes because OverfillError is a subclass of TradeError.

Prefer testing the most specific exception that represents the intended contract unless the abstraction being tested deliberately exposes the base exception type.

Interview Notes#

pytest.raises verifies that an operation fails in the expected way.

In context-manager form:

  • the expected exception interrupts the remaining statements in the block;

  • pytest handles that exception;

  • execution continues after the block;

  • exc_info.value contains the actual exception object.

The important distinction is between an exception being raised and an exception remaining unhandled. An unhandled exception terminates the current normal execution path; a handled exception can allow execution to continue at the appropriate point.