0
0
PyTesttesting~15 mins

Testing multiple exceptions in PyTest - Deep Dive

Choose your learning style9 modes available
Overview - Testing multiple exceptions
What is it?
Testing multiple exceptions means writing tests that check if your code correctly raises different types of errors in different situations. Instead of testing just one error, you verify that your code handles several possible mistakes properly. This helps ensure your program behaves safely and predictably when things go wrong.
Why it matters
Without testing multiple exceptions, bugs related to error handling can go unnoticed, causing crashes or unexpected behavior in real use. Properly testing all possible errors protects users and developers from surprises and makes software more reliable and easier to fix. It builds confidence that your code handles problems gracefully.
Where it fits
Before this, you should understand basic pytest testing and how to test a single exception. After this, you can learn about parameterized tests and advanced error handling strategies to write cleaner and more efficient tests.
Mental Model
Core Idea
Testing multiple exceptions means checking that your code raises the right error for each different wrong input or situation.
Think of it like...
It's like checking that a vending machine returns the correct error message if you press a button for a sold-out snack, insert the wrong coin, or try to open the door early.
┌───────────────────────────────┐
│          Test Function         │
├─────────────┬─────────────────┤
│ Input Case  │ Expected Error  │
├─────────────┼─────────────────┤
│ Case 1      │ Exception Type A│
│ Case 2      │ Exception Type B│
│ Case 3      │ Exception Type C│
└─────────────┴─────────────────┘
Build-Up - 6 Steps
1
FoundationBasic exception testing with pytest
🤔
Concept: Learn how to test that a single exception is raised using pytest.
In pytest, you can check if a function raises an error using the 'with pytest.raises(ExpectedException):' block. Inside this block, you call the function that should raise the error. If the error occurs, the test passes; if not, it fails. Example: import pytest def divide(a, b): return a / b def test_divide_by_zero(): with pytest.raises(ZeroDivisionError): divide(1, 0)
Result
The test passes if dividing by zero raises ZeroDivisionError; otherwise, it fails.
Understanding how to test a single exception is the foundation for testing multiple exceptions later.
2
FoundationUnderstanding multiple exceptions in code
🤔
Concept: Recognize that functions can raise different exceptions depending on input or state.
A function might raise different errors for different bad inputs. For example, a function that processes user input might raise ValueError if the input is wrong format, or TypeError if the input type is wrong. Example: def process(value): if not isinstance(value, int): raise TypeError("Expected int") if value < 0: raise ValueError("Negative not allowed") return value * 2
Result
The function raises TypeError for wrong type and ValueError for negative numbers.
Knowing that multiple exceptions can come from one function prepares you to test each case separately.
3
IntermediateWriting separate tests for each exception
🤔Before reading on: Do you think one test can check multiple exceptions at once, or do you need separate tests? Commit to your answer.
Concept: Learn to write individual tests for each exception case to keep tests clear and focused.
For each exception your function can raise, write a separate test function. This makes it easier to see which error case fails. Example: def test_process_type_error(): with pytest.raises(TypeError): process('string') def test_process_value_error(): with pytest.raises(ValueError): process(-1)
Result
Each test checks one exception and passes only if that exception is raised.
Separating tests by exception type helps isolate failures and improves test clarity.
4
IntermediateUsing pytest.mark.parametrize for multiple exceptions
🤔Before reading on: Can you guess how to test multiple exceptions in one test using pytest? Commit to your answer.
Concept: Use pytest's parametrize decorator to test multiple inputs and expected exceptions in one test function.
pytest.mark.parametrize lets you run the same test with different inputs and expected exceptions. Example: import pytest @pytest.mark.parametrize("input_value, expected_exception", [ ('string', TypeError), (-1, ValueError), ]) def test_process_exceptions(input_value, expected_exception): with pytest.raises(expected_exception): process(input_value)
Result
The test runs twice, once for each input and expected exception, passing if the correct exception is raised each time.
Parametrization reduces code duplication and groups related exception tests neatly.
5
AdvancedCapturing exception details for assertions
🤔Before reading on: Do you think you can check the error message text when testing exceptions? Commit to your answer.
Concept: Learn to capture the exception object to check its message or attributes for more precise testing.
You can assign the exception to a variable using 'as excinfo' and then check its message. Example: @pytest.mark.parametrize("input_value, expected_message", [ ('string', 'Expected int'), (-1, 'Negative not allowed'), ]) def test_process_exception_messages(input_value, expected_message): with pytest.raises(Exception) as excinfo: process(input_value) assert expected_message in str(excinfo.value)
Result
The test passes only if the exception message contains the expected text.
Checking exception details ensures your code not only raises errors but also provides helpful messages.
6
ExpertTesting multiple exceptions in complex workflows
🤔Before reading on: Can you predict how to test exceptions raised deep inside chained function calls? Commit to your answer.
Concept: Understand how to test exceptions that occur inside multiple layers of function calls or asynchronous code.
When exceptions happen inside nested calls, your test still catches them if you call the outer function inside 'pytest.raises'. For async code, use 'pytest.mark.asyncio' and 'await' inside the raises block. Example: async def async_process(value): if value == 0: raise ZeroDivisionError('Zero not allowed') return 10 / value import pytest @pytest.mark.asyncio def test_async_process_zero(): with pytest.raises(ZeroDivisionError): await async_process(0)
Result
The test passes if the async function raises the expected exception.
Knowing how to test exceptions in async or layered code ensures your tests cover real-world complex scenarios.
Under the Hood
pytest uses Python's context manager protocol to catch exceptions inside the 'with pytest.raises()' block. When the code inside runs, if the expected exception is raised, pytest captures it and marks the test as passed. If no exception or a different exception occurs, pytest marks the test as failed. Parametrization runs the test function multiple times with different inputs, automating repetitive checks.
Why designed this way?
This design keeps tests simple and readable by isolating exception checks in a clear block. Using context managers leverages Python's built-in features for clean syntax. Parametrization avoids code duplication and encourages DRY (Don't Repeat Yourself) principles. This approach balances clarity, flexibility, and power for testing error handling.
┌───────────────────────────────┐
│ pytest.raises(ExpectedException)│
│ ┌───────────────────────────┐ │
│ │   Code that may raise     │ │
│ │   an exception            │ │
│ └───────────────────────────┘ │
│           │                   │
│           ▼                   │
│  Exception raised?           │
│    ┌───────────────┐         │
│    │ Yes           │ No      │
│    │ Test passes   │ Test fails│
│    └───────────────┘         │
└───────────────────────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Can one pytest.raises block check for multiple different exceptions at once? Commit to yes or no.
Common Belief:You can put multiple exception types in one pytest.raises to check them all at once.
Tap to reveal reality
Reality:pytest.raises expects a single exception type or a tuple of types, but it treats the tuple as 'any of these exceptions'. It does not test different inputs separately for different exceptions in one block.
Why it matters:Misusing this can cause tests to pass incorrectly if any exception in the tuple is raised, hiding bugs where the wrong exception is raised for a specific input.
Quick: Does pytest.raises catch exceptions raised outside its block? Commit to yes or no.
Common Belief:pytest.raises will catch exceptions raised anywhere in the test function.
Tap to reveal reality
Reality:pytest.raises only catches exceptions raised inside its 'with' block. Exceptions outside cause the test to error or fail.
Why it matters:Placing code that raises exceptions outside the block leads to false test failures and confusion.
Quick: Can you test exception messages by default with pytest.raises? Commit to yes or no.
Common Belief:pytest.raises automatically checks the exception message matches the expected error.
Tap to reveal reality
Reality:pytest.raises only checks the exception type by default. To check messages, you must capture the exception object and assert on its message explicitly.
Why it matters:Assuming message checks happen automatically can miss bugs where the wrong error message is shown, reducing test effectiveness.
Quick: Does parametrize run tests sequentially or in parallel by default? Commit to your answer.
Common Belief:pytest.mark.parametrize runs tests in parallel by default for speed.
Tap to reveal reality
Reality:Parametrize runs tests sequentially by default. Parallel execution requires extra plugins like pytest-xdist.
Why it matters:Expecting parallelism without setup can cause confusion about test order and shared state issues.
Expert Zone
1
When testing multiple exceptions, the order of parametrize inputs can affect test readability and debugging speed; grouping related exceptions helps.
2
Capturing and asserting on exception attributes beyond messages (like error codes) can catch subtle bugs in complex systems.
3
In asynchronous tests, forgetting to await inside pytest.raises can cause false positives or missed exceptions.
When NOT to use
Testing multiple exceptions with parametrize is not ideal when each exception requires very different setup or teardown steps; in such cases, separate test functions with fixtures are better.
Production Patterns
In real projects, teams often combine parametrize with custom helper functions to generate test cases dynamically from error specifications, ensuring coverage as the code evolves.
Connections
Exception handling in programming
Builds-on
Understanding how exceptions work in code is essential to write meaningful tests that check for those exceptions.
Test-driven development (TDD)
Builds-on
Testing multiple exceptions fits naturally into TDD by specifying all error cases before implementation, guiding robust code design.
Quality control in manufacturing
Analogy in process
Just like testing multiple failure modes in a product ensures reliability, testing multiple exceptions ensures software robustness.
Common Pitfalls
#1Testing multiple exceptions in one test without parametrization
Wrong approach:def test_process_errors(): with pytest.raises(TypeError): process('string') with pytest.raises(ValueError): process(-1)
Correct approach:import pytest @pytest.mark.parametrize("input_value, expected_exception", [ ('string', TypeError), (-1, ValueError), ]) def test_process_exceptions(input_value, expected_exception): with pytest.raises(expected_exception): process(input_value)
Root cause:The incorrect approach runs both raises blocks sequentially in one test, but if the first raises passes, the second is still reached; however, this approach is less clear and can hide which exception failed.
#2Catching exceptions outside the pytest.raises block
Wrong approach:def test_divide_by_zero(): divide(1, 0) with pytest.raises(ZeroDivisionError): pass
Correct approach:def test_divide_by_zero(): with pytest.raises(ZeroDivisionError): divide(1, 0)
Root cause:The exception must be raised inside the 'with' block; otherwise, pytest.raises cannot catch it.
#3Not checking exception messages when needed
Wrong approach:with pytest.raises(ValueError): process(-1)
Correct approach:with pytest.raises(ValueError) as excinfo: process(-1) assert 'Negative not allowed' in str(excinfo.value)
Root cause:pytest.raises only checks exception type by default; message checks require explicit assertions.
Key Takeaways
Testing multiple exceptions ensures your code handles all error cases correctly, improving reliability.
Use separate tests or pytest.mark.parametrize to clearly and efficiently test different exceptions.
Always place the code that raises exceptions inside the pytest.raises block to catch errors properly.
Capture exception details when needed to verify error messages or attributes for precise testing.
Testing exceptions in async or layered code requires special syntax but follows the same principles.