Port Mortem 2026 · Track H · TypeScript → Python

PyFuse

$ pip install pyfusejs

fuse.js, rebuilt in Python. Same results — proved, not claimed.

Ayan Bag · github.com/ayanbag/pyfuse

the problem

Python has no fuzzy search engine

rapidfuzz

Compares two strings.
That's the whole job.

?

Search a collection. Weighted fields, nested paths, ranked results.

Elasticsearch

A server to deploy, tune and pay for.

fuse.js is exactly that missing middle — and it only runs in JavaScript.

The capability isn't missing from computing. It's trapped in the wrong ecosystem.

PyFuse · fuse.js → Python2
what was ported

Not a string matcher. A search engine.

Bitap corebit-parallel approximate matching, 32-char word
Weighted keys + nested pathsauthor.lastName, tags[], normalised weights
Extended query operators=exact 'include ^prefix suffix$ !inverse
Logical composition$and / $or query trees
Token searchinverted index with IDF / BM25-style ranking
Scoringbitap × key weight × field-length norm

~3,283 lines · zero runtime dependencies · workers/* excluded (DECISIONS §17)

PyFuse · fuse.js → Python3
the standard

A port is only worth as much as its proof

Anyone can write a fuzzy search library.
The hard part is proving it behaves like the original.

So the original became an oracle:

PyFuse · fuse.js → Python4
proof 1 of 2

The original test suite, unmodified

285/297of fuse.js's own vitest tests pass
0test files modified
95.96%byte-for-byte identical suite
test/*.test.js  (UNMODIFIED)
      ↓   import Fuse from '../dist/fuse.mjs'   ← vitest alias
  fuse_shim.mjs → py_bridge.mjs (Atomics.wait) → server.py → pyfuse

All 13 spec files import the engine from one specifier. Redirect that, and the tests run against Python.

PyFuse · fuse.js → Python5
the honest part

12 fail. None is a bug in the port.

10
JavaScript callables

sortFn, getFn, function tokenize, Fuse.use — a closure over a live JS heap cannot be serialised

1
Document-list copy

fuse.js mutates the caller's array; this port copies it — deliberate, DECISIONS §13

1
Corrected error message

"Use Fuse(...)" not "Use new Fuse(...)"new is JavaScript syntax, DECISIONS §19

Every failure is classified in compat/README.md. Nothing is suppressed to force a pass.

PyFuse · fuse.js → Python6
proof 2 of 2

Differential fuzzing against the live oracle

51,569random cases in 60 seconds
0structural divergences
859/sthroughput, long-lived Node oracle

Identical result sets · identical ordering · identical match indices · identical refIndex

Random datasets, queries and options generated with hypothesis, fed to both engines, results compared field by field. Not example tests — property tests.

PyFuse · fuse.js → Python7
the one limit i could not close

Scores agree to 1e-13, not to the bit

pow(0.1, 0.3846666666666666)
   CPython : 0.4124139370464501      ← correctly rounded
   V8      : 0.41241393704645002     ← fdlibm, not correctly rounded

They disagree by exactly 1 ULP on 10.04% of calls. Scoring does one pow per matched key.

I transcribed V8's fdlibm pow — 200 lines, 95.6% bit-exact — then stopped.

The residual error was uniform across every branch, which means the algorithm was right and the target was wrong. Closing it meant reverse-engineering V8 with nothing to diff against.

Consequence, disclosed: 1 ULP can break a score tie → 8 order flips in 51,569 (0.016%).

PyFuse · fuse.js → Python8
why differential testing earns its keep

The fuzzer caught me being wrong

“The float divergence only affects the last digits.
It never changes the output.”

— what I wrote in my notes, early on

It was wrong. 1 ULP is enough to split an exact tie, and the tie-break then fires in one engine but not the other — so ordering does change, 8 times in 51,569.

It is now its own category in fuzz/log.txt, and the claim is corrected in README.md and DECISIONS §1 — rather than quietly dropped.

PyFuse · fuse.js → Python9
why i wanted it in python

The problems that made me port it

Ticket triage

10K+ free-text tickets a month. Misspelled services, pasted stack traces.

Fuzzy search is the cheap pre-filter before the expensive LLM path.

→ 5 auto-routed · 1 suggested · 2 escalated

Reconciliation

2M+ rows, Oracle → SQL Server. The join key does not survive the migration.

ACME CORPORATION LTD
  vs  Acme Corp. Limited

→ matched · needs-review · missing, both ways

Both ship in examples/ and run in one command. Everything I work on is Python. The tool was not.

PyFuse · fuse.js → Python10
code quality

Idiomatic Python, not transliterated TypeScript

0runtime dependencies
0casts / type: ignore
417native tests
20logged decisions
PyFuse · fuse.js → Python11
the number i am not hiding

fuse.js is ~13x faster. That's fine.

pyfusefuse.js
throughput7.0 /s92.8 /s
latency p99637.0 ms49.8 ms
peak RSS31.3 MiB53.7 MiB

V8 JITs the Bitap inner loop; CPython interprets it. The value here is reach and parity, not speed — and an honest regression is worth more than a cherry-picked win.

just test · just compat · just fuzz · just demo

Every claim in this deck is one command away.

PyFuse · fuse.js → Python12