hydo-assert for Python
hydo runs a whole system in a deterministic virtual machine, injects faults (process kills, network faults, power cuts, disk errors) and replays every failure bit for bit. This package lets a Python program that runs under hydo say, from the inside, what must be true and what should happen. hydo collects what the program reports, and an exploration (many runs with different faults) reports each property as passed, failed (with a schedule that replays the failure) or never exercised.
The package uses only the standard library and needs Python 3.8 or later. Outside hydo it does almost nothing: the records go to standard error (or nowhere), and fault points stay off.
Install
pip install hydo-assert
The distribution is hydo-assert; the module is hydo_assert. In a guest image without pip,
copy the folder hydo_assert onto the program's PYTHONPATH.
A complete program
A small key/value store that declares three properties and checks them:
import hydo_assert as ha
READ_BACK = ha.always("kv.read_back", "every acknowledged write reads back with its value")
OVERWRITE = ha.sometimes("kv.overwrite", "a write replaced an existing value")
LOST_REPLY = ha.buggify("kv.lost_reply", "the write lands but its reply is lost")
def main():
ha.catalog() # list every property, also the ones that never run
store = {}
ha.setup_complete() # the system is ready: hydo may start injecting faults
for key, value in [("a", 1), ("b", 2), ("a", 3)]:
old = store.get(key)
store[key] = value
OVERWRITE.check(old is not None, {"key": key})
if LOST_REPLY.fire({"key": key}):
continue # the client never hears back (only when hydo turns this point on)
acked = store.get(key) == value
READ_BACK.check(acked, {"key": key, "value": value})
if __name__ == "__main__":
main()
Run it with HYDO_ASSERT_OUT=stderr to see what it reports. It writes these lines (under hydo
they go to the guest's console, where hydo reads them):
HYDO-ASSERT-DECL {"t":"always","id":"kv.read_back","msg":"every acknowledged write reads back with its value"}
HYDO-ASSERT-DECL {"t":"sometimes","id":"kv.overwrite","msg":"a write replaced an existing value"}
HYDO-ASSERT-DECL {"t":"sometimes","id":"buggify.kv.lost_reply","msg":"fault point fired: the write lands but its reply is lost"}
HYDO-SETUP-COMPLETE
HYDO-ASSERT {"t":"always","id":"kv.read_back","ok":true,"msg":"every acknowledged write reads back with its value","details":{"key":"a","value":1}}
HYDO-ASSERT {"t":"sometimes","id":"kv.overwrite","ok":true,"msg":"a write replaced an existing value","details":{"key":"a"}}
A process writes each outcome of a property once: the first pass and the first failure, with
the details of that evaluation. That is why the later passes of kv.read_back write nothing.
How to
Declare properties and check them
A property has an id (stable across versions of your program) and a message for people. Its kind says what hydo expects:
| kind | the property holds when | hydo reports a failure when |
|---|---|---|
always |
the condition is true every time, and it is checked at least once | it is false once, or never checked |
always_or_unreachable |
the condition is true every time it is checked | it is false once |
sometimes |
the condition is true at least once in an exploration | it is never true |
reachable |
the code is reached at least once in an exploration | it is never reached |
unreachable |
the code is never reached | it is reached |
Declare properties at module level, so that the catalog lists them once the module is imported:
import hydo_assert as ha
STORED = ha.always("kv.stored", "every acknowledged write is stored")
PARSES = ha.always_or_unreachable("kv.log_parses", "every complete log line parses")
RETRIED = ha.sometimes("kv.retried", "a client retried a request")
RECOVERED = ha.reachable("kv.recovered", "the server replayed a non-empty log")
UNKNOWN = ha.unreachable("kv.unknown_command", "the server got a command it does not know")
STORED.check(True, {"key": "a"}) # returns the condition
PARSES.check(True) # the details are optional
RETRIED.check(False) # a false `sometimes` writes nothing
RECOVERED.hit({"entries": 3})
command = "GET"
if command not in ("GET", "SET"):
UNKNOWN.hit({"command": command})
Compare two values
cmp checks left op right and puts both values and the operator into the details, so a
failure shows them:
import hydo_assert as ha
LOG_SIZE = ha.always("kv.log_size", "the log stays under its cap")
size = 12
LOG_SIZE.cmp(size, "<=", 10000) # details: {"left":12,"op":"<=","right":10000}
The operator is one of <, <=, ==, !=, >= and >.
Check that something happens in time (eventually)
An eventually property says that after this point, the condition becomes true within a
deadline. For example: after a restart, the server answers within 5 seconds. Time inside a hydo
guest is deterministic, so the verdict is the same on every replay.
import time
import hydo_assert as ha
BACK = ha.eventually("kv.back", "the server answers within 5 s of its start", 5.0) # seconds
started = time.monotonic()
def answers():
return time.monotonic() - started >= 0.02 # stands for a real request
assert BACK.await_cond(answers, poll=0.005) # polls every 5 ms
# Or mark the start and the end yourself:
watch = BACK.begin()
# ... the server comes up ...
watch.done() # or watch.expire() when you know it will not
await_cond blocks the thread. In asyncio code use await_async, whose condition may be a
plain function or a coroutine function:
import asyncio
import hydo_assert as ha
BACK = ha.eventually("kv.back_async", "the server answers within 5 s", 5.0)
async def answers():
return True
async def main():
assert await BACK.await_async(answers, 0.1)
asyncio.run(main())
Add a fault point (BUGGIFY)
A fault point is a place where your program misbehaves on purpose: it drops a reply, fails a
write, waits too long. Normally the point does nothing (fire returns false). hydo can turn
points on for a run, so that faults happen where only your program can make them. FoundationDB
calls this technique BUGGIFY.
import time
import hydo_assert as ha
LOST_REPLY = ha.buggify("kv.lost_reply_point", "the write lands but its reply is lost")
SLOW_DISK = ha.buggify("kv.slow_disk", "a write takes 10 ms longer")
reply = "OK"
if LOST_REPLY.fire():
reply = None # misbehave: the client never gets the reply
if SLOW_DISK.fire({"key": "a"}):
time.sleep(0.01)
print(reply)
To let hydo turn the points on, add buggify: true to the project's hydo.yaml and explore
with hydo explore --buggify. Each run then gets a seed that turns some points on, and a point
that is on fires on some of its calls. To try it outside hydo, set
HYDO_BUGGIFY=<seed in hex>,<points on, per mille>,<calls that fire, per mille>; for example
HYDO_BUGGIFY=2a,1000,1000 makes every point fire every time.
Record an operation history
hydo can check a whole history of operations after a run. It checks that every read is
consistent with the writes before it (linearizability per key), and that no list append goes
missing. Your workload's
client records each operation twice: when it starts (invoke) and when the outcome is known.
from hydo_assert import history as hh
kv = hh.History("kv")
write = kv.invoke(3, "write", "k1", hh.enc("v1")) # client 3, before the request
# ... send the request, then record the outcome:
write.ok() # it happened; fail(): it did not; info(): the outcome is unknown
read = kv.invoke(3, "read", "k1")
read.ok("k1", hh.value_or_nil("v1"))
Then name the history in the project's hydo.yaml:
checks:
- id: kv.linearizable
type: linearizable
model: register
history: kv
msg: every read sees the latest acknowledged write
hydo guide checks describes the checks and their options.
Choose where the records go
Under hydo the records go to the guest's console (/dev/ttyS0 for properties, /dev/console
for histories), which hydo reads even when the program's standard output goes to a log file.
Outside hydo, where the console cannot be opened, they go to standard error. Two variables
change this, and each is read once, at the first record:
HYDO_ASSERT_OUT: a path of an existing file,stderr,off, orlogging(each record logged at INFO on the loggerhydo_assert) for the properties;HYDO_HIST_OUT: a path (created if missing),stderr, orofffor the histories.
Run a Python program in a hydo guest
Set PYTHONHASHSEED=0 so that the program does not depend on a random hash seed, and
PYTHONDONTWRITEBYTECODE=1 because the guest's root file system is read-only. A complete
project with a hydo.yaml is the python-kv example:
https://github.com/kakoc/hydo/tree/main/hydo/onboard/examples/python-kv.
Reference
- The API: the docstrings of
hydo_assertandhydo_assert.history(help(hydo_assert)). - The records, and how hydo judges each property type: https://github.com/kakoc/hydo/blob/main/hydo/ASSERTIONS.md.
- The history format and the checkers: https://github.com/kakoc/hydo/blob/main/hydo/HISTORIES.md.
- The same SDK for Rust, JavaScript and Go writes the same records, byte for byte: https://github.com/kakoc/hydo/tree/main/sdk.
Rules of this package:
- The catalog (
catalog()) lists the properties in the order they were declared, which for module-level declarations is the import order. Call it once, early, in the process whose console hydo reads. - Details are encoded as Go's
encoding/jsonencodes the same values: keys in UTF-8 byte order, a tuple or a set as an array,bytesas base64, any other object asstr(obj). Details that are not a mapping are wrapped as{"value": ...}. A record whose details hold a NaN or an infinity is not written. The caller's dict is never changed. - Every call is safe from threads and from asyncio code: one lock, never held across a blocking
call or an
await. Afteros.fork()the child keeps the parent's record of what was written. - A history's process is an int or a token without spaces; its arguments are tokens (encode
free-form values with
enc). Versions are int64 values. A process has at most one operation in flight.
License
Apache-2.0.
Metadata
Release files for hydo-assert 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| hydo_assert-0.3.0.tar.gz | 35.9 kB | With provenance | Details for hydo_assert-0.3.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| hydo_assert-0.3.0-py3-none-any.whl | Python 3 | none | any | Details for hydo_assert-0.3.0-py3-none-any.whl |
Total release size: 61.3 kB
Release files / hydo_assert-0.3.0.tar.gz
| Download URL | hydo_assert-0.3.0.tar.gz |
|---|---|
| Size | 35.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
0560b50bd7fd822001bc6b2a01054dd0e5ca5f947c9f12504f5f159ff0d69a87
|
|
BLAKE2b-256 checksum How to use checksums |
77353f8c5d3d753027ac82d114ed26a6bf1e2a786dd1ac64e55417ee740062ad
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / hydo_assert-0.3.0-py3-none-any.whl
| Download URL | hydo_assert-0.3.0-py3-none-any.whl |
|---|---|
| Size | 25.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e00a7dc2a3fe731d386af03c8980c8ce9d83575fbe325433a14090d14809ba2c
|
|
BLAKE2b-256 checksum How to use checksums |
63f9501845a78466348b5d57f3bdb37ccf335cc9d851ba175377b6378b2feb23
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for hydo-assert 0.3.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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for hydo_assert-0.3.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA 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.
To learn more about protecting yourself from malicious actors, visit our package security documentation.