A function that raises an exception on invalid input is behaving correctly — raising is part of its contract. Testing that behavior is just as important as testing the happy path.
**pytest.raises — the basic pattern:**
```python
import pytest
def divide(a, b):
return a / b
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
```
`pytest.raises` is a context manager. Inside the `with` block you call code that *should* raise. Three outcomes:
| What happens | Test result |
|---|---|
| Expected exception raised | PASSED |
| No exception raised | FAILED — `DID NOT RAISE` |
| Different exception raised | FAILED — the wrong exception propagates |
The third case is important: if `divide(10, 0)` raised `ValueError` instead of `ZeroDivisionError`, the test would fail with the unexpected `ValueError` in the output. You can't accidentally pass with the wrong exception type.
**Checking the exception message with `match`:**
The `match` parameter takes a **regex** pattern matched against `str(exception)`. You don't need to match the entire message — a distinctive substring is enough:
```python
def set_age(age):
if age < 0:
raise ValueError(f"Age {age} is invalid: must be non-negative")
return age
def test_negative_age():
with pytest.raises(ValueError, match=r"must be non-negative"):
set_age(-5)
```
Use raw strings (`r"..."`) for regex patterns. Common patterns:
- `match=r"must be"` — substring match
- `match=r"\d+ is invalid"` — match a number followed by text
- `match=r"^Age"` — message must start with "Age"
**Inspecting the exception object:**
Capture the `ExceptionInfo` object after the `with` block:
```python
with pytest.raises(ValueError) as excinfo:
set_age(-5)
excinfo.value # the exception instance
excinfo.type # the exception class (ValueError)
str(excinfo.value) # the full string representation
excinfo.value.args # the args tuple passed to the exception
```
**Exception subclasses:**
`pytest.raises(Exception)` would also catch `ValueError`, `TypeError`, etc. — any subclass of `Exception`. Use the most specific exception type you expect:
```python
# too broad — accepts any exception, hides bugs
with pytest.raises(Exception):
risky()
# correct — only accepts the specific type you designed for
with pytest.raises(ValueError):
risky()
```
**Asserting no exception:**
Testing that code does *not* raise requires no special syntax. Just call the function normally — if it raises unexpectedly, pytest catches the exception and fails the test:
```python
def test_valid_input():
result = set_age(25) # should not raise
assert result == 25 # verify the return value too
```
**Complete example — a withdrawal function with custom exception:**
```python
import pytest
class InsufficientFundsError(Exception):
pass
def withdraw(balance, amount):
if not isinstance(amount, (int, float)):
raise TypeError(f"Amount must be a number, got {type(amount).__name__}")
if amount <= 0:
raise ValueError(f"Amount must be positive, got {amount}")
if amount > balance:
raise InsufficientFundsError(
f"Cannot withdraw {amount}: balance is only {balance}"
)
return balance - amount
```
```python
# Test each exception type separately
def test_non_numeric_amount():
with pytest.raises(TypeError):
withdraw(100, "fifty")
def test_negative_amount():
with pytest.raises(ValueError, match=r"must be positive"):
withdraw(100, -10)
def test_zero_amount():
with pytest.raises(ValueError):
withdraw(100, 0)
def test_insufficient_funds():
with pytest.raises(InsufficientFundsError):
withdraw(50, 100)
def test_insufficient_funds_message():
with pytest.raises(InsufficientFundsError, match=r"balance is only 50"):
withdraw(50, 100)
```
**Inspecting the exception after the block:**
```python
def test_exception_details():
with pytest.raises(InsufficientFundsError) as excinfo:
withdraw(50, 100)
assert excinfo.type is InsufficientFundsError
assert "100" in str(excinfo.value) # requested amount in message
assert "50" in str(excinfo.value) # available balance in message
```
**Testing that valid input works (no exception):**
```python
def test_successful_withdrawal():
new_balance = withdraw(100, 40)
assert new_balance == 60
def test_withdraw_entire_balance():
new_balance = withdraw(100, 100)
assert new_balance == 0
```
**Common mistake — putting assertions inside the with block:**
```python
# WRONG: if withdraw() raises immediately, the assert never runs
with pytest.raises(InsufficientFundsError):
result = withdraw(50, 100)
assert result is None # this line is dead code
# RIGHT: assertions about results go outside the with block
with pytest.raises(InsufficientFundsError) as excinfo:
withdraw(50, 100)
assert "100" in str(excinfo.value) # runs after the block
```
The `with` block ends as soon as the exception is raised. Any code after the raising line but still inside the `with` block is unreachable.
```python
import pytest
# Basic: assert exception type
with pytest.raises(SomeException):
code_that_should_raise()
# Check message with regex
with pytest.raises(SomeException, match=r"pattern"):
code_that_should_raise()
# Inspect exception object after the block
with pytest.raises(SomeException) as excinfo:
code_that_should_raise()
excinfo.value # the exception instance
excinfo.type # the exception class
str(excinfo.value) # string representation
# Accept multiple exception types
with pytest.raises((ValueError, TypeError)):
ambiguous_function()
```
**Common mistake — assertion inside the with block:**
```python
# WRONG: assert never runs if the exception fires first
with pytest.raises(ValueError):
result = risky_call()
assert result == 42 # skipped!
# RIGHT: assertions go outside the with block
with pytest.raises(ValueError):
risky_call()
# result assertions here, after confirming the exception happened
```
**Testing exception subclasses:**
`pytest.raises(Exception)` matches `ValueError`, `TypeError`, etc. (any subclass).
Use the exact type when you care which exception was raised.
Write `divide(a, b)` that returns `a / b`. Write a test that asserts `ZeroDivisionError` is raised when `b=0`. Also write a test that verifies the function returns correct results when `b` is non-zero.
import pytest
def divide(a, b):
return a / b
def test_divide_by_zero():
pass
def test_divide_normal():
pass
Write `set_age(age)` that raises `ValueError` with message `'Age must be between 0 and 150'` when `age < 0` or `age > 150`. Write tests asserting the ValueError is raised and its message contains `'between 0 and 150'` using the `match` parameter.
import pytest
def set_age(age):
pass # raise ValueError with specific message for invalid ages
def test_negative_age():
pass
def test_age_too_large():
pass
Solution
import pytest
def set_age(age):
if age < 0 or age > 150:
raise ValueError('Age must be between 0 and 150')
return age
def test_negative_age():
with pytest.raises(ValueError, match=r'between 0 and 150'):
set_age(-1)
def test_age_too_large():
with pytest.raises(ValueError, match=r'between 0 and 150'):
set_age(151)
def test_valid_age():
assert set_age(25) == 25
assert set_age(0) == 0
assert set_age(150) == 150
Write `parse_positive(value)` that raises `TypeError` if `value` is not a number and `ValueError` if `value <= 0`. Write separate tests for each exception type, each verifying the right exception is raised for the right input.
import pytest
def parse_positive(value):
pass # TypeError for non-numbers, ValueError for non-positive
def test_non_number_raises_type_error():
pass
def test_non_positive_raises_value_error():
pass
Solution
import pytest
def parse_positive(value):
if not isinstance(value, (int, float)):
raise TypeError(f'Expected a number, got {type(value).__name__}')
if value <= 0:
raise ValueError(f'Value must be positive, got {value}')
return value
def test_non_number_raises_type_error():
with pytest.raises(TypeError):
parse_positive('five')
with pytest.raises(TypeError):
parse_positive(None)
def test_non_positive_raises_value_error():
with pytest.raises(ValueError):
parse_positive(0)
with pytest.raises(ValueError):
parse_positive(-10)
def test_valid_input():
assert parse_positive(5) == 5
assert parse_positive(0.1) == pytest.approx(0.1)
Define `InsufficientFundsError(Exception)` and `withdraw(balance, amount)` that raises it when `amount > balance`. Write a test asserting the custom exception is raised, and another using `match` to verify the message mentions the requested amount.
Write `safe_divide(a, b)` that returns `a / b` when `b != 0` and returns `0` when `b == 0` — no exception in either case. Write tests for both paths, verifying the correct return values.
import pytest
def safe_divide(a, b):
pass # return a/b normally, return 0 if b is 0
def test_normal_division():
pass
def test_zero_denominator_returns_zero():
pass
Solution
def safe_divide(a, b):
if b == 0:
return 0
return a / b
def test_normal_division():
assert safe_divide(10, 2) == 5.0
assert safe_divide(-6, 3) == -2.0
def test_zero_denominator_returns_zero():
result = safe_divide(10, 0)
assert result == 0
def test_zero_numerator():
assert safe_divide(0, 5) == 0.0
No split tab
Cookie preferences
We use necessary cookies to run the site. With your permission, we can also save your site preferences and use analytics and advertising cookies to understand usage and support the project.
Open tools in tabs.Exercises, IDE tools, and trainers stay available as site tabs.
Switch without losing context.Move between explanations, code, and utilities while keeping your place.
Use the sidebar as your map.The left panels hold navigation, settings, files, libraries, and tool controls.
PythonJavaScriptSQLite
One IDE, three practical modes
Python in the browser.Run small scripts, try libraries, and practice API requests without installing anything.
JavaScript for quick experiments.Test browser-friendly code and compare ideas next to your learning materials.
SQLite for data practice.Open the database explorer to inspect tables, write queries, and learn SQL workflows locally.
TopicIDE
Work side by side with split tabs
Keep instructions visible.Open an exercise or reference page beside the IDE instead of jumping back and forth.
Compare tools while you learn.Place regex checks, explanations, and code experiments next to each other when the task needs it.
Close the split when you are done.The workspace returns to a single focused tab, and your open site tabs remain available.
Code Typing Trainer
Or plain text
This trainer is designed for a physical keyboard.Open this section on a laptop or desktop with a wide screen. Touch typing practice will not work correctly on a phone.
Speed: 0 chars/min
0 words/min
Best speed (60s): 0 chars/min
0 words/min
Errors: 0
Total time: 0.0 s
To practice touch typing, avoid looking at your physical keyboard.