How do I write a test that an exception is raised?¶
You've got a function that's supposed to raise an exception when given bad input — divide(1, 0) should raise ZeroDivisionError, validate_age(-5) should raise ValueError, and so on. Writing the test with a try/except block looks reasonable but quietly does the wrong thing: a test that's meant to fail will pass if the exception never fires.
The right tool is self.assertRaises() as a context manager. It asserts the block raises the expected exception, captures the exception object so you can inspect it, and fails the test loudly if nothing was raised at all.
import unittest
def divide(a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("cannot divide by zero")
return a / b
class TestDivide(unittest.TestCase):
def test_raises_on_zero_divisor(self):
# The context-manager form is the one to use in almost every case.
# The block must raise ZeroDivisionError or the test fails.
with self.assertRaises(ZeroDivisionError) as ctx:
divide(10, 0)
# ctx.exception is the exception object — inspect it like any other.
self.assertIn("divide by zero", str(ctx.exception))
unittest.main(argv=[''], exit=False, verbosity=2)
Two variations on the basic pattern: matching the message with a regex, and asserting different inputs raise different exception types.
import unittest
def validate_age(age: int) -> int:
if not isinstance(age, int):
raise TypeError(f"age must be int, got {type(age).__name__}")
if age < 0:
raise ValueError(f"age cannot be negative: {age}")
if age > 150:
raise ValueError(f"age unrealistic: {age}")
return age
class TestValidateAge(unittest.TestCase):
def test_negative_age(self):
# assertRaisesRegex checks the exception type AND that the message
# matches the given regex. Cleaner than asserting on str(exc) by hand.
with self.assertRaisesRegex(ValueError, r"cannot be negative"):
validate_age(-5)
def test_unrealistic_age(self):
with self.assertRaisesRegex(ValueError, r"unrealistic"):
validate_age(999)
def test_wrong_type(self):
with self.assertRaisesRegex(TypeError, r"must be int"):
validate_age("twenty")
unittest.main(argv=[''], exit=False, verbosity=2)
import unittest
def calculate_discount(price: float, percentage: float) -> float:
if not isinstance(price, (int, float)):
raise TypeError("price must be a number")
if not isinstance(percentage, (int, float)):
raise TypeError("percentage must be a number")
if price < 0:
raise ValueError("price cannot be negative")
if not 0 <= percentage <= 100:
raise ValueError("percentage must be between 0 and 100")
return price * (1 - percentage / 100)
class TestCalculateDiscount(unittest.TestCase):
"""One assertion per test method — when a test fails, the failure
message points at exactly the input that broke."""
def test_negative_price_raises_value_error(self):
with self.assertRaises(ValueError):
calculate_discount(-100, 10)
def test_percentage_above_100_raises_value_error(self):
with self.assertRaises(ValueError):
calculate_discount(100, 150)
def test_string_price_raises_type_error(self):
with self.assertRaises(TypeError):
calculate_discount("100", 10)
unittest.main(argv=[''], exit=False, verbosity=2)
Why it works¶
The with self.assertRaises(SomeException): block is doing two things in the background, and both matter.
When the block runs, assertRaises enters its __enter__ and lets the body execute normally. If the body raises an instance of the expected exception class (or a subclass), __exit__ swallows the exception and the test continues. If the body raises a different exception, that one propagates out and the test errors. If the body finishes without raising anything, __exit__ raises an AssertionError saying "expected SomeException, got nothing" — and that's the bit a try/except can't replicate cleanly. The whole point of the test is that the exception must fire; "no exception" should fail the test loudly, not pass it silently.
The optional as ctx binds the captured exception to a name so you can inspect it after the block. ctx.exception is the exception instance — you can read its message via str(ctx.exception), check attributes (ctx.exception.errno), or use assertIsInstance to confirm a more specific subclass.
assertRaisesRegex is the same idea with a built-in message check. It fails if the exception isn't raised, and it fails if the exception's str() doesn't match the regex. Use it when the message itself is part of the contract — error messages your users see, error codes embedded in messages, anything where "raises ValueError" isn't specific enough.
The "one assertion per test" pattern in the third extra is what makes test failures readable. If a single test method asserts five different inputs raise the right exception and the second one regresses, the failure message tells you the test broke — but you have to read the test source to find out which of the five inputs is the culprit. Five named tests turn the failure list itself into the diagnosis.
Trade-offs¶
assertRaises(Exception) works, and it's almost always wrong. Catching Exception means the test passes if the function raises anything — including NameError because you fat-fingered the function call, or ImportError because the module fails to import. Always assert the most specific exception type the function is documented to raise.
The two-argument form self.assertRaises(SomeException, callable, *args, **kwargs) exists for legacy reasons. The context-manager form is clearer in every case and lets you inspect the exception afterwards. Prefer it.
Don't write try/except inside a test as a way to "handle" exceptions you expect. The test should fail when something unexpected happens, and try/except swallows that signal. The only time try/except belongs in a test is when you genuinely want to assert something after an exception was raised — and even then, assertRaises with ctx.exception does the job better.
Avoid testing for the exact exception message string with assertEqual(str(ctx.exception), "..."). Messages are easy to tweak for clarity ("invalid age" → "age is invalid") and you don't want every test breaking when you improve a message. assertRaisesRegex with a partial regex is the right level of specificity — it pins down the meaningful part without locking in the exact wording.
Related¶
- How to handle multiple exceptions — once you know the function raises the right exception, you can write the calling code that handles it.
- How to create custom exceptions — custom types make
assertRaisescalls precise without overloading the message. - How to run tests in Jupyter — the
unittest.main(argv=[''], exit=False)pattern these examples use. - Assertions reference — the full set of
assertXmethods, including the ones built onassertRaises.