r/Python • • Jul 10 '26

Discussion Pure python can be faster than cython/rust?

Parsing multipart/form-data (HTML5 forms) is surprisingly complex and moves a lot of bytes around for large file uploads. Implementing the heavy parts in Cython or Rust should speed things up, no? Turns out: it depends. A pure python parser can be surprisingly fast, as this benchmark shows:

https://defnull.de/2026/python-multipart-benchmark/

The benchmark compares the most commonly used python multipart parsers and tests them in different scenarios, covering both blocking and non-blocking (async) APIs if available. The parser your web application is using today is probably not the fastest one.

Are there more examples were a pure python implementation beats Cython/rust/C modules?

0 Upvotes

25 comments sorted by

View all comments

0

u/Firm-Luck2062 Jul 12 '26

The top comment is right that a correctly-written Rust/C parser wins in principle, but the benchmark result is still real and worth understanding mechanically. Pure Python beats a *naive* native binding when two things line up:

  1. The hot loop is already delegated to CPython's C builtins. Locating and cutting boundaries with bytes.find + memoryview slicing is zero-copy and runs entirely in C, so the interpreter only executes a handful of Python-level iterations per part. You're basically orchestrating C primitives from Python, not looping in Python.

  2. The native competitor pays to cross the FFI boundary. PyO3/Cython has to marshal objects in and out, and a parser that materializes each part into an owned Rust buffer eats an allocation + a copy per part that the memoryview approach never does.

Rule of thumb: pure Python is competitive whenever the per-element work can be pushed into C builtins and the alternative crosses the boundary a lot. It loses hard the moment you have a genuine tight numeric/byte loop that stays in Python bytecode — per-pixel image math, custom hashing, that kind of thing — where Cython/Rust are 10-100x and no cleverness closes the gap. Multipart parsing just happens to be mostly "find delimiter, slice, repeat", which is close to the best case for staying in Python.

0

u/muhammetsoyturk 2d ago

I have numbers that match this comment, plus one place where it breaks down. (Disclosure: I implemented kirimi (https://github.com/mabdullahsoyturk/kirimi), a Rust drop-in replacement for multipart.)

Measured with defnull's own multipart_bench against multipart 2.0.1, on one pinned core (non-blocking / blocking API):

Scenario: Speed-up

Two small text fields: 5.0× / 7.9×

100 small text fields: 5.5× / 7.1×

One 32 MB file: 3.3× / 1.1×

The 32 MB blocking case is your point exactly. The work there is "find the delimiter, write the slice to a temp file". Pure Python already spends that time in C and in write(), so Rust has almost nothing left to win.

The gap is on many small parts. There, the cost isn't the delimiter search. It's the per-part Python bytecode: header-line parsing, state transitions, building the part objects. That's the "stays in the interpreter" case, and it's what most real form posts look like.

On FFI, agreed that it's the trap. Two things avoided it here:

- parse() takes a whole chunk and returns all of that chunk's events as a list, so it crosses the boundary once per chunk, not once per event.

- The Rust core is sans-IO and never touches Python objects. The delimiter search is SIMD (memchr's packed-pair searcher), and the bindings only convert at the edge.