z80-python
A readable, pure-Python Z80 instruction-core reference implementation.
z80-python is a SingleStep-complete and ZEX-certified processor core built to
be read, learned from, embedded in real machines, and inspected by humans and AI
tools. 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 — instruction families are organized as ordinary Python rather than generated tables, native extensions, or opaque optimizations;
- 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 supplies four memory and I/O methods and controls when the processor advances; and
- inspectable — processor state, disassembly, bounded debugging, and structured traces make execution explainable without contaminating the hot core path.
Validation
The instruction core has passed:
- all 1,604 opcode/prefix files in the pinned
SingleStepTests/z80corpus—1,604,000 state transitions covering registers, flags, memory, I/O ordering, T-states, alternate registers,R, WZ/MEMPTR, Q, and undocumented behavior; and - ZEXDOC and ZEXALL long-sequence CRC exercisers under both CPython and PyPy.
The current processor implementation was recertified after the lifecycle and inspection work. 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.
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.3.0.
| Source | Status | Contents |
|---|---|---|
PyPI 0.3.0 |
Published release | Complete validated core and inspection toolkit |
GitHub v0.2.0 |
Published milestone | RESET, CPU state, disassembly, and debugger foundations |
GitHub v0.3.0 |
Current release | API hardening and advanced trace diagnostics |
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 pip install -e .
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
A machine subclasses Z80CPU and supplies its 16-bit memory and I/O spaces:
from z80_python import Z80CPU
class Machine(Z80CPU):
def __init__(self) -> None:
super().__init__()
self.memory = bytearray(0x10000)
self.ports = bytearray(0x10000)
def read_byte(self, addr: int) -> int:
return self.memory[addr & 0xFFFF]
def write_byte(self, addr: int, value: int) -> None:
self.memory[addr & 0xFFFF] = value & 0xFF
def read_port(self, addr: int) -> int:
return self.ports[addr & 0xFFFF]
def write_port(self, addr: int, value: int) -> None:
self.ports[addr & 0xFFFF] = value & 0xFF
cpu = Machine()
cpu.memory[:3] = bytes((0x3E, 0x2A, 0x3C)) # LD A,2Ah; INC A
assert cpu.step() == 7
assert cpu.step() == 4
assert cpu.a == 0x2B
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
The implementation is organized by instruction family behind a small public
Z80CPU facade. Conventional Python control flow keeps opcode behavior easy to
trace from dispatch to implementation, including documented comments around
undocumented Z80 behavior.
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.
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.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
- Undocumented behavior notes
- Debugging and agent-tooling roadmap
- Contribution guidance
License
MIT. External validation artifacts retain their own licenses and are not bundled in this distribution.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file z80_python-0.3.0.tar.gz.
File metadata
- Download URL: z80_python-0.3.0.tar.gz
- Upload date:
- Size: 134.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b5b0bfe912623f2a33517922e2c4e3ebf5ba407c4e251f4181c7b982653dcb0a
|
|
| MD5 |
6b2f4441ac6937976a178289c9ccb51d
|
|
| BLAKE2b-256 |
1a24cb6556b1cba636ad27867e1468b2b46b950437c2e97eae949efb785efa72
|
File details
Details for the file z80_python-0.3.0-py3-none-any.whl.
File metadata
- Download URL: z80_python-0.3.0-py3-none-any.whl
- Upload date:
- Size: 39.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da2107ff71ce62330060a7df2ccf5215af795c14ccbc2b4026172833dc2523d1
|
|
| MD5 |
b1a715182e56c7fdb81b28494b6bc77d
|
|
| BLAKE2b-256 |
981624ef198c60da74d7f33d37f21fe621998c71b745fbef09899c1cb61cd320
|