Using Hypothesis for Property-Based Testing in Python

Traditional unit testing checks expected inputs and outputs. But what if you could test your functions against a wide range of generated inputs, including edge cases you never thought of? That’s where property-based testing comes in — and in Python, the go-to tool is the powerful library Hypothesis.

This guide walks you through what property-based testing is, how Hypothesis works, and how to use it effectively in real-world Python code.

What is Property-Based Testing?

Property-based testing is a method where tests are written as rules or properties your code should always satisfy, regardless of input. Instead of writing dozens of test cases manually, the framework generates inputs automatically and tests whether your code holds up.

Example: Property

The reverse of a reversed list should be the original list.
No need to test [1, 2, 3], [0], or [] separately. Hypothesis will generate all kinds of lists — including edge cases you wouldn’t even consider.

Why Use Hypothesis?

  • Test with more coverage and fewer lines of code
  • Uncover edge-case bugs effortlessly
  • Fewer missed cases compared to manually written tests
  • Works seamlessly with pytest and unittest

Installing Hypothesis

Install with pip:

pip install hypothesis

Or with pytest support:

pip install hypothesis pytest

Basic Example

Let’s test this property: Reversing a list twice gives the original list.

from hypothesis import given
from hypothesis.strategies import lists, integers

@given(lists(integers()))
def test_reverse_identity(xs):
    assert list(reversed(list(reversed(xs)))) == xs

Hypothesis will automatically:

  • Generate random lists of integers
  • Run this test hundreds of times
  • Shrink inputs to the smallest failing case if it fails

Common Hypothesis Strategies

Strategy Description
integers() Any integer
text() Unicode strings
lists(...) Lists of elements
dictionaries() Dicts with keys and values
booleans() True/False
emails() Valid email strings (via hypothesis.extra)
one_of(...) Randomly chooses from provided strategies

You can combine and customize them for complex data.

Real-World Example: Email Validator

Let’s say you wrote a function to validate email addresses:

import re

def is_valid_email(s: str) -> bool:
    return re.match(r"[^@]+@[^@]+\.[^@]+", s) is not None

Now test it with property-based testing:

from hypothesis import given
from hypothesis.strategies import text

@given(text())
def test_email_does_not_crash(s):
    is_valid_email(s)

This ensures your function never throws on any kind of text input — even bizarre Unicode strings

Custom Strategies

You can define custom input strategies using @composite:

from hypothesis.strategies import composite

@composite
def user_names(draw):
    first = draw(text(min_size=1, max_size=10))
    last = draw(text(min_size=1, max_size=10))
    return f"{first}_{last}"

Use it in your test:

@given(user_names())
def test_username_has_underscore(name):
    assert "_" in name

Shrinking: Finding Minimal Failing Cases

When a test fails, Hypothesis shrinks the input to find the smallest example that still fails. This makes debugging much easier.

Best Practices

  • Use clear, high-level properties
  • Combine with example-based tests
  • Limit input size if performance matters
  • Use .filter() to constrain inputs
  • Avoid stateful or side-effect-heavy code

Conclusion

Hypothesis transforms the way you test by generating smart, varied inputs that help uncover bugs traditional tests miss. Whether you’re building a utility function or a critical backend service, property-based testing improves confidence and code quality.

Start simple, think in properties, and let Hypothesis do the heavy lifting.

Keep Coding…

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top