Python
Testing
Exception Handling
Unit Testing
Programming

How do you test that a Python function throws an exception?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

To test that a Python function raises an exception, call it inside an assertion that expects that exception. The two most common tools are unittest.TestCase.assertRaises from the standard library and pytest.raises from pytest.

The key idea is not just "the test should fail if something goes wrong." A good exception test verifies that the right exception is raised for the right reason.

Example Function Under Test

Suppose you have a function that rejects empty names:

python
1def normalize_name(name: str) -> str:
2    if not name:
3        raise ValueError("name must not be empty")
4    return name.strip().title()

The test should prove that invalid input raises ValueError.

Using unittest

With unittest, use assertRaises as a context manager:

python
1import unittest
2
3
4def normalize_name(name: str) -> str:
5    if not name:
6        raise ValueError("name must not be empty")
7    return name.strip().title()
8
9
10class NormalizeNameTests(unittest.TestCase):
11    def test_empty_name_raises_value_error(self):
12        with self.assertRaises(ValueError):
13            normalize_name("")
14
15
16if __name__ == "__main__":
17    unittest.main()

This is the standard-library solution and works well in projects that already use unittest.

Using pytest

With pytest, the equivalent is:

python
1import pytest
2
3
4def normalize_name(name: str) -> str:
5    if not name:
6        raise ValueError("name must not be empty")
7    return name.strip().title()
8
9
10def test_empty_name_raises_value_error():
11    with pytest.raises(ValueError):
12        normalize_name("")

This is shorter and is one reason many Python projects prefer pytest.

Assert the Message When It Matters

Sometimes the exception type alone is not enough. If your API contract depends on a specific message, assert that too.

With unittest:

python
1import unittest
2
3
4class NormalizeNameTests(unittest.TestCase):
5    def test_message_is_clear(self):
6        with self.assertRaises(ValueError) as cm:
7            normalize_name("")
8
9        self.assertEqual(str(cm.exception), "name must not be empty")

With pytest:

python
1import pytest
2
3
4def test_message_is_clear():
5    with pytest.raises(ValueError, match="name must not be empty"):
6        normalize_name("")

Do this when the message is part of the behavior you care about, not just because it is possible.

Custom Exceptions Work the Same Way

If your code raises a custom exception, test it the same way:

python
1class ConfigError(Exception):
2    pass
3
4
5def load_config(data: dict) -> str:
6    if "host" not in data:
7        raise ConfigError("missing host")
8    return data["host"]

Then:

python
1import pytest
2
3
4def test_missing_host_raises_config_error():
5    with pytest.raises(ConfigError):
6        load_config({})

The testing pattern does not change just because the exception class is your own.

Avoid Broad Exception Assertions

This is technically valid but usually too weak:

python
with pytest.raises(Exception):
    normalize_name("")

It passes for almost any failure, including the wrong one. Prefer the narrowest useful exception type:

  • 'ValueError instead of Exception'
  • 'FileNotFoundError instead of OSError'
  • your domain-specific exception instead of a generic base class

Specificity makes tests protect behavior rather than just "something exploded."

Parameterize Repeated Failure Cases

If several inputs should fail in the same way, parameterization keeps the test clean:

python
1import pytest
2
3
4@pytest.mark.parametrize("bad_name", ["", None])
5def test_invalid_names_raise(bad_name):
6    with pytest.raises(ValueError):
7        normalize_name(bad_name)

That is much easier to extend than copying the same test body several times.

Common Pitfalls

The most common mistake is putting too much code inside the raises block. Keep only the call that is supposed to fail there, or the test may pass for the wrong reason.

Another mistake is asserting an overly broad exception type such as Exception. That makes tests weak and hides regressions.

Teams also forget to test the successful path. If a function should raise on bad input and return a value on good input, both behaviors deserve tests.

Finally, do not catch the exception inside the function and then test for printed output unless printing is the intended API. If failure should be observable to callers, let the exception propagate and test that directly.

Summary

  • Use assertRaises in unittest or pytest.raises in pytest.
  • Assert the specific exception type, not a broad catch-all.
  • Check the message only when it is part of the expected behavior.
  • Parameterize repeated bad-input cases to keep tests concise.
  • Keep the exception assertion focused on the single call that is meant to fail.

Course illustration
Course illustration

All Rights Reserved.