pyroid¶
pyroid reimplements popular Python libraries in Rust, with the same API and the same output. Most of its modules are many times faster than the library they replace. It runs on CPython 3.11 to 3.15, including free-threaded 3.14t and 3.15t.
$ pip install pyroid
Change the import. The output is the same:
import pyroid.ftfy as ftfy # was: import ftfy
import pyroid.json_repair as json_repair # was: import json_repair
ftfy.fix_text("naïve résumé") # 'naïve résumé'
json_repair.repair_json("{'a': 1, 'b': [2, 3") # '{"a": 1, "b": [2, 3]}'
For code you don't own, such as a pipeline library, patch its imports instead:
import pyroid
pyroid.patch("ftfy", "fasttext") # from here on, `import ftfy` gets pyroid.ftfy
import ftfy
ftfy.fix_text("naïve résumé") # 'naïve résumé', computed by pyroid
See pyroid.patch() for the libraries it covers.
What's in it¶
| Area | Module | Replaces | Identical output? |
|---|---|---|---|
| Text and encoding | pyroid.ftfy |
ftfy 6.3.1 | byte-identical (parity) |
pyroid.translit |
anyascii 0.3.3; a permissive alternative to GPL Unidecode |
identical to anyascii; not Unidecode's output |
|
| Language ID and NLP | pyroid.fasttext |
fastText inference (fasttext, fasttext-predict, ...) |
bit-identical labels and probabilities on Linux (glibc) and Windows x64; on macOS the probabilities can differ in the last bits (parity) |
pyroid.nltk |
NLTK 3.10.4's sent_tokenize (Punkt), word_tokenize, Treebank and regexp tokenizers |
same sentences, tokens and offsets (parity) | |
pyroid.jieba |
jieba 0.42.1 | same words, offsets and frequencies (parity) | |
| Web and archives | pyroid.trafilatura |
Trafilatura's text extraction, 2.3.1 or 1.11.0 (the release datatrove 0.10.1 installs), with lxml 6.1.3's HTML parser and jusText 3.0.2 | the same text, byte for byte (parity) |
pyroid.warc |
warcio 1.8.2's WARC/ARC reader, and datatrove 0.10.1's WARC reading | same records, documents, exceptions and messages (parity) | |
| Dedup and similarity | pyroid.datasketch |
datasketch 2.0.0's MinHash, LeanMinHash, MinHashLSH, LSH Forest and LSH Ensemble | same hash values, bytes and query results (parity) |
pyroid.dedup |
datatrove 0.10.1's MinHash and sentence dedup | same signatures, removals and files (parity) | |
| Quality filters | pyroid.filters |
datatrove 0.10.1's Gopher, C4 and FineWeb filters, PII formatter, C4 bad words, symbol lines and URL filter | same decisions, reasons and text (parity) |
pyroid.nemo |
NeMo Curator 1.4.0's heuristic document filters and string modifiers | same scores, exceptions and modified text (parity) | |
| Evaluation | pyroid.metrics |
rouge-score 0.1.2 and sacrebleu 2.6.0 (BLEU, chrF, chrF++) | identical scores and signatures, no tolerance (parity) |
| JSON | pyroid.json_repair |
json_repair 0.64.5 | same output, linear on 13 input families where json_repair is quadratic (parity) |
Example: cleaning LLM training data¶
Building a pretraining or fine-tuning corpus means running every document through the same few steps: fix broken Unicode, identify the language, drop low-quality pages, remove near-duplicates. The usual tools for them are pure Python (ftfy, datatrove's filters and MinHash) or C++ extensions that lag behind new Pythons (fastText).
On one core, datatrove's FineWeb chain spends about 79% of its CPU in the four quality filters and about 18% in MinHash. ftfy is 2.3% and language ID 1% (the first baseline run, in a Docker VM: method and caveats). The filters and MinHash both tokenize every document with spaCy and then do their own work in Python. pyroid moves both to Rust.
From raw WARC files, text extraction with Trafilatura comes first, and it costs more than all the
other steps together. On 8 Common Crawl WARC files, stock datatrove spent 5,543 of its 6,759 CPU
seconds in it (AWS EC2 c7i.4xlarge, 8 workers). pyroid.trafilatura
ports that step too.
On 100,000 FineWeb documents, pyroid's chain is 27× faster than datatrove on 8 processes, with
the same output. From WARC files, pyroid clean runs the whole pipeline 10.7× faster (AWS EC2
c7i.4xlarge; see Speed).
The command line takes Parquet or JSONL and gives the same output as datatrove's FineWeb pipeline:
$ pip install "pyroid[cli]"
$ pyroid clean in.parquet out.parquet --recipe fineweb
See the command line. In Python, the same steps are a few calls:
import pyroid.ftfy as ftfy # was: import ftfy
import pyroid.fasttext as fasttext # was: import fasttext
import pyroid.filters as filters # datatrove's Gopher / C4 / FineWeb filters
docs = ["naïve résumé", "Some other web page ..."]
# ["naïve résumé", ...], like [ftfy.fix_text(d) for d in docs] but on all cores
docs = ftfy.fix_text_batch(docs)
# fastText's own .bin / .ftz files; a batch runs without the GIL
model = fasttext.load_model("lid.176.bin")
labels, probs = model.predict(docs, k=1) # [["__label__fr"], ["__label__en"]]
# datatrove's keep/drop decision per document
keep = filters.gopher_quality(docs) # [False, False]: both are under 50 words
Batch functions take a str, a list or tuple of str, or an Arrow string column (pyarrow,
polars, pandas, Hugging Face datasets), and return text and masks in the same kind
(Inputs and outputs).
The filters and dedup tokenize 4,049 of datatrove's 4,343 language codes natively (Japanese with an installed SudachiDict dictionary, Thai with an installed PyThaiNLP) and send the rest through datatrove's own tokenizers (Languages).
Install¶
$ pip install pyroid # every module but fasttext and datasketch; no dependencies
$ pip install "pyroid[fasttext]" # adds numpy, which pyroid.fasttext returns arrays in
$ pip install "pyroid[cli]" # the pyroid command line (pyarrow)
[datatrove] and [hf] add the datatrove steps and the Hugging Face helpers. There are prebuilt
wheels for Linux (glibc and musl), macOS and Windows, on x86-64 and ARM64. See
Install.
Integrations¶
| Integration | What it does |
|---|---|
pyroid clean |
datatrove's FineWeb pipeline as one command over Parquet or JSONL, or from WARC files, with datatrove's output document for document |
| Large datasets | the same commands over thousands of files: worker processes, --shard I/N across machines, s3://, gs:// and hf:// inputs, resumable runs, and MinHash dedup across shards in datatrove's file format |
pyroid.patch() |
makes import ftfy, fasttext, anyascii, datasketch, trafilatura, nltk, warcio and nemo_curator give pyroid's functions, in code you don't own |
| datatrove steps | pyroid.integrations.datatrove: datatrove's pipeline steps with the same names and signatures, Trafilatura included, as a one-import change |
| NeMo Curator stages | pyroid.integrations.nemo_curator: NeMo Curator 1.4.0's CPU text stages, subclassing NeMo's classes with the same arguments |
Hugging Face datasets |
pyroid.integrations.hf: batched callables for Dataset.map and Dataset.filter, and the FineWeb recipe |
Speed¶
From Common Crawl WARC files to deduplicated documents, stock datatrove's FineWeb pipeline took
848.3 s with 8 workers. With pyroid's steps in datatrove's executor, WARC reader included, it took
89.4 s (9.5×), and pyroid clean --recipe fineweb-warc took 79.6 s (10.7×). The input was 8 WARC
files of CC-MAIN-2026-39 (171,595 HTML records), on an AWS EC2 c7i.4xlarge (Intel Xeon Platinum
8488C) pinned to 8 CPUs. The kept documents are the same once the pages stock datatrove drops at
its 1 s extraction timeout are left out of pyroid's run too
(From WARC files).
On 100,000 FineWeb documents, with the text already extracted, pyroid's chain took 10.2 s on 8 threads against 272.1 s for datatrove on 8 processes (about 27×), and 43.4 s against 2,444.4 s on one (about 56×), with identical output, on the same instance type. On the 10,000-document dev slice the ratios are 18× and 38×. On a c8g.4xlarge (AWS Graviton4) the 100k run gives 29× and 50×.
Per stage, on the 10k dev slice and the c7i, pyroid on one thread against the incumbent in one process:
| Module | Incumbent | Speedup |
|---|---|---|
pyroid.ftfy |
ftfy 6.3.1 | 108× |
pyroid.fasttext |
fasttext-predict 0.9.2.4 | 1.6× (.bin), 3.6× (.ftz) |
pyroid.filters |
datatrove 0.10.1's Gopher, C4 and FineWeb filters | 35–69× |
pyroid.dedup |
datatrove 0.10.1's MinHash signatures | 34× |
pyroid.translit |
anyascii 0.3.3 | 53× |
The other modules have only been measured in a Docker/OrbStack VM on Apple Silicon, so treat their numbers as indicative (one thread unless stated):
| Module | Incumbent | Speedup |
|---|---|---|
pyroid.trafilatura.extract, default options (compat="2.3.1" / "1.11.0") |
Trafilatura 2.3.1 / 1.11.0 | 7.2× / 8.6× |
pyroid.metrics, ROUGE |
rouge-score 0.1.2 | 59–65× |
pyroid.metrics, corpus BLEU / chrF |
sacrebleu 2.6.0 | 11× / 19× |
pyroid.json_repair.repair_json, broken JSON |
json_repair 0.64.5 | about 30× |
pyroid.datasketch, MinHash signatures, 1 / 8 threads |
datasketch 2.0.0 | 8× / 21× |
pyroid.jieba.lcut, a batch, 1 / 8 threads |
jieba 0.42.1 | 24× / 58× |
pyroid.nltk.sent_tokenize / word_tokenize |
NLTK 3.10.4 | 60× / 31× |
pyroid.filters, FineWeb-2 settings, Chinese / Japanese |
datatrove 0.10.1 | 44× / 7.8× |
The Benchmarks page has every number, how it was measured and what it doesn't show.
Correctness¶
Each module is tested differentially against the library it replaces. The inputs are pinned FineWeb and FineWeb-2 documents, pinned Common Crawl pages, pinned evaluation data, the incumbent's own test corpus, and Hypothesis fuzzing that includes lone surrogates, mojibake and every option. The counts are on the Compatibility page, and every known difference is listed in each guide.
All suites ran on Linux aarch64 in a Docker VM, and CI runs the parity/ suites on Linux x86_64
and aarch64. The ftfy, fastText, filters, dedup, metrics and json_repair suites also ran natively on
x86_64 and aarch64 on AWS EC2, and the Trafilatura pages on x86_64, with 0 differences
(Platforms). On macOS, Windows and musl Linux, every release's
wheels pass pyroid's test suite before they are published.
Where a module meets an option or input it does not port, it hands the call to the real library
with a FallbackWarning, or raises an error.
Results don't depend on the number of threads or workers, or on the machine's load. Stock datatrove's Trafilatura step does: it drops whichever pages take more than 1 s.
Licence¶
pyroid's own code is MIT. The ported parts keep their licences, so the package's licence
expression is MIT AND Apache-2.0 AND BSD-2-Clause AND BSD-3-Clause AND ISC AND
PSF-2.0 AND Unicode-3.0 AND Zlib:
- Apache-2.0 for the ftfy, datatrove, rouge-score, NLTK (Porter stemmer and tokenizers), sacrebleu, Trafilatura, SudachiPy and warcio ports;
- MIT for the fastText, spaCy, indic-nlp-library, urduhack, json_repair, datasketch, jieba (and its dictionary) and libxml2 (HTML parser) ports;
- BSD-2-Clause for the jusText port and its stoplists;
- BSD-3-Clause for numpy's random generator, scipy's QUADPACK integration, tldextract and lxml;
- PSF-2.0 for CPython's
ipaddress; - ISC for anyascii's data;
- Unicode-3.0 for Unicode Character Database data;
- Zlib for zlib-rs, the Rust crate behind
pyroid.warc's gzip decoding.
The NOTICE file lists the ported files, and the
licence texts are in LICENSES/, with
those of the Rust crates the extension links in THIRD-PARTY-RUST.txt there.