z80-python
A readable, pure-Python Z80 instruction-core reference implementation.
z80-python is a processor core built to be read, learned from, embedded in
real machines, and inspected by humans and AI tools. It passes every oracle it
is checked against, from values captured on real Zilog silicon (z80test,
ZEXALL) to the emulator-derived SingleStepTests corpus, each named with its
tier below. It implements the Z80 instruction set and deterministic processor
lifecycle at instruction boundaries while leaving memory maps, devices, and
machine scheduling to the host.
The project is deliberately:
- readable — every instruction is an ordinary Python method, not generated code, a native extension, or an opaque optimization; tables only route each opcode to its method, which cites the manual page its rule comes from;
- pure Python — the core has no runtime dependencies and supports CPython and PyPy;
- independently validated — correctness claims come from external test oracles, not from code-generation confidence;
- embeddable — a host passes in its memory (and, optionally, I/O) as callables and controls when the processor advances; and
- inspectable — processor state, disassembly, bounded debugging with
breakpoints, watchpoints and bus-access tracking, structured traces, and
python -m z80_pythonfor stepping a binary make execution explainable without contaminating the hot core path.
Validation
Each oracle below is named with its tier: where its expected values came from. A lower tier detects; the highest tier that checks a claim decides it (oracle tiers). The instruction core passes:
- hardware-captured: three of raxoft/z80test's programs (
z80full,z80memptr,z80ccf), whose values were recorded on a real 48K ZX Spectrum's Zilog NMOS Z80: every flag including the undocumented X/Y, WZ (MEMPTR), and SCF/CCF's Q-dependent behavior, for the instructions it covers; - hardware-captured, coarse: ZEXDOC and ZEXALL, whose per-group CRCs were "found empirically on a real Z80", under CPython and PyPy; they cannot see WZ, Q, R, I/O or T-states;
- emulator-derived: all 1,604 files of the pinned
SingleStepTests/z80corpus, 1,604,000 cases covering registers, flags, memory, I/O, T-states,R, WZ, Q and the order of memory accesses, generated by a port of the Ares emulator; - emulator-derived: FUSE 1.6.0's Z80 core test set, 1,356 cases, of which 1,350 agree and the six that do not are each decided by a higher-tier source the core follows instead; and an interrupt-lifecycle cross-check against the independent superzazu/z80 core;
- documentation: runs of DD/FD prefixes, from Young's The Undocumented Z80 Documented; and every handler's rule, cited to a page of Zilog's manual or a section of Young's in its docstring.
The order of memory accesses has no hardware-captured oracle at all: it rests on SingleStepTests and FUSE agreeing.
Release 0.4.0 was certified against every one of these, on both CPython and
PyPy, at commit b1720b6. Exact source revisions, hashes, commands, timings,
and scope limits are recorded in the validation evidence.
This is an instruction-level semantic and lifecycle claim. It is not a claim of cycle-accurate bus-pin behavior or of a complete computer.
CI coverage
The two badges cover different things, and neither covers everything. A green CI badge alone would not mean the oracle gates above passed, so they are separated rather than implied:
| Badge | Runs | When |
|---|---|---|
| CI | the fast suite, Ruff check and format, the example host, a wheel build and installed-API smoke test, on CPython 3.12/3.13/3.14 and PyPy 3.11; plus the FUSE oracle | every push and pull request |
| Oracles | SingleStepTests (1,604 files, 1,604,000 cases), z80test (z80full, z80ccf, z80memptr), FUSE, and the superzazu/z80 interrupt cross-check, each against its pinned corpus |
weekly, and on demand |
ZEXDOC and ZEXALL are certified locally, not in CI. They need
zexdoc.com/zexall.com, which no script here fetches, and take roughly 90
minutes each under CPython. Their revisions, file hashes, commands and timings
are in the validation evidence; treat that record, not a
badge, as the citation for those two gates.
Vibe coded, oracle validated
This project was developed with extensive AI assistance—“vibe coded” in the colloquial sense. That history is intentional and worth stating plainly: the implementation demonstrates what AI-assisted engineering can produce when the feedback loop is stronger than the model's confidence.
Generated emulator code can be plausible and wrong, especially around prefixes, undocumented flags, WZ/MEMPTR, Q, and refresh behavior. The project therefore does not treat AI output or code review as proof. Independent SingleStep vectors, focused regressions, ZEX CRCs, and real emulator integrations are the authority. See AI-assisted development and validation.
Version status
The current release is 0.4.0 (see its release note
and the changelog). It changes how a host embeds the CPU; the
changelog's Breaking section has the one-line migration.
| Source | Status | Contents |
|---|---|---|
PyPI and GitHub v0.4.0 |
Current release | Callable embedding contract, 2.5x speed, every handler cited, oracle tiers, bus-access tracking and watchpoints |
PyPI and GitHub v0.3.0 |
Previous release | API hardening and advanced trace diagnostics |
GitHub v0.2.0 |
Milestone | RESET, CPU state, disassembly, and debugger foundations |
Install from PyPI
python -m pip install z80-python
Install the source tree
git clone https://github.com/alewman/z80-python.git
cd z80-python
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install -e .
Recent Debian, Ubuntu, Fedora, and Homebrew Pythons refuse pip install into
the system interpreter (PEP 668); the virtual environment line above is the
supported way around that.
For tests, linting, and development tools:
python -m pip install -e ".[dev]"
The distribution is named z80-python; its public import is z80_python to
avoid ambiguity with the unrelated z80 distribution on PyPI.
Minimal host
The host owns memory and devices and passes the CPU its buses as callables. The core always passes a 16-bit address and an 8-bit value, so a bytearray's own methods are a complete memory:
from z80_python import Z80CPU
memory = bytearray(0x10000)
cpu = Z80CPU(memory.__getitem__, memory.__setitem__) # read_byte, write_byte
memory[:3] = bytes((0x3E, 0x2A, 0x3C)) # LD A,2Ah; INC A
assert cpu.step() == 7
assert cpu.step() == 4
assert cpu.a == 0x2B
Devices on the I/O bus come in as read_port= and write_port=; without them
IN reads 0xFF and OUT goes nowhere. This is the embedding contract of the
whole family: m6800-python takes its
bus the same way. start-here has
the rest. Until 0.4.0 a host subclassed Z80CPU and defined the four bus
methods; that form now raises a TypeError naming this one.
step() advances one instruction or accepted lifecycle boundary and returns its
documented T-state total. Registers and modeled processor state are directly
readable and writable. decode_and_execute() remains the historical
instruction-only entry point; new hosts should normally use step().
Processor lifecycle
The core models external processor inputs as deterministic transitions between instructions:
- RESET is host-controlled and level-sensitive. While asserted, each
step()applies the documented processor-state effects in 3 T-states without fetching an instruction or accessing the stack. - NMI is accepted at the next available boundary, wakes HALT, preserves the
prior
IFF1inIFF2, pushesPC, and enters0x0066in 11 T-states. - Maskable interrupts honor
IFF1, the one-instructionEIdelay, HALT, and interrupt modes 0, 1, and 2. IM 0 is intentionally bounded to device-suppliedRSTopcodes.
RESET has priority over NMI; NMI has priority over an acceptable maskable
interrupt. Hosts schedule devices from returned T-state totals and request
interrupts through the public lifecycle API rather than mutating PC, SP, or
interrupt flip-flops to synthesize entry.
See the interrupt lifecycle contract for exact state transitions and exclusions.
Reference-core boundary
z80-python owns:
- Z80 instruction semantics and documented instruction T-state totals;
- registers, flags, alternate registers, and modeled internal processor state;
- RESET, NMI, maskable interrupts, EI delay, RETN, and HALT behavior; and
- processor-level observation and debugging values.
A host machine owns:
- ROM, RAM, memory maps, ports, mappers, and open-bus behavior;
- video, audio, input, and other devices;
- frame, scanline, clock, and interrupt scheduling;
- device events and side-effect-free memory peeking; and
- complete machine save states, deterministic replay, and rewind.
Accordingly, the project does not claim:
- cycle-accurate bus cycles, contention,
WAIT,BUSREQ, or pin timing; - interrupt-acknowledge bus callbacks, daisy chains, or arbitrary IM 0 injected instructions;
- a complete CP/M system, arcade board, console, or computer; or
- a universal machine scheduler or save-state format.
These are scope boundaries, not unfinished promises. They keep the core readable, portable, and useful across different machines.
Learning and inspection
New to the Z80? Read Start here first: the register file, the flag byte, the opcode bit fields every handler decodes, the prefix model, and the three internal registers (WZ, Q, R) that only emulator authors meet.
The implementation is organized by instruction family behind a small public
Z80CPU facade. Every opcode handler's docstring starts with its Zilog
mnemonic, so grep DJNZ src/ lands on the implementation, and ends with where
its rule comes from: a page of Zilog's Z80 CPU User Manual or a section of
Young's Undocumented Z80 Documented, plus the SingleStepTests file (and,
where one exists, the z80test program) that pins any WZ or Q rule neither
manual covers. The hardware reason behind each undocumented flag or MEMPTR
effect is a comment on the line that encodes it. A test
(tests/test_readability.py) enforces the mnemonic and the citations the same
way the vector gate enforces correctness.
The development tree also provides immutable CPUState capture and restoration
for processor-owned state and a complete structured disassembler for every opcode
form supported by the core. Disassembly requires an explicit side-effect-free byte
reader: debugging must not accidentally acknowledge or mutate a mapped device.
These APIs make the project useful as:
- a reference while learning Z80 instructions and flags;
- a tested foundation for emulators and machine experiments;
- a source of deterministic examples and regression witnesses; and
- an executable environment where an AI agent can inspect real state instead of guessing from source code alone.
See CPU state, disassembly, and undocumented behavior.
Conformance for ports in other languages
A manifest pins a machine (memory, initial state, host profile, interrupt events, stop rule); the reference core produces a trace for it, and a core in any language can be diffed against that trace after every instruction, with T-states and all 28 processor-state fields compared:
python -m z80_python.conformance trace examples/conformance/interrupts.json --out ref.jsonl
python -m z80_python.conformance diff examples/conformance/interrupts.json yours.jsonl
See conformance for the host contract and the certification ladder, and the trace schema for the record format.
Advanced diagnostics and tooling
The following development-tree features support emulator diagnosis but are secondary to the instruction core itself:
DebugSessionwraps an existing host with bounded execution, execute breakpoints, lifecycle-aware records, T-state totals, and bounded history. It adds no callback or history overhead when unused.CommandDebuggeris a dependency-free text-stream frontend for registers, stepping, bounded runs, breakpoints, disassembly, memory display, and history.- Trace comparison finds the first differing instruction, lifecycle boundary, timing result, or individual CPU-state field across two executions.
- Versioned JSON Lines persistence allows large traces to be written, reloaded, and compared incrementally without buffering complete runs.
- Live lockstep comparison can advance two machines under a finite budget and stop at the causal processor-visible difference while preserving bounded pre-divergence context.
A Galaxian integration proved the intended diagnostic behavior: two otherwise
identical boards differed only in their vblank NMI-enable latch. Comparison
stopped at the exact boundary where one scheduler first asserted pending NMI—one
step before the later control-flow divergence into 0x0066.
These are deterministic tools, not an embedded LLM or AI provider. Any human UI, agent, MCP adapter, or model can consume the same structured contracts. See debug sessions, trace comparison, and the tooling roadmap.
Development
Run the ordinary quality gate:
python -m pytest -q
python -m ruff check .
python examples/minimal_z80_host.py
The vector corpus and ZEX binaries are external artifacts and are intentionally not bundled. Fetch the pinned vector corpus with:
python scripts/fetch_test_vectors.py
python -m pytest -q
ZEX recertification is reserved for release candidates and semantic-core changes; reproduction instructions and finite execution budgets are documented in the validation evidence.
Project records
- 0.4.0 release notes
- 0.3.0 release notes
- Validation evidence and scope
- Public API stability
- AI-assisted development and validation
- Extraction provenance
- Interrupt lifecycle
- CPU state
- Disassembly
- Debug sessions
- Trace comparison
- Conformance for other cores
- Trace schema
- Start here: Z80 primer
- Undocumented behavior
- Debugging and agent-tooling roadmap
- Contribution guidance
License
MIT. External validation artifacts retain their own licenses and are not bundled in this distribution.
Metadata
Release files for z80-python 0.4.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 | |
|---|---|---|---|
| z80_python-0.4.0.tar.gz | 214.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| z80_python-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 279.1 kB
Release files / z80_python-0.4.0.tar.gz
| Download URL | z80_python-0.4.0.tar.gz |
|---|---|
| Size | 214.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8bdb3052d8f1a409a07f5f92022a5af65f0d5a59b4e30b10f4f83431a43bd59e
|
|
BLAKE2b-256 checksum How to use checksums |
a8045a0409f0bfc7fb553ba3d1f375902db832f2b2157fa2357cb5825f121c78
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 21, 2026.
Transparency logRelease files / z80_python-0.4.0-py3-none-any.whl
| Download URL | z80_python-0.4.0-py3-none-any.whl |
|---|---|
| Size | 65.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a06a7136ff2aa86df9d3843d5cff99df63ba165061c38dd2dcdaa1a506169866
|
|
BLAKE2b-256 checksum How to use checksums |
aafe13e6234a9f6977a37bc96e0a23e11314a2f1c9d19e0afa6151820488c32a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Sep 21, 2026.
Transparency log