## The limits of example-based testing
Every test you have written so far is *example-based*: you pick specific inputs, call the function, and assert specific outputs.
```python
def test_reverse():
assert reverse('hello') == 'olleh'
assert reverse('') == ''
assert reverse('a') == 'a'
```
You chose these three examples. You tested what you thought to test. If you did not think of `'racecar'` (a palindrome), or `'café'` (unicode), or `' '` (whitespace), those inputs go untested.
The fundamental weakness: **example-based tests only cover the examples you thought to write.**
## Property-based testing: describe the invariant, let the library find the counterexample
Property-based testing takes a different approach. Instead of choosing examples, you describe a *property* — an invariant that must hold for any valid input — and let a library generate hundreds of random inputs to try to falsify it.
```python
from hypothesis import given
from hypothesis import strategies as st
@given(st.text())
def test_reverse_involution(s):
# Property: reversing a string twice gives back the original string.
assert reverse(reverse(s)) == s
```
Hypothesis generates random strings — short, long, empty, unicode, control characters, strings with spaces — and runs the test body for each one. If any input causes the assertion to fail, hypothesis reports it as a failing example and tries to *shrink* it to the smallest possible failing case.
## Installing and importing
```bash
pip install hypothesis
```
```python
from hypothesis import given, assume, settings
from hypothesis import strategies as st
```
## The @given decorator
`@given(strategy)` is the core of hypothesis. It turns a regular test function into a property test by injecting generated values as arguments:
```python
@given(st.integers())
def test_abs_is_non_negative(n):
assert abs(n) >= 0
```
Hypothesis generates integers across the full range — positives, negatives, zero, `sys.maxsize`, `-sys.maxsize - 1` — and runs the test body for each. This test will pass for all of them because `abs` is correct.
If you had a buggy implementation:
```python
def buggy_abs(n):
return n # bug: negative numbers returned unchanged
```
Hypothesis quickly finds the minimal counterexample:
```
Falsifying example: test_abs_is_non_negative(n=-1)
```
The original failing input might have been `-273`. Hypothesis shrinks it to `-1` — the smallest integer that falsifies `abs(n) >= 0` for a function that returns `n` unchanged.
## Strategies: how hypothesis generates data
A *strategy* describes a set of values hypothesis can generate. The `hypothesis.strategies` module (`st`) provides a comprehensive library:
| Strategy | Generates |
|---|---|
| `st.integers()` | Any integer |
| `st.integers(min_value=0, max_value=100)` | Integer in [0, 100] |
| `st.floats()` | Any float (including NaN, inf) |
| `st.floats(allow_nan=False, allow_infinity=False)` | Finite float |
| `st.text()` | Any unicode string |
| `st.text(alphabet=string.ascii_lowercase, min_size=1)` | Non-empty lowercase ASCII |
| `st.booleans()` | True or False |
| `st.lists(st.integers())` | List of integers |
| `st.lists(st.integers(), min_size=1, max_size=10)` | Non-empty list, up to 10 items |
| `st.dictionaries(st.text(), st.integers())` | Dict with text keys, integer values |
| `st.one_of(st.integers(), st.text())` | Either an integer or a text |
## Shrinking: finding the minimal failing example
When hypothesis finds a failing input, it does not stop and report that input immediately. It tries to *shrink* it — find a smaller, simpler input that also fails. This is one of hypothesis's most valuable features.
For example, if hypothesis generates a 500-character string that fails your test, it will try progressively shorter strings until it finds the shortest string that still fails. The reported failure is the minimal example, not the original random one — making it easy to understand the bug.
```
Falsifying example: test_slugify(s='A B')
# Not: s='Random Long String With Spaces And Punctuation!!!'
```
## assume(): filtering invalid inputs
Sometimes a property only makes sense for a subset of inputs. `assume()` tells hypothesis to skip examples that do not satisfy a precondition:
```python
from hypothesis import given, assume
from hypothesis import strategies as st
@given(st.integers(), st.integers())
def test_division(a, b):
assume(b != 0) # skip examples where b is zero
result = a / b
assert isinstance(result, float)
```
When hypothesis generates `b=0`, `assume(b != 0)` raises an internal exception and hypothesis discards that example, trying a different one. The test is only run on valid inputs.
**Caution:** `assume()` should be used sparingly. If most generated inputs are discarded, hypothesis generates many more examples to find valid ones, and may give up with `Unsatisfied`. Use constrained strategies instead when possible:
```python
# Better than assume(b != 0):
@given(st.integers(), st.integers().filter(lambda x: x != 0))
def test_division(a, b):
result = a / b
assert isinstance(result, float)
```
## settings: controlling hypothesis behaviour
```python
from hypothesis import settings
@settings(max_examples=500) # run 500 examples instead of the default 100
@given(st.text())
def test_something(s):
...
```
| Setting | Default | Description |
|---|---|---|
| `max_examples` | 100 | How many valid examples to generate |
| `deadline` | 200 ms | Maximum time per example (set `None` to disable) |
| `suppress_health_check` | `[]` | Disable specific health checks |
For tests that call a real API, set `deadline=None` to avoid false failures from network latency:
```python
@settings(max_examples=10, deadline=None)
@given(st.text(min_size=1, max_size=20, alphabet=string.ascii_lowercase))
def test_search(query):
resp = requests.get(f'{BASE_URL}/api/search/?q={query}')
assert resp.status_code in (200, 404)
```
## When to use hypothesis
**Good fit:**
- Pure functions with mathematical properties (sorting, encoding, parsing)
- Functions with invertible operations (`encode`/`decode`, `compress`/`decompress`)
- Validation functions that should reject or accept any input of a type
- Data structure invariants (tree height, list ordering, set membership)
**Poor fit:**
- Tests that require specific setup state in a database
- Tests where the expected output depends on the input in a non-trivial way (then use parametrize)
- Tests of side-effectful functions (calls to APIs, writing files) — too slow for 100 examples
The clearest sign that hypothesis is right for a test: the property is a universal statement like "for all X, property(X) is True."
Hypothesis in practice: invariants, shrinking, and composite strategies
## Setup
```bash
pip install hypothesis
```
## Example 1: The simplest property — abs is non-negative
```python
# test_abs.py
from hypothesis import given
from hypothesis import strategies as st
@given(st.integers())
def test_abs_non_negative(n):
assert abs(n) >= 0
```
```
$ pytest test_abs.py -v
test_abs.py::test_abs_non_negative PASSED (ran 100 examples)
```
Hypothesis generated 100 integers — including large positives, large negatives, zero, and edge cases like `sys.maxsize` — and verified the property for each. This is more thorough than any manual parametrize list.
## Example 2: Invertible operation — double reverse
```python
# test_reverse.py
from hypothesis import given
from hypothesis import strategies as st
def reverse(s: str) -> str:
return s[::-1]
@given(st.text())
def test_reverse_involution(s):
assert reverse(reverse(s)) == s
```
"Reverse twice equals identity" is the classic property for string reversal. Hypothesis will try empty strings, single characters, unicode characters (`'é'`, `'🐍'`), strings with null bytes, and more. The property holds for all of them.
## Example 3: Sorted list invariants
```python
# test_sorting.py
from hypothesis import given
from hypothesis import strategies as st
@given(st.lists(st.integers()))
def test_sorted_list_is_ordered(lst):
result = sorted(lst)
for i in range(len(result) - 1):
assert result[i] <= result[i + 1]
@given(st.lists(st.integers()))
def test_sort_preserves_length(lst):
assert len(sorted(lst)) == len(lst)
@given(st.lists(st.integers()))
def test_sort_preserves_elements(lst):
assert sorted(lst, reverse=True) == list(reversed(sorted(lst)))
assert set(sorted(lst)) == set(lst)
```
Notice three separate properties for the same function: ordering, length, element set. Each property is simple; together they give strong confidence in `sorted`.
## Example 4: assume() to constrain inputs
```python
# test_division.py
from hypothesis import given, assume
from hypothesis import strategies as st
def safe_divide(a: int, b: int) -> float:
return a / b
@given(st.integers(), st.integers())
def test_safe_divide_result_type(a, b):
assume(b != 0)
result = safe_divide(a, b)
assert isinstance(result, float)
@given(st.integers(min_value=1), st.integers(min_value=1))
def test_divide_positive_by_positive(a, b):
result = safe_divide(a, b)
assert result > 0
```
The second test uses a constrained strategy (`min_value=1`) instead of `assume()` — this is more efficient because hypothesis generates only valid inputs from the start, rather than generating and discarding many invalid ones.
## Example 5: Finding a bug with hypothesis — shrinking in action
```python
# test_buggy_max.py
from hypothesis import given
from hypothesis import strategies as st
def my_max(lst):
result = lst[0]
for x in lst[1:]:
if x > result:
result = x
return result
@given(st.lists(st.integers(), min_size=1))
def test_my_max_equals_builtin(lst):
assert my_max(lst) == max(lst)
```
If you introduced a bug — say, `>` instead of `>=` might cause issues in some cases, or if the loop had an off-by-one — hypothesis would find the minimal failing list. For example, if the bug only manifests with repeated maximum values, hypothesis would shrink to `[1, 1]` rather than reporting `[5, 3, 1, 5, 8, 5]`.
## Example 6: @st.composite — custom strategy
`@st.composite` lets you build a strategy that combines multiple draws to produce a structured value:
```python
# test_composite.py
from hypothesis import given
from hypothesis import strategies as st
@st.composite
def valid_user(draw):
username = draw(st.text(
alphabet='abcdefghijklmnopqrstuvwxyz0123456789_',
min_size=3,
max_size=20,
))
age = draw(st.integers(min_value=13, max_value=120))
return {'username': username, 'age': age}
def validate_user(user):
if len(user['username']) < 3:
raise ValueError('Username too short')
if user['age'] < 13:
raise ValueError('Too young')
return True
@given(valid_user())
def test_valid_user_always_validates(user):
assert validate_user(user) is True
```
`draw(strategy)` inside a `@st.composite` function samples a value from `strategy`. The decorated function becomes a strategy that can be passed to `@given`. This is the right tool when you need to generate structured data where one field constrains another.
## Running hypothesis with verbosity
```bash
pytest test_abs.py -v --hypothesis-show-statistics
```
```
test_abs_non_negative:
- 100 passing examples
- Typical runtimes: 0-1ms
```
To see every generated example:
```bash
pytest test_abs.py -v -s --hypothesis-verbosity=verbose
```
Install `hypothesis` and write a property-based test `test_abs_non_negative` that:
- Uses `@given(st.integers())` to generate any integer
- Asserts that `abs(n) >= 0`
Also write `test_abs_of_abs_equals_abs` that asserts `abs(abs(n)) == abs(n)` for any integer.
Both tests should pass — they are testing a correct Python built-in.
# test_abs_property.py
from hypothesis import given
from hypothesis import strategies as st
@given(st.integers())
def test_abs_non_negative(n):
...
@given(st.integers())
def test_abs_of_abs_equals_abs(n):
...
Solution
# test_abs_property.py
from hypothesis import given
from hypothesis import strategies as st
@given(st.integers())
def test_abs_non_negative(n):
assert abs(n) >= 0
@given(st.integers())
def test_abs_of_abs_equals_abs(n):
assert abs(abs(n)) == abs(n)
02
Test that reversing a string twice returns the original
Define a `reverse(s: str) -> str` function that reverses a string using slicing (`s[::-1]`).
Write a property test `test_reverse_involution` using `@given(st.text())` that asserts:
```
reverse(reverse(s)) == s
```
Also write `test_reverse_empty` using `@given(st.text(max_size=0))` — the reverse of an empty string is an empty string.
# test_reverse.py
from hypothesis import given
from hypothesis import strategies as st
def reverse(s: str) -> str:
...
@given(st.text())
def test_reverse_involution(s):
...
@given(st.text(max_size=0))
def test_reverse_empty(s):
...
Solution
# test_reverse.py
from hypothesis import given
from hypothesis import strategies as st
def reverse(s: str) -> str:
return s[::-1]
@given(st.text())
def test_reverse_involution(s):
assert reverse(reverse(s)) == s
@given(st.text(max_size=0))
def test_reverse_empty(s):
assert reverse(s) == ''
03
Use assume() to test division with a non-zero denominator
Write a function `safe_divide(a: float, b: float) -> float` that returns `a / b`.
Write two property tests:
1. `test_divide_with_assume` — use `@given(st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False), st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False))` and `assume(b != 0)`. Assert `safe_divide(a, b) * b == pytest.approx(a)` (multiplying result by denominator gives back numerator).
2. `test_divide_positive_constrained` — use `@given(st.floats(min_value=0.1, max_value=100), st.floats(min_value=0.1, max_value=100))`. Assert the result is positive.
# test_division_property.py
import pytest
from hypothesis import given, assume
from hypothesis import strategies as st
def safe_divide(a: float, b: float) -> float:
return a / b
@given(
st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False),
st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False),
)
def test_divide_with_assume(a, b):
assume(b != 0)
result = safe_divide(a, b)
assert result * b == pytest.approx(a, rel=1e-6)
@given(
st.floats(min_value=0.1, max_value=100),
st.floats(min_value=0.1, max_value=100),
)
def test_divide_positive_constrained(a, b):
result = safe_divide(a, b)
assert result > 0
Solution
# test_division_property.py
import pytest
from hypothesis import given, assume
from hypothesis import strategies as st
def safe_divide(a: float, b: float) -> float:
return a / b
@given(
st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False),
st.floats(min_value=-1e15, max_value=1e15, allow_nan=False, allow_infinity=False),
)
def test_divide_with_assume(a, b):
assume(b != 0)
result = safe_divide(a, b)
assert result * b == pytest.approx(a, rel=1e-6)
@given(
st.floats(min_value=0.1, max_value=100),
st.floats(min_value=0.1, max_value=100),
)
def test_divide_positive_constrained(a, b):
result = safe_divide(a, b)
assert result > 0
04
Test that max(lst) >= min(lst) for any non-empty list
Write a property test using `@given(st.lists(st.integers(), min_size=1))` that asserts:
- `max(lst) >= min(lst)`
- `max(lst)` is an element of `lst` (i.e., `max(lst) in lst`)
- `min(lst)` is an element of `lst` (i.e., `min(lst) in lst`)
All three properties should be separate `assert` statements in a single test function `test_max_min_properties`.
# test_max_min.py
from hypothesis import given
from hypothesis import strategies as st
@given(st.lists(st.integers(), min_size=1))
def test_max_min_properties(lst):
assert ...
assert ...
assert ...
Solution
# test_max_min.py
from hypothesis import given
from hypothesis import strategies as st
@given(st.lists(st.integers(), min_size=1))
def test_max_min_properties(lst):
assert max(lst) >= min(lst)
assert max(lst) in lst
assert min(lst) in lst
Write a `@st.composite` strategy `valid_user(draw)` that generates a user dict with:
- `username` — 3–20 characters from `string.ascii_lowercase + string.digits + '_'`
- `age` — integer between 13 and 120
- `email` — a simple fake email constructed as `f'{username}@example.com'`
Note: derive `email` from the same `username` value you drew (so they are consistent).
Write a `validate_user(user)` function that:
- Raises `ValueError('Username too short')` if `len(username) < 3`
- Raises `ValueError('Too young')` if `age < 13`
- Returns `True` otherwise
Write `test_valid_user_always_validates` using `@given(valid_user())` that asserts `validate_user(user) is True`.
# test_composite.py
import string
from hypothesis import given
from hypothesis import strategies as st
@st.composite
def valid_user(draw):
username = draw(st.text(
alphabet=...,
min_size=3,
max_size=20,
))
age = draw(st.integers(min_value=13, max_value=120))
return {
'username': username,
'age': age,
'email': f'{username}@example.com',
}
def validate_user(user):
if len(user['username']) < 3:
raise ValueError('Username too short')
if user['age'] < 13:
raise ValueError('Too young')
return True
@given(valid_user())
def test_valid_user_always_validates(user):
assert validate_user(user) is True
Solution
# test_composite.py
import string
from hypothesis import given
from hypothesis import strategies as st
@st.composite
def valid_user(draw):
username = draw(st.text(
alphabet=string.ascii_lowercase + string.digits + '_',
min_size=3,
max_size=20,
))
age = draw(st.integers(min_value=13, max_value=120))
return {
'username': username,
'age': age,
'email': f'{username}@example.com',
}
def validate_user(user):
if len(user['username']) < 3:
raise ValueError('Username too short')
if user['age'] < 13:
raise ValueError('Too young')
return True
@given(valid_user())
def test_valid_user_always_validates(user):
assert validate_user(user) is True
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.