Skip to main content

Hegel for Python

Property-based testing for Python, built on the Hegel engine (libhegel). The engine generates inputs, shrinks failures, keeps an example database and prints the failing test case; this package binds it to Python through a pyo3 extension module and provides the test-writing API.

The API is the Hegel API, shared in shape with the Rust, Go and TypeScript bindings: tests take a TestCase and draw values from generators. How it compares with Hypothesis is set out in docs/comparison-with-hypothesis.md; engine behaviour that differs from Python's re or from Hypothesis is recorded in docs/engine-issues.md.

Installation

Hegel for Python is the hegeltest package, imported as hegel. It needs Python 3.11 or later and, until wheels are published, a Rust toolchain:

pip install git+https://github.com/hegeldev/hegel-python

Quickstart

import hegel
from hegel import generators as gs


@hegel.test
def test_sorting_is_idempotent(tc):
    xs = tc.draw(gs.lists(gs.integers()))
    assert sorted(sorted(xs)) == sorted(xs)

The test runs under pytest (or by calling it). When it fails, Hegel shrinks the input and reports the minimal failing example, named after the variables the draws were assigned to:

Falsifying example:
xs = [0, 0]

Draw names

A draw assigned to a plain variable (xs = tc.draw(...)) is printed under that name. A draw made in a loop, or assigned to a name that is assigned more than once in the test, is numbered (x_1, x_2, ...), and a draw that is not assigned to a variable is printed as draw_1, draw_2, and so on, as in the Rust library:

@hegel.test
def test_totals(tc):
    n = tc.draw(gs.integers(0, 10))
    for _ in range(n):
        x = tc.draw(gs.integers())
    total = tc.draw(gs.integers()) + tc.draw(gs.integers())
    ...
Falsifying example:
n = 2
x_1 = 0
x_2 = 0
draw_1 = 0
draw_2 = 0

tc.draw(generator, name="...") gives a draw an explicit (always numbered) name, and tc.draw_silent(generator) draws without printing. Draws made inside a composite generator are not printed separately.

Generators

hegel.generators provides generators for integers, floats, booleans, text (by codec, category or alphabet, and from regular expressions), bytes, dates and times, UUIDs, IP addresses, emails, URLs and domains, lists, sets, dicts, tuples, permutations and samples, and combinators: map, filter, flat_map, one_of (or |), optional, just, sampled_from, recursive, deferred, composite, builds and from_type.

@hegel.composite
def sorted_pairs(tc, limit):
    a = tc.draw(gs.integers(0, limit))
    b = tc.draw(gs.integers(a, limit))
    return (a, b)

Test case methods

Besides draw, a test case has assume(condition) and reject() to discard the current input, note(message) to add a line to the failure report, target(score, label) to steer generation toward higher scores, event(label) and event_value(label, value) for the run's statistics, and repeat(body), which runs body an engine-chosen number of times and then ends the test case.

Configuration

@hegel.test(test_cases=1000, seed=42) sets options for one test. Settings cover the number of test cases, verbosity, seeding and derandomization, the example database, phases, health checks, multiple-failure reporting and the random backend.

Settings start from a profile. The engine provides development (the default locally), ci (chosen automatically on a CI server) and workload (under Antithesis); a project can pick one in a hegel.toml file or with the HEGEL_DEFAULT_PROFILE environment variable. Your own profiles are registered in Python:

hegel.Settings.register_profile("thorough", test_cases=10_000)
hegel.Settings.set_default_profile("thorough")

Failing examples are saved in .hegel/examples and replayed first on the next run. database="path" keeps them elsewhere, and database="" turns the database off.

Under pytest, --hegel-profile, --hegel-seed, --hegel-verbosity, --hegel-derandomize and --hegel-database adjust every Hegel test in the run.

Explicit test cases and reproduction

@hegel.explicit_test_case(xs=[1, 1])
@hegel.test
def test_no_duplicates(tc):
    xs = tc.draw(gs.lists(gs.integers()))
    ...

A failure report ends with a @hegel.reproduce_failure("...") line; add that decorator below @hegel.test to replay exactly that failure.

Stateful testing

class Counter(hegel.StateMachine):
    def __init__(self):
        self.count = 0

    @hegel.rule()
    def increment(self, tc):
        n = tc.draw(gs.integers(1, 10))
        self.count += n

    @hegel.invariant()
    def non_negative(self, tc):
        assert self.count >= 0


test_counter = hegel.state_machine_test(Counter)

Rules and invariants take the test case and draw their own values. The machine's constructor takes either no arguments or the test case.

hegel.Pool holds values that rules produce for later rules to use, with generators that draw a value and leave it in the pool or take it out:

class Files(hegel.StateMachine):
    def __init__(self, tc):
        self.handles = hegel.Pool(tc)

    @hegel.rule()
    def open(self, tc):
        name = tc.draw(gs.text(min_size=1))
        self.handles.add(open_file(name))

    @hegel.rule()
    def close(self, tc):
        handle = tc.draw(self.handles.values_consumed())
        handle.close()

Setting max_concurrency above 1 on the class runs the rules from several threads at once, in rounds grouped by the rules' group, with Hegel choosing the concurrency level and attributing the output to each worker; hegel.ConcurrentPool is the pool to share between them.

Coming from Hypothesis

A Hypothesis test maps onto a Hegel one draw by draw:

# Hypothesis                                  # Hegel
@given(st.lists(st.integers()))               @hegel.test
def test_reversing_twice(xs):                 def test_reversing_twice(tc):
    ...                                           xs = tc.draw(gs.lists(gs.integers()))
                                                  ...

docs/comparison-with-hypothesis.md lists the correspondences and the differences. Hypothesis's own test suite, ported to the Hegel API, runs under tests/hypothesis_port.

Development

just setup        # create the virtualenv and build the extension
just check        # formatting, lint, types, tests with coverage, the ported suite

Metadata

Release files for hegeltest 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hegeltest 0.1.0
File Size Uploaded Provenance
hegeltest-0.1.0.tar.gz 266.4 kB Without provenance Details

Built distributions (wheels)

Table of built distributions (wheels) for hegeltest 0.1.0
File
hegeltest-0.1.0-cp314-cp314-macosx_11_0_arm64.whl CPython 3.14 CPython 3.14 macOS 11.0+ ARM64 Details
hegeltest-0.1.0-cp313-cp313-macosx_11_0_arm64.whl CPython 3.13 CPython 3.13 macOS 11.0+ ARM64 Details
hegeltest-0.1.0-cp312-cp312-macosx_11_0_arm64.whl CPython 3.12 CPython 3.12 macOS 11.0+ ARM64 Details
hegeltest-0.1.0-cp311-cp311-macosx_11_0_arm64.whl CPython 3.11 CPython 3.11 macOS 11.0+ ARM64 Details

Total release size: 7.9 MB

Release files / hegeltest-0.1.0.tar.gz

Download URL hegeltest-0.1.0.tar.gz
Size 266.4 kB
Tags Source
SHA-256 checksum
How to use checksums
8ada34759e87797fd03adc4ebba2e3df721a6869a0ac802876bb041bd73f05de
BLAKE2b-256 checksum
How to use checksums
492f3830ce572b3c2c0966c4f99e75ec913780ff6fa7ca70e3b0ab1e684e524d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / hegeltest-0.1.0-cp314-cp314-macosx_11_0_arm64.whl

Download URL hegeltest-0.1.0-cp314-cp314-macosx_11_0_arm64.whl
Size 1.9 MB
Tags CPython 3.14 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
9377ab15c6c120bf93e6fefcceab0ea0bf9c947d68d084deb8913cb09ba902b3
BLAKE2b-256 checksum
How to use checksums
719db2d99c4bf121aecc7bec5c29fb963adb23edc2faad8e918b9637d6c59ba7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / hegeltest-0.1.0-cp313-cp313-macosx_11_0_arm64.whl

Download URL hegeltest-0.1.0-cp313-cp313-macosx_11_0_arm64.whl
Size 1.9 MB
Tags CPython 3.13 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
59c2340e3bb4cde1886b7685abbbf418e086ef59fff12633c623efb363ad73c6
BLAKE2b-256 checksum
How to use checksums
7a1ed5b979c14f96d758c21d1ebc2115c9cede953e629977f3de6f685f6a9bf1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / hegeltest-0.1.0-cp312-cp312-macosx_11_0_arm64.whl

Download URL hegeltest-0.1.0-cp312-cp312-macosx_11_0_arm64.whl
Size 1.9 MB
Tags CPython 3.12 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
871aae98a108cde0257d26f806693b7dc28c84ed764bb0722ea542d3f45172b9
BLAKE2b-256 checksum
How to use checksums
49fb0370847db4682ec9ada45a2901dcdfd4b801e23bb09893bd9f5aa958287e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / hegeltest-0.1.0-cp311-cp311-macosx_11_0_arm64.whl

Download URL hegeltest-0.1.0-cp311-cp311-macosx_11_0_arm64.whl
Size 1.9 MB
Tags CPython 3.11 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
2683ca93381f3afcba802799f3ec25c1f69ee010c5193fe96c1ea5b002cc5f74
BLAKE2b-256 checksum
How to use checksums
d0225828afc826d1c31af0575d89e54e9500dd590a422f7149d2650d32adb383
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for hegeltest 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page