Hypothesis¶
Property testing is a testing method that does not check whether specific inputs lead to specific outputs, but instead generates random inputs, runs the programme with all of these inputs, and then verifies the validity of this property.
In Python, you can use Hypothesis to generate such inputs parametrically, enabling you to quickly find errors in your tests.
See also
The Jupyter Tutorial describes how Hypothesis can also be used in Jupyter Notebooks.
Installation¶
$ uv add --group tests hypothesis
C:> uv add --group tests hypothesis
Alternatively, Hypothesis can also be installed using extensions, for example:
$ uv add --group tests "hypothesis[numpy, pandas]"
C:> uv add --group tests "hypothesis[numpy, pandas]"
Example using strategies and given¶
First, we import sample data for floats and lists from hypothesis.strategies. To be able to apply this sample data to our test function, we also import hypothesis.given:
1import pytest 2from hypothesis import given 3from hypothesis.strategies import floats, lists
For our test, we will now use
hypothesis.givenas a dekorator to convert the test function into a parameterised one, which is then executed with a wide range of suitable data:6@given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1)) 7def test_mean(ls): 8 mean = sum(ls) / len(ls) 9 assert min(ls) <= mean <= max(ls)
Finally, we run the test:
$ uv run pytest docs/test/test_hypothesis.py ============================= test session starts ============================== platform darwin -- Python 3.13.0, pytest-9.0.3, pluggy-1.6.0 rootdir: /Users/veit/cusy/trn/python-basics-tutorial-de plugins: hypothesis-6.152.1 collected 1 item test_hypothesis.py F [100%] =================================== FAILURES =================================== __________________________________ test_mean ___________________________________ @given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1)) > def test_mean(ls): test_hypothesis.py:6: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ls = [9.9792015476736e+291, 1.7976931348623157e+308] @given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1)) def test_mean(ls): mean = sum(ls) / len(ls) > assert min(ls) <= mean <= max(ls) E assert inf <= 1.7976931348623157e+308 E + where 1.7976931348623157e+308 = max([9.9792015476736e+291, 1.7976931348623157e+308]) test_hypothesis.py:8: AssertionError ---------------------------------- Hypothesis ---------------------------------- Falsifying example: test_mean( ls=[9.9792015476736e+291, 1.7976931348623157e+308], ) =========================== short test summary info ============================ FAILED test_hypothesis.py::test_mean - assert inf <= 1.7976931348623157e+308 ============================== 1 failed in 0.44s ===============================
C:> uv run pytest docs/test/test_hypothesis.py ============================= test session starts ============================== platform darwin -- Python 3.13.0, pytest-9.0.3, pluggy-1.6.0 rootdir: C:\Users\veit\python-basics-tutorial-de plugins: hypothesis-6.152.1 collected 1 item test_hypothesis.py F [100%] =================================== FAILURES =================================== __________________________________ test_mean ___________________________________ @given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1)) > def test_mean(ls): test_hypothesis.py:6: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ls = [9.9792015476736e+291, 1.7976931348623157e+308] @given(lists(floats(allow_nan=False, allow_infinity=False), min_size=1)) def test_mean(ls): mean = sum(ls) / len(ls) > assert min(ls) <= mean <= max(ls) E assert inf <= 1.7976931348623157e+308 E + where 1.7976931348623157e+308 = max([9.9792015476736e+291, 1.7976931348623157e+308]) test_hypothesis.py:8: AssertionError ---------------------------------- Hypothesis ---------------------------------- Falsifying example: test_mean( ls=[9.9792015476736e+291, 1.7976931348623157e+308], ) =========================== short test summary info ============================ FAILED test_hypothesis.py::test_mean - assert inf <= 1.7976931348623157e+308 ============================== 1 failed in 0.44s ===============================
In the list
[9.9792015476736e+291, 1.7976931348623157e+308], the calculation of the mean givesinf, andinfis not less than the larger of the two numbers.
Example with regular expressions¶
In the following example, we attempt to extract the
usernameanddomainwith a regular expression from an email address using:import re def parse_email(email): result = re.match(r"(?P<username>\w+).(?P<domain>[\w\.]+)", email).groups() return result
Now let’s write a test called
test_parse_email()to check our function. We’ll use the emails strategy from Hypothesis as our input values. As a result, we expect, for example, that forveit@cusy.io, theusernameis veit and thedomainis cusy.io.In our test, we assume, on the one hand, that two entries are always returned and that the second entry contains a full stop (
.):from hypothesis import given from hypothesis.strategies import emails @given(emails()) def test_parse_email(email): result = parse_email(email) # print(result) assert len(result) == 2 assert "." in result[1]
Now let’s run the test:
$ uv run pytest docs/test/test_emails.py ============================= test session starts ============================== platform darwin -- Python 3.13.0, pytest-9.0.3, pluggy-1.6.0 rootdir: /Users/veit/cusy/trn/python-basics-tutorial-de configfile: pyproject.toml plugins: hypothesis-6.152.1 collected 1 item docs/test/test_emails.py F [100%] =================================== FAILURES =================================== _______________________________ test_parse_email _______________________________ + Exception Group Traceback (most recent call last): | File "/Users/veit/cusy/trn/python-basics-tutorial-de/docs/test/test_emails.py", line 12, in test_parse_email | def test_parse_email(email): | ^^^ | File "/Users/veit/cusy/trn/python-basics-tutorial-de/.venv/lib/python3.13/site-packages/hypothesis/core.py", line 2264, in wrapped_test | raise the_error_hypothesis_found | ExceptionGroup: Hypothesis found 2 distinct failures. (2 sub-exceptions) +-+---------------- 1 ---------------- | Traceback (most recent call last): | File "/Users/veit/cusy/trn/python-basics-tutorial-de/docs/test/test_emails.py", line 16, in test_parse_email | assert '.' in result[1] | AssertionError: assert '.' in '0' | Falsifying example: test_parse_email( | email='0/0@A.AC', | ) +---------------- 2 ---------------- | Traceback (most recent call last): | File "/Users/veit/cusy/trn/python-basics-tutorial-de/docs/test/test_emails.py", line 13, in test_parse_email | result = parse_email(email) | File "/Users/veit/cusy/trn/python-basics-tutorial-de/docs/test/test_emails.py", line 8, in parse_email | result = re.match(r"(?P<username>\w+).(?P<domain>[\w\.]+)", email).groups() | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | AttributeError: 'NoneType' object has no attribute 'groups' | Falsifying example: test_parse_email( | email='/@A.AC', | ) +------------------------------------ =========================== short test summary info ============================ FAILED docs/test/test_emails.py::test_parse_email - ExceptionGroup: Hypothesis found 2 distinct failures. (2 sub-exceptions) ============================== 1 failed in 0.14s ===============================
The email address provided by Hypothesis,
0/0@A.ac, shows that our regular expression in theparse_email()method is not yet sufficient. We will therefore adjust our regular expression and then run the test again:--- /home/runner/work/python-basics-tutorial/python-basics-tutorial/docs/test/test_emails.py +++ /home/runner/work/python-basics-tutorial/python-basics-tutorial/docs/test/test_emails_2.py @@ -5,7 +5,9 @@ def parse_email(email): - result = re.match(r"(?P<username>\w+).(?P<domain>[\w\.]+)", email).groups() + result = re.match( + r"(?P<username>[\.\w\-\!~#$%&\|{}\+\/\^\`\=\*']+).(?P<domain>[\w\.\-]+)", email + ).groups() return result
$ uv run pytest docs/test/test_emails_2.py ============================= test session starts ============================== platform darwin -- Python 3.13.0, pytest-9.0.3, pluggy-1.6.0 rootdir: /Users/veit/cusy/trn/python-basics-tutorial-de configfile: pyproject.toml plugins: hypothesis-6.152.1 collected 1 item docs/test/test_emails_2.py . [100%] ============================== 1 passed in 0.29s ===============================
Third-party extensions¶
There are a number of open-source libraries that extend Testing capabilities. Some of these are listed on Third-party extensions; you can find more on PyPI by searching for keywords or using the framework classifier.