Skip to content

zttp

zttp

A sans-IO HTTP parser for Python with a Zig core! ⚡


Source Code: https://github.com/Kludex/zttp


zttp is a sans-IO HTTP parser whose engine is written in Zig. It speaks HTTP/1.1, HTTP/2, and HTTP/3, and it does no I/O of its own: you feed it bytes and pull out events, and you ask it for bytes to send. It never touches a socket, so it works with any I/O you like (asyncio, threads, a green-thread library, a test harness), and it's a joy to test.

It's the same idea as h11, with a hand-written Zig engine underneath instead of pure Python. It has no dependencies.

The key features are:

  • Sans-IO: a clean, event-based API. Feed bytes with receive_data, pull Request / Data / EndOfMessage events with next_event. No callbacks, no sockets, no surprises.
  • HTTP/1.1, HTTP/2, and HTTP/3: the same event API for all three. Pass protocol=zttp.HTTP2 and you get multiplexed streams, HPACK, and flow control (see HTTP/2); pass protocol=zttp.HTTP3 and a from-scratch QUIC transport feeds the same events from UDP datagrams (see HTTP/3).
  • Fast: a hand-written Zig engine with branch-light scanning and minimal allocation (see Performance for the numbers).
  • Safe: strict by default. It defends against request smuggling, rejects bare LF line endings, bounds every buffer, and ships in Zig's safety-checked build.
  • Typed: a py.typed package with full type hints. Your editor knows every event field.
  • Tested: a high-level test suite at 100% coverage, plus the Zig core's own tests and an adversarial-input fuzz harness.

Experimental

zttp is experimental. The API and behaviour may change at any time, and it is not yet ready for production use.

Installation

uv add zttp

Requirements

zttp needs CPython 3.10+ and runs on Linux, macOS, and Windows.

Example

Let's parse an HTTP request. You play the server: bytes come in, events come out. A Connection takes your role - SERVER to receive requests or CLIENT to receive responses.

parse.py
import zttp

conn = zttp.Connection(zttp.SERVER)

conn.receive_data(
    b"GET /hello?name=you HTTP/1.1\r\n"
    b"Host: example.com\r\n"
    b"\r\n"
)

event = conn.next_event()
print(event.method, event.target)
#> b'GET' b'/hello?name=you'

print(conn.next_event())
#> EndOfMessage(trailers=[])

Feed receive_data whatever bytes you have - a whole request, half a request, or a single byte - and zttp buffers and resumes. Each next_event call returns the next complete event, or the NEED_DATA sentinel when it needs more bytes.

Run it:

$ python parse.py

b'GET' b'/hello?name=you'
EndOfMessage(trailers=[])

That's it. The buffering, the header parsing, the body framing: all Zig. 🎉

Tip

Notice there were no callbacks. You don't register on_header / on_body functions and lose control of the flow. You pull events when you are ready. That's what sans-IO means. Read Architecture for the why.

Where to go next

  • First steps


    The 30-second tour of the read side: feed bytes, pull events.

  • HTTP/1.1


    The write side: build requests and responses, get bytes to send.

  • HTTP/2


    The same API, multiplexed: streams, flow control, and the control events.

  • HTTP/3


    The same events again, fed by UDP datagrams through a QUIC transport.

  • Architecture


    What sans-IO is, and why a parser should do no I/O.

  • Performance


    The benchmarks, the methodology, and the honest caveats.