Experimental drop-in replacement for CPython's float formatting and parsing, backed by Żmij and ffc.h (the C99 port of fast_float) — pure C, no C++ — in the exact configuration proposed for CPython itself.
After pip install floatium, every subsequent Python process uses
Żmij for repr(float) / str(float) / float.__format__ and ffc.h
for float("..."). Existing code,
existing tests, existing output — just faster. Works with an unmodified
stock CPython; no interpreter rebuild required.
pip install floatiumSource builds require a C++17 compiler (GCC 9+, Clang 12+, or MSVC 2019+) and CMake ≥ 3.20:
git clone https://github.com/eendebakpt/floatium
cd floatium
pip install -e '.[test]'No system libraries are required; {fmt}, fast_float, Ryu, Żmij,
ffc.h, and Wuffs are all vendored. See INTERNALS.md for the build
options and backend matrix.
CPython's float formatting has gone through
Python/dtoa.c
— David Gay's 1991 reference implementation — for three decades. The
code is ~2,800 lines of hand-tuned C and slower than modern alternatives.
Its parsing counterpart (_Py_dg_strtod in the same file) has similar
constraints.
Floatium demonstrates what replacing both sides looks like, as a pip package against stock CPython. Output is bit-identical to stock on every input we've tested (see PARITY.md — currently zero divergences against CPython 3.15's stdlib test suite).
Numbers below are from a release CPython 3.14.3 build
(python -m bench.bench_ns_per_op, median of fastest third of samples,
lower is better) using floatium's default zmij format backend +
ffc parse backend:
| Corpus | Operation | Stock (ns) | floatium (ns) | Speedup |
|---|---|---|---|---|
| random_uniform | repr(x) |
284 | 105 | 2.71× |
| random_uniform | f"{x:.4f}" |
120 | 111 | 1.07× |
| random_uniform | float(s) |
121 | 45 | 2.69× |
| random_bits | repr(x) |
817 | 151 | 5.40× |
| random_bits | f"{x:.4f}" |
1,933 | 453 | 4.27× |
| random_bits | float(s) |
277 | 68 | 4.07× |
| financial | repr(x) |
168 | 89 | 1.89× |
| financial | f"{x:.4f}" |
144 | 114 | 1.26× |
| financial | float(s) |
37 | 38 | 0.97× |
| scientific | repr(x) |
639 | 147 | 4.34× |
| scientific | f"{x:.4f}" |
1,080 | 281 | 3.84× |
| scientific | float(s) |
213 | 64 | 3.33× |
| integer_valued | repr(x) |
143 | 92 | 1.54× |
| integer_valued | f"{x:.4f}" |
168 | 110 | 1.53× |
| integer_valued | float(s) |
43 | 45 | 0.97× |
Wins are largest on hard inputs. random_bits and scientific
corpora — values whose decimal expansion stresses dtoa's big-integer
path — see 4–5.4× on repr / f"{x:.4f}" and 3.3–4.1× on float(s). The
fmt_opt backend is faster still on f"{x:.4f}" for huge magnitudes
(10× via Ryu d2fixed, see BENCHMARKS.md); the default
is zmij because one unmodified C library is what CPython can take.
On financial and integer_valued, parse stays near 1× because the
inputs are short, integer-valued strings where dtoa's fast path and
ffc.h's Eisel–Lemire path both finish in tens of nanoseconds —
there's not enough work to amortize either parser's setup cost.
The table above is the default configuration. For the full
per-backend matrix — fmt_opt / fmt / ryu_opt / zmij formatting
and fast_float / wuffs / ffc / stock parsing — see
BENCHMARKS.md. The zmij + ffc pair is the pure-C
configuration floatium proposes for CPython itself; see
INTERNALS.md.
Run locally with:
python -m bench.bench_ns_per_op --markdown # quick ns/op table
bench/run_all.sh # full pyperf sweepOnce floatium is installed, every Python process automatically uses
the replacement at startup — no env var, no import needed in your code:
python -c "import json; print(json.dumps([0.1, 0.2, 1e100]))"
# [0.1, 0.2, 1e+100]The mechanism is a .pth file placed in site-packages/ at install
time. site.py executes it during interpreter startup, before user
code, and the hook calls install() unless you've opted out.
Disable autopatch in this environment with the CLI:
python -m floatium disable # writes a marker file in site-packages
python -m floatium enable # remove the marker (the default state)
python -m floatium status # show whether autopatch is onFor temporary or per-process opt-out, set FLOATIUM_AUTOPATCH=0 in the
environment. It wins over the marker file and is the right knob for
CI, tooling, and ad-hoc subprocesses.
import floatium
floatium.install() # patch PyFloat_Type slots
assert repr(0.1) == "0.1"
floatium.uninstall() # restoreimport floatium
with floatium.enabled(): # ensure patched in the block
do_something_float_heavy()
with floatium.enabled(False): # ensure UN-patched in the block
stock_value = float(s) # measured against stock CPythonenabled(True) (the default) installs floatium for the block and
restores the entry state on exit. enabled(False) is the inverse: it
unpatches for the block, useful when autopatch is on globally but you
want to hand a single hot section to stock CPython.
| Surface | How |
|---|---|
repr(x), str(x), f"{x}" |
PyFloat_Type.tp_repr pointer swap |
f"{x:.2f}", format(x, spec) |
__format__ entry in PyFloat_Type.tp_dict |
"{}".format(x) |
via __format__ |
float("1.5") |
PyFloat_Type.tp_new + tp_vectorcall swap |
json.dumps([x]) |
via __repr__ |
"%g" % x is not patched — see PARITY.md.
For the full backend matrix (which format backend / parse backend combinations are available and how to A/B them), see INTERNALS.md.
tools/run_stdlib_tests.py runs a curated set of CPython's own stdlib
regression tests with floatium autopatched. Zero divergences on the
default set as of the last update; see PARITY.md for the
current status.
python tools/run_stdlib_tests.py
# ==> running: python -m test test_float test_strtod test_fstring ...
# == Tests result: SUCCESS ==
# All 12 tests OK.See PARITY.md for the full list. Summary:
"%g" % xis not patched. The%operator callsPyOS_double_to_stringdirectly fromlibpython; no pip package can intercept that withoutLD_PRELOAD.- Patching is process-global. One
install()per process; affects all threads, all modules. Py_TPFLAGS_IMMUTABLETYPEbypass. We write directly toPyFloat_Typeslots, bypassing the public type-attribute API that honors the flag. This is a known-stable internal technique (CPython itself does it during bootstrap) but explicitly not part of the stable ABI.- Free-threaded builds. Supported; patching must happen at import
time (the only safe window). Method-cache invalidation uses
PyType_Modified()except on 3.15 debug builds (where that call asserts on static types), in which case the wrapper clearstp_version_tagdirectly. Either way the type is immediately given a fresh tag viaPyUnstable_Type_AssignVersionTag(), because 3.15+ requires static builtin types to carry a non-zero tag whenever a subinterpreter starts. float("1_000.5")andfloat("inf")take the fallback path. Both parse correctly (output is bit-identical to stock) but don't benefit from the parser's speed.
- floatium wrapper code: MIT.
- Żmij (vendored in
third_party/zmij/): MIT OR BSL-1.0. - ffc.h (vendored in
third_party/ffc/): Apache-2.0 OR MIT OR BSL-1.0. {fmt}(vendored inthird_party/fmt/): MIT.fast_float(vendored inthird_party/fast_float/): Apache-2.0 OR MIT OR Boost-1.0.- Ryu (vendored in
third_party/ryu/): Apache-2.0 OR Boost-1.0. - Wuffs (vendored in
third_party/wuffs/): Apache-2.0 OR MIT. src/format_short.ccis a port of code from CPythonPython/pystrtod.c, which is under the PSF License. The port preserves the original attribution and is compatible with downstream MIT redistribution under the PSF license's permissive terms.- Name and vectorcall patching inspired by copium (MIT).
See LICENSE and the per-directory LICENSE* files.