Skip to content

pyroid.integrations

Plug-ins for the tools you already run. import pyroid imports none of them, and each needs an extra: pyroid[datatrove], pyroid[nemo], pyroid[hf] or pyroid[cli] (for recipes). Guides: datatrove steps, NeMo Curator stages, Hugging Face datasets, the command line, pyroid.patch().

These modules have no stubs: this page is generated from their source.

Patching: pyroid.integrations

pyroid.patch and pyroid.unpatch are the same functions as pyroid.integrations.patch and pyroid.integrations.unpatch.

pyroid for datatrove and NeMo Curator pipelines, Hugging Face datasets and existing imports.

import pyroid imports none of this, and nothing changes behaviour until you call it.

  • pyroid.integrations.datatrove: 15 drop-in datatrove pipeline steps (FTFYFormatter, LanguageFilter, the Gopher/C4/FineWeb quality filters, C4BadWordsFilter, URLFilter, PIIFormatter, SymbolLinesFormatter, MinhashDedupSignature and the four sentence-dedup steps) that subclass datatrove's own classes and give the same output. The steps that tokenize run in Rust for every language pyroid tokenizes natively.
  • pyroid.integrations.nemo_curator: NeMo Curator 1.4.0's ScoreFilter, Score, Filter and Modify stages, its 23 heuristic filters, two fastText filters, UnicodeReformatter and string modifiers, subclassing NeMo's classes, with the same output, one pyroid call per batch.
  • pyroid.integrations.recipes: the FineWeb cleaning chain over Arrow batches (what pyroid clean --recipe fineweb runs).
  • pyroid.integrations.hf: helpers for Hugging Face datasets (Dataset.map(batched=True) / .filter on Arrow batches).
  • patch / unpatch / patching: explicit, reversible sys.modules shims so that existing import ftfy / import fasttext code gets pyroid's drop-ins without edits (also pyroid run -p ftfy,fasttext script.py).

PatchWarning

Bases: UserWarning

patch replaced a module with pyroid's drop-in.

Says what was patched, which version pyroid reproduces and how to undo it; shown once per process for each name, plus once more if the installed package is another version. Silence it with warnings.simplefilter("ignore", pyroid.integrations.PatchWarning).

patch

patch(*names: str) -> None

Make import <name> give pyroid's drop-in, for each of names.

patch("ftfy", "fasttext") puts shims in sys.modules, so a later import ftfy, from ftfy import fix_text, import fasttext.FastText... gets pyroid's functions, with everything pyroid does not provide forwarded to the real package (imported now, if it is installed). Modules that imported the real package earlier keep it. Patching a patched name does nothing; unpatch undoes it.

Parameters:

  • *names (str, default: () ) –

    "ftfy", "fasttext", "anyascii", "datasketch", "trafilatura", "nltk", "warcio" and/or "nemo_curator".

Raises:

  • TypeError –

    No names were given.

  • ValueError –

    A name is not supported (nothing is patched then).

  • ImportError –

    The pyroid module behind a name cannot be imported (pyroid.fasttext and pyroid.datasketch need numpy).

Warns:

  • PatchWarning –

    Once per process for each name: what was patched and how to undo it; and if the installed package is not the version pyroid reproduces.

unpatch

unpatch(*names: str) -> None

Undo patch for names (all patched names if none are given).

sys.modules gets back exactly the entries it had for each name and its submodules before patch (the real module if it was imported, else no entry; submodules imported meanwhile are dropped, so the next import is fresh), and pyroid's fallback to the real package is restored. Modules that imported a shim keep it, and it keeps working. Names that are not patched are ignored.

Raises:

patched

patched() -> tuple[str, ...]

The names currently patched, in SUPPORTED order.

patching

patching(*names: str) -> AbstractContextManager[None]

patch names for the duration of a with block.

On exit, only the names this block patched are unpatched (one patched before stays).

Raises:

run

run(argv: Sequence[str]) -> int

pyroid run [-p NAMES] (-m MODULE | SCRIPT) [ARGS...]: run a target with patches.

Patches NAMES (comma-separated; default $PYROID_PATCH, else ftfy,fasttext, skipping fasttext with a PatchWarning if numpy is missing), exports PYROID_PATCH and puts HOOK_DIR first on PYTHONPATH (so child Python processes are patched too), then runs the target in this process with runpy, as python -m MODULE ARGS / python SCRIPT ARGS would: sys.argv, sys.path[0], __name__ == "__main__". It is meant to be the last thing a process does: the patches, the environment and sys.argv stay as they are afterwards.

Parameters:

  • argv (Sequence[str]) –

    The arguments after pyroid run.

Returns:

  • int –

    0 when the target finishes; 2 for a usage error (message on stderr) or a missing script; 1 when the module cannot be found (as python).

Raises:

  • SystemExit –

    The target exited (sys.exit); its code is the exit code.

  • BaseException –

    Whatever else the target raises propagates, traceback and all.

Constants

The names patch() accepts, pyroid run's defaults, and the start-up hook's directory (put it on PYTHONPATH, with the names in PYROID_PATCH, to patch Python processes started without pyroid run):

SUPPORTED module-attribute

SUPPORTED: tuple[str, ...] = (
    "ftfy",
    "fasttext",
    "anyascii",
    "datasketch",
    "trafilatura",
    "nltk",
    "warcio",
    "nemo_curator",
)

DEFAULT_RUN_NAMES module-attribute

DEFAULT_RUN_NAMES: tuple[str, ...] = ('ftfy', 'fasttext')

ENV_VAR module-attribute

ENV_VAR = 'PYROID_PATCH'

HOOK_DIR module-attribute

HOOK_DIR: str = os.path.join(
    os.path.dirname(os.path.abspath(__file__)),
    "_autopatch",
)

pyroid.integrations.datatrove

Each step subclasses datatrove's step of the same name; its constructor takes datatrove's arguments plus the keyword-only batch_size and threads (and model_path for LanguageFilter, compat for Trafilatura).

Drop-in datatrove pipeline steps backed by pyroid.

Change the import and leave the pipeline as it is:

# from datatrove.pipeline.filters import GopherQualityFilter, LanguageFilter
from pyroid.integrations.datatrove import GopherQualityFilter, LanguageFilter

Each class here subclasses datatrove's own step of the same name and takes the same constructor arguments, plus two keyword-only ones:

  • batch_size: documents per pyroid call (default 1000).
  • threads: pyroid's threads= (default 1, since datatrove already runs one task per process). Pass None for pyroid's whole thread pool when you run few tasks.

The documents that come out are the same as with datatrove's step: same text, same metadata, same keep/drop decisions and drop reasons, so exclusion writers and later steps see the same data. The work happens in Rust, in batches, without the GIL:

  • FTFYFormatter: pyroid.ftfy (byte-identical to ftfy 6.3.1; ftfy itself is not needed).
  • LanguageFilter: datatrove's own filter() and FastTextLID.predict code, with the fastText call replaced by one batched pyroid.fasttext prediction (no fasttext-numpy2-wheel or fasteners needed when model_path is given; without it the model is downloaded to datatrove's own cache, as datatrove does).
  • GopherRepetitionFilter, GopherQualityFilter, C4QualityFilter, FineWebQualityFilter: pyroid.filters (spaCy is not needed).
  • MinhashDedupSignature: pyroid.dedup signatures, written to the same files, byte for byte, so datatrove's MinhashDedupBuckets -> MinhashDedupCluster -> MinhashDedupFilter run on them unchanged.
  • PIIFormatter, SymbolLinesFormatter: pyroid.filters (same text, same replacement rotation).
  • C4BadWordsFilter, URLFilter: pyroid.filters (same decisions and reasons; datatrove's own random draws, word lists and blocklists).
  • SentenceDedupSignature, SentenceFindDedups, SentenceDedupFilter, SentenceDedupBuildIndex: pyroid.dedup; the same signature, duplicate and index files, byte for byte, and the same documents out.
  • Trafilatura: pyroid.trafilatura (byte-identical to the installed trafilatura, 1.11.0 as datatrove installs it, or 2.3.1; in this process, with no timeout).
  • WarcReader: pyroid.warc.iter_documents (warcio 1.8.2's parsing, the gzip layer and datatrove's process_record in Rust; the same documents and errors; warcio is not needed). It takes datatrove's arguments only.

Languages: the filter and MinHash steps take datatrove's language (a datatrove language code such as "fra_Latn"). Every code pyroid tokenizes natively, which is 4,049 of datatrove's 4,343 with a SudachiDict dictionary and PyThaiNLP installed and 4,025 with neither (pyroid.filters.languages(), Language.native), runs in Rust with that language's datatrove tokenizer reproduced exactly. Any other code (the Stanza languages, Korean, Japanese without a dictionary, Thai without PyThaiNLP, ...), a code datatrove has no tokenizer for, or a WordTokenizer object runs datatrove's own code for that step, with a one-time pyroid.FallbackWarning.

What differs: per-line statistics of C4QualityFilter (line-total, line-filter-*) are not counted, and batched steps also count batches.

Needs datatrove (and numpy for LanguageFilter/MinhashDedupSignature): pip install 'pyroid[datatrove]'.

FTFYFormatter

FTFYFormatter(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's FTFYFormatter (same arguments and defaults) on pyroid.ftfy.

doc.text becomes ftfy.fix_text(doc.text, config=self.config), byte for byte, computed batch_size documents at a time.

format

format(text: str) -> str

One document (datatrove's API); run batches instead.

run

run(
    data: Iterable[Any], rank: int = 0, world_size: int = 1
) -> Any

Fix doc.text of every document, batch_size at a time.

LanguageFilter

LanguageFilter(
    *args: Any,
    model_path: Any = None,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's LanguageFilter (same arguments) on pyroid.fasttext.

Extra keyword-only arguments: model_path (a local lid.176.bin or GlotLID .bin; default: download to datatrove's cache, as datatrove does), batch_size and threads.

The decision and the language / language_score (and language_script, top_language_*_score) metadata come from datatrove's own filter() code. When only the top label can matter (no keep_top_pairs_threshold, and no languages, label_only, or a threshold above 0.501, since fastText's probabilities sum to at most 1 + 176e-5), pyroid predicts the top label only instead of all of them.

filter

filter(doc: Any) -> Any

One document (datatrove's API); filter_batch batches instead.

filter_batch

filter_batch(batch: list[Any]) -> list[Any]

Predict the batch in one pyroid call, then run datatrove's filter() per doc.

GopherRepetitionFilter

GopherRepetitionFilter(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's GopherRepetitionFilter (same arguments) on pyroid.filters.

GopherQualityFilter

GopherQualityFilter(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's GopherQualityFilter (same arguments) on pyroid.filters.

C4QualityFilter

C4QualityFilter(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's C4QualityFilter (same arguments) on pyroid.filters.

Kept documents get the same line removal; per-line statistics are not counted.

FineWebQualityFilter

FineWebQualityFilter(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's FineWebQualityFilter (same arguments) on pyroid.filters.

MinhashDedupSignature

MinhashDedupSignature(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's MinhashDedupSignature (stage 1) on pyroid.dedup.

Writes the same bucket_XXX/RANK.minhash.sig files, byte for byte (the sort pass is datatrove's own), so datatrove's later MinHash stages run on them unchanged. spaCy is not needed. language is datatrove's: shingles come from that language's word tokenizer, in Rust for every code pyroid tokenizes natively; another one runs datatrove's own step (pyroid.FallbackWarning). Note that datatrove's sort pass logs "Found existing sig files ... Skipping sig writing step": pyroid has just written them.

run

run(
    data: Iterable[Any], rank: int = 0, world_size: int = 1
) -> None

Write this rank's signatures, then datatrove's own sort-and-verify pass.

PIIFormatter

PIIFormatter(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's PIIFormatter (same arguments and defaults) on pyroid.filters.

doc.text gets the same email and IP replacements, byte for byte, in the same rotation: the counters are datatrove's own (emails_replacer._replace_i and ip_replacer._replace_i), and whether an address is replaced is asked of datatrove's own validator, once per distinct address and batch. The regexes run in Rust, batch_size documents at a time.

format

format(text: str) -> str

One document (datatrove's API); run batches instead.

run

run(
    data: Iterable[Any], rank: int = 0, world_size: int = 1
) -> Any

Rewrite doc.text of every document, batch_size at a time.

C4BadWordsFilter

C4BadWordsFilter(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's C4BadWordsFilter (same arguments) on pyroid.filters.

The word search runs in Rust, batch_size documents at a time; the lists are loaded exactly as datatrove loads them (its download cache and pinned URLs). Everything else is datatrove's own: self.uniform (its default_rng(seed)) is drawn once per flagged document in order, and the same statistics are counted. A batch that contains a document whose language has no list raises before any of the batch is yielded (datatrove yields the documents before it first).

filter

filter(doc: Any) -> Any

One document (datatrove's API); filter_batch batches instead.

filter_batch

filter_batch(batch: list[Any]) -> list[Any]

One pyroid search for the batch, then datatrove's decision per document.

URLFilter

URLFilter(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's URLFilter (same arguments) on pyroid.filters.URLFilter.

The same keep/drop decisions and filter_reason strings (domain, subdomain, url, hard_blacklisted, soft_blacklisted, blacklisted_subword), from doc.metadata["url"], batch_size URLs per pyroid call. The suffix lists are this step's own tldextractor's (its cached Public Suffix List or tldextract's snapshot), and with use_integrated_lists the block lists are datatrove's, extracted into datatrove's cache as datatrove does; both are loaded at the first batch, in the worker. pyahocorasick is not needed.

What differs: a document without a url raises datatrove's AssertionError when its batch is filtered (before any document of that batch is passed on); datatrove's per-document DeprecationWarning from tldextract's registered_domain (hidden by Python's default filters) is not emitted; block_listed_domains and the other list attributes hold the constructor's entries only (the integrated lists live in Rust); banned_subwords_automaton is None.

download_data

download_data() -> None

Build the pyroid filter: the lists, and datatrove's if use_integrated_lists.

filter

filter(doc: Any) -> Any

One document (datatrove's API); filter_batch batches instead.

filter_batch

filter_batch(batch: list[Any]) -> list[Any]

One pyroid call for the batch's URLs.

SymbolLinesFormatter

SymbolLinesFormatter(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's SymbolLinesFormatter (same arguments) on pyroid.filters.

doc.text becomes the same text, byte for byte, computed batch_size documents at a time (FineWeb-2 runs it as SymbolLinesFormatter(symbols_to_remove=["|"], replace_char="\n")).

format

format(text: str) -> str

One document (datatrove's API); run batches instead.

run

run(
    data: Iterable[Any], rank: int = 0, world_size: int = 1
) -> Any

Rewrite doc.text of every document, batch_size at a time.

SentenceDedupSignature

SentenceDedupSignature(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's SentenceDedupSignature (stage 1) on pyroid.dedup.

Writes the same {bucket:04d}/{rank:05d}.c4_sig files, byte for byte (the same buckets, the same empty files), so datatrove's own later stages run on them unchanged. spaCy is not needed. language is datatrove's: every code pyroid tokenizes natively (pyroid.filters.languages()) runs in Rust; another one with split_sentences=True runs datatrove's own code (pyroid.FallbackWarning).

run

run(
    data: Iterable[Any], rank: int = 0, world_size: int = 1
) -> None

Signatures of this rank's documents, batch_size at a time; then the files.

SentenceFindDedups

SentenceFindDedups(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's SentenceFindDedups (stage 2) as a merge in Rust.

Reads the same signature (and index) files and writes the same {rank:04d}/{stem}.c4_dup files, byte for byte; local files are read by Rust directly, remote ones through fsspec. batch_size and threads are accepted for uniformity (the merge is one sequential pass).

run

run(
    data: Any = None, rank: int = 0, world_size: int = 1
) -> None

Find the duplicate windows of this finder task's hash bucket.

SentenceDedupFilter

SentenceDedupFilter(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's SentenceDedupFilter (stage 3) on pyroid.dedup.

Reads the same .c4_dup files and yields the same documents with the same texts; the exclusion writer gets the same documents (>>> / <<<-marked texts). Only documents with duplicates go to Rust, batch_size at a time. spaCy is not needed. language is datatrove's: a code pyroid does not tokenize natively (pyroid.filters.languages()) that needs the tokenizer runs datatrove's own code (pyroid.FallbackWarning).

run

run(
    data: Iterable[Any], rank: int = 0, world_size: int = 1
) -> Any

Remove this rank's duplicate windows, batch_size documents at a time.

SentenceDedupBuildIndex

SentenceDedupBuildIndex(
    *args: Any,
    batch_size: int = DEFAULT_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's SentenceDedupBuildIndex as a merge in Rust (the same index file).

Like datatrove's own step, it fails (StopIteration) on an empty signature file: that case runs datatrove's code. batch_size and threads are accepted for uniformity.

run

run(
    data: Any = None, rank: int = 0, world_size: int = 1
) -> None

Write the distinct hashes of every signature file.

Trafilatura

Trafilatura(
    favour_precision: bool = True,
    include_images: bool = False,
    timeout: float = 1,
    deduplicate: Any = True,
    *,
    compat: str | None = None,
    batch_size: int = TRAFILATURA_BATCH_SIZE,
    threads: Any = 1,
    **kwargs: Any,
)

datatrove's Trafilatura extractor (same arguments and defaults) on pyroid.trafilatura.

doc.text becomes trafilatura.extract(doc.text, favor_precision=favour_precision, include_comments=False, deduplicate=deduplicate, **kwargs), byte for byte, computed in this process batch_size documents at a time; the same documents are forwarded and dropped, with the same statistics (total, extracted, forwarded, dropped, doc_len). Errors count as datatrove counts them: a page whose extraction raises is broken_process (it kills datatrove's child process, and the next child starts with an empty dedup cache, so the rest of the task does here too), and arguments that make trafilatura.extract("") raise (datatrove's warm-up call) make every page timeout. Trafilatura itself is not needed.

Extra keyword-only arguments: compat, the trafilatura release to reproduce (None: the installed trafilatura's version when pyroid reproduces it, which includes the 1.11.0 that datatrove[processing] installs, or pyroid's default, 2.3.1, when trafilatura is not installed; for any other installed version the documents go to datatrove's own step, with a one-time pyroid.FallbackWarning), batch_size and threads.

What differs: there is no child process and no timeout (pages datatrove gives up on after timeout seconds, counting timeout and restarting its dedup cache, get their text here). With deduplicate=True the dedup cache is a fresh pyroid.trafilatura.LRUCache per run, i.e. per task, as datatrove's per-task child process has a fresh one. Time is tracked per batch.

extract

extract(text: str) -> str

One document (datatrove's API); run batches instead.

run

run(
    data: Iterable[Any], rank: int = 0, world_size: int = 1
) -> Any

Extract the text of every document, batch_size at a time.

WarcReader

datatrove's WarcReader (same arguments and defaults) on pyroid.warc.iter_documents.

Each file is read as datatrove's read_file reads it, with warcio's parsing, the decompression and process_record in Rust: the same documents (text, id, metadata: url, date, file_path and default_metadata), the same records skipped, and the same exceptions, warnings and stderr messages at the same document, for gzip and plain files, on the local filesystem and on any other fsspec filesystem (ArchiveLoadFailed is warcio's class, as datatrove raises it, when warcio is installed). limit, skip, sharding and the statistics are datatrove's own code. The page text is decoded as datatrove decodes it: UTF-8, else cchardet's guess; python-magic is called only for records without WARC-Identified-Payload-Type. warcio itself is not needed.

How a file is read: a local .gz (compression="infer" or "gzip") or plain file is opened and read in Rust without the GIL, with the gzip layer reproducing CPython's gzip.GzipFile (datatrove reads through it); on another filesystem the file is opened with data_folder.open(path, "rb", compression=None) and that same emulation runs over it. Any other compression ("zstd", an inferred .bz2, ...), and .gz when fsspec's gzip opener is not CPython's (python-isal installed), use datatrove's own data_folder.open(..., compression=...), read as is.

What differs: time is tracked per document, including the reading of the records before it, plus once per file for the records after the last document (datatrove times each record's process_record).

read_file

read_file(filepath: str) -> Any

Yield the documents of one WARC file (datatrove's read_file).

pyroid.integrations.nemo_curator

Each class subclasses NeMo Curator 1.4.0's class of the same name and takes its arguments; the stages also take the keyword threads.

Drop-in NeMo Curator text stages backed by pyroid.

Change the imports and leave the pipeline as it is:

# from nemo_curator.stages.text.filters import ScoreFilter
# from nemo_curator.stages.text.filters.heuristic import WordCountFilter
from pyroid.integrations.nemo_curator import ScoreFilter, WordCountFilter

pipeline.add_stage(ScoreFilter(WordCountFilter(min_words=80)))

The stages ScoreFilter, Score, Filter and Modify subclass NeMo's and take the same arguments, plus a keyword threads (pyroid's threads=, default 1: NeMo already runs one worker per CPU). They score or modify a whole batch in one pyroid call instead of calling score_document / modify_document once per row through pandas.Series.apply, and they do this for NeMo's own filter and modifier objects too, so a pipeline built from NeMo's YAML configs only needs its ScoreFilter / Modify targets changed (or pyroid.patch with "nemo_curator").

The filters and modifiers here subclass NeMo's classes of the same name and take the same arguments. Their score_document / modify_document run pyroid's kernel for one document, so they are faster inside NeMo's own stages as well:

  • the heuristic filters of nemo_curator.stages.text.filters.heuristic (string and repetition);
  • FastTextLangId and FastTextQualityFilter, which load their model with pyroid.fasttext (fasttext, which needs a C++ compiler to install, is not needed);
  • UnicodeReformatter (pyroid.ftfy; ftfy is not needed), and the string modifiers BoilerPlateStringModifier, MarkdownRemover, NewlineNormalizer, QuotationRemover, UrlRemover, LineRemover, Slicer and FastTextLabelModifier.

The output is the same as with NeMo's code: the same scores (same values and pandas dtypes), the same kept rows, the same text, and the same exception where NeMo raises one (for a document NeMo's code would fail on, the stage runs NeMo's code, which raises). Anything pyroid does not port runs NeMo's own code, with a one-time pyroid.FallbackWarning where a setting caused it: a custom url_regex, Japanese word splitting (MeCab), precomputed sentences / paragraphs / ngrams on a filter, subclasses of these classes, and multi-input modifiers.

RepeatingTopNGramsFilter breaks ties between equally frequent n-grams the way NeMo does: by the order of a Python set, which depends on the process's string hash seed. NeMo's own scores for such documents therefore change from run to run unless PYTHONHASHSEED is fixed; pyroid gives the score NeMo gives in the same process.

Needs NeMo Curator 1.4.0 (Python 3.11 to 3.13); pyroid.nemo has the same kernels without it.

ScoreFilter dataclass

ScoreFilter(*, threads: int | None = 1)

NeMo's ScoreFilter, scoring and filtering each batch in one pyroid call.

The same arguments as NeMo's, plus threads (keyword; default 1).

compute_filter_mask

compute_filter_mask(
    df: DataFrame,
    filter_obj: Any,
    text_field: str,
    score_field: str | None,
    invert: bool,
) -> Series

Score df[text_field] in one pyroid call and return NeMo's keep mask.

Score dataclass

Score(*, threads: int | None = 1)

NeMo's Score, scoring each batch in one pyroid call.

The same arguments as NeMo's, plus threads (keyword; default 1).

process

process(batch: Any) -> Any

Score the batch, one pyroid call per filter.

Filter dataclass

Filter(*, threads: int | None = 1)

NeMo's Filter, deciding each batch with array comparisons.

The same arguments as NeMo's, plus threads (keyword; default 1; unused, for symmetry).

compute_filter_mask

compute_filter_mask(
    df: DataFrame,
    filter_fn: Any,
    filter_field: str,
    invert: bool,
) -> Series

NeMo's keep mask for the scores in df[filter_field], as array comparisons.

Modify dataclass

Modify(*, threads: int | None = 1)

NeMo's Modify, modifying each batch in one pyroid call.

The same arguments as NeMo's, plus threads (keyword; default 1). Modifiers with several input fields run NeMo's code.

process

process(batch: Any) -> Any

Modify the batch, one pyroid call per modifier.

FastTextLangId

FastTextLangId(
    model_path: str | None = None,
    min_langid_score: float = 0.3,
    lang: str | None = None,
)

NeMo's FastTextLangId, with the model loaded by pyroid.fasttext.

The same arguments and the same score strings (str([score, lang])) as NeMo's.

model_check_or_download

model_check_or_download() -> None

Raise FileNotFoundError if the model file is missing, as NeMo does.

load_model

load_model() -> None

Load the model with pyroid.fasttext.load_model.

score_document

score_document(text: str) -> str

NeMo's score string str([score, lang]) for one document.

keep_document

keep_document(score: float | str) -> bool

NeMo's decision for a score string.

FastTextQualityFilter

FastTextQualityFilter(
    model_path: str | None = None,
    label: str = "__label__hq",
    alpha: float = 3,
    seed: int = 42,
)

NeMo's FastTextQualityFilter, with the model loaded by pyroid.fasttext.

The same arguments, scores and draws: keep_document draws from numpy's global random state (seeded once, when the filter is created), as NeMo does, also in pyroid's batched stages.

model_check_or_download

model_check_or_download() -> None

Raise FileNotFoundError if the model file is missing, as NeMo does.

load_model

load_model() -> None

Load the model with pyroid.fasttext.load_model.

score_document

score_document(text: str) -> float

The probability of label (or 1 minus the top label's), as NeMo computes it.

keep_document

keep_document(score: float) -> bool

Draw from numpy's global random state and compare, as NeMo does.

UnicodeReformatter

UnicodeReformatter(
    config: Any = None,
    unescape_html: str | bool = "auto",
    remove_terminal_escapes: bool = True,
    fix_encoding: bool = True,
    restore_byte_a0: bool = True,
    replace_lossy_sequences: bool = True,
    decode_inconsistent_utf8: bool = True,
    fix_c1_controls: bool = True,
    fix_latin_ligatures: bool = False,
    fix_character_width: bool = False,
    uncurl_quotes: bool = False,
    fix_line_breaks: bool = False,
    fix_surrogates: bool = True,
    remove_control_chars: bool = True,
    normalization: Literal["NFC", "NFD", "NFKC", "NFKD"]
    | None = None,
    max_decode_length: int = 1000000,
    explain: bool = True,
)

NeMo's UnicodeReformatter through pyroid.ftfy (ftfy itself is not needed).

The same arguments and defaults as NeMo's, which differ from ftfy's: no Unicode normalization, and ligatures, character widths, curly quotes and line breaks are left alone.

modify_document

modify_document(text: str) -> str

pyroid.ftfy.fix_text(text, config=self.config).

pyroid.integrations.hf

Dataset.map and Dataset.filter helpers for Hugging Face datasets.

Each helper returns a callable for batched=True:

import datasets
from pyroid.integrations import hf

ds = datasets.Dataset.from_parquet("fineweb.parquet").with_format("arrow")
ds = ds.map(hf.fix_text(), batched=True)  # ftfy
ds = ds.map(hf.langid(), batched=True)  # + language, language_score
ds = ds.filter(hf.quality_mask(), batched=True)  # the Gopher, C4 and FineWeb filters

res = hf.fineweb(datasets.Dataset.from_parquet("fineweb.parquet"))  # the whole recipe
res.dataset.to_parquet("clean.parquet")
print(res.stats)

The callables take both batch kinds datasets hands out:

  • Arrow batches (ds.with_format("arrow"), the zero-copy path). The callable gets a pyarrow.Table whose text column pyroid reads in place, without building a Python str for it. A map callable returns a pyarrow.Table with every column of the batch. datasets replaces the batch with it, so remove_columns= has no effect: call .remove_columns() on the dataset. A filter callable returns a boolean pyarrow.ChunkedArray.
  • Python batches (the default format). The callable gets the dict of lists and returns a dict of the new or changed columns, which datasets merges into the batch, or a list[bool] for filter. None texts stay None and are dropped by filters.

Arrow nulls are dropped by filters too (a null mask entry drops the row). Don't pass input_columns=: the callables read their column by name. They work the same in a streaming IterableDataset's map and filter, except that an Arrow-format IterableDataset.filter needs datasets 5 (older versions reject it whatever the function). fineweb needs a Dataset, because MinHash reads the survivors back. Tested with datasets 3.6, 4.8 and 5.1.

The callables pickle by their settings, so datasets can fingerprint them (cached results are reused) and ship them to num_proc worker processes, each of which loads the fastText model itself. A loaded pyroid.fasttext.Model can't be pickled: pass its path for both.

Needs pyarrow, numpy and datasets (pip install 'pyroid[hf]').

LidModel module-attribute

LidModel: TypeAlias = (
    "str | os.PathLike[str] | pyroid.fasttext.Model | None"
)

ANNOTATION_COLUMNS module-attribute

ANNOTATION_COLUMNS = (
    "keep",
    "reason",
    "language",
    "language_score",
)

Stats dataclass

Stats(
    docs: int,
    kept: int,
    dropped: dict[str, int] = dict(),
    reasons: dict[str, int] = dict(),
    seconds: dict[str, float] = dict(),
)

What fineweb did.

Attributes:

  • docs (int) –

    Documents in.

  • kept (int) –

    Documents out.

  • dropped (dict[str, int]) –

    Documents dropped per stage, in chain order (lid, the four filters, minhash; null too if there were null texts).

  • reasons (dict[str, int]) –

    Documents dropped per reason ("gopher_quality:gopher_short_doc", "minhash", ...), most frequent first.

  • seconds (dict[str, float]) –

    Wall time: annotate (the Dataset.map of ftfy, LID and the filters), dedup (MinHash over the survivors), total (including building the output datasets).

FineWebResult dataclass

FineWebResult(
    dataset: Dataset, annotated: Dataset, stats: Stats
)

What fineweb returns.

Attributes:

  • dataset (Dataset) –

    The kept documents, in order: cleaned text, the input's other columns, and language/language_score.

  • annotated (Dataset) –

    Every document, with the text as it left the chain and the columns keep (the final decision) and reason (None if kept; "minhash" for a near-duplicate; see annotate).

  • stats (Stats) –

    Counts and timings.

fix_text

fix_text(
    column: str = "text",
    config: TextFixerConfig | None = None,
    *,
    threads: int | None = None,
    **kwargs: Any,
) -> Callable[[Batch], Any]

A Dataset.map(batched=True) function that runs ftfy.fix_text on a column.

Parameters:

Returns:

  • Callable[[Batch], Any] –

    The map function (see the module docstring for the batch kinds).

langid

langid(
    lid_model: LidModel = None,
    column: str = "text",
    *,
    k: int = 1,
    threads: int | None = None,
    language_column: str = "language",
    score_column: str = "language_score",
) -> Callable[[Batch], Any]

A Dataset.map(batched=True) function that adds fastText language ID columns.

Each document is predicted as one line (newlines become spaces, as in datatrove's FastTextLID).

Parameters:

  • lid_model (LidModel, default: None ) –

    A path to a fastText LID model (lid.176.bin), a loaded pyroid.fasttext.Model (in-process only, see the module docstring), or None for pyroid.integrations.recipes.lid_model_path ($PYROID_LID_MODEL, else downloaded once into a cache).

  • column (str, default: 'text' ) –

    The text column.

  • k (int, default: 1 ) –

    Number of labels; 1 adds a string and a float column, any other value (-1: all labels) adds list columns, best first.

  • threads (int | None, default: None ) –

    pyroid's threads=.

  • language_column (str, default: 'language' ) –

    Name of the label column (labels without __label__).

  • score_column (str, default: 'language_score' ) –

    Name of the probability column (float64).

Returns:

  • Callable[[Batch], Any] –

    The map function. Null texts get null labels.

quality_mask

quality_mask(
    column: str = "text",
    filters: Sequence[Any] | None = None,
    *,
    threads: int | None = None,
) -> Callable[[Batch], Any]

A Dataset.filter(batched=True) function: keep what passes the quality filters.

The filters run in order, as datatrove's pipeline does (pyroid.filters.apply). Only the keep decision is used: C4Quality's line removal is not applied to the text (use annotate or fineweb for the text as it leaves the chain).

Parameters:

  • column (str, default: 'text' ) –

    The text column.

  • filters (Sequence[Any] | None, default: None ) –

    pyroid.filters configs or datatrove filter instances, in order; None is FineWeb's set (GopherRepetition, GopherQuality, C4Quality(filter_no_terminal_punct=False), FineWebQuality).

  • threads (int | None, default: None ) –

    pyroid's threads=.

Returns:

keep_mask

keep_mask(column: str = 'keep') -> Callable[[Batch], Any]

A Dataset.filter(batched=True) function that reads a boolean column.

For instance the keep column of annotate (ds.filter(hf.keep_mask(), batched=True)). Nulls drop the row.

Parameters:

  • column (str, default: 'keep' ) –

    The boolean column.

Returns:

annotate

annotate(
    column: str = "text",
    *,
    lid_model: LidModel = None,
    threads: int | None = None,
) -> Callable[[Batch], Any]

A Dataset.map(batched=True) function that runs the FineWeb chain but the dedup.

Per batch, pyroid.integrations.recipes.FineWeb.annotate: ftfy -> LID (en, p > 0.65) -> GopherRepetition -> GopherQuality -> C4Quality -> FineWebQuality. column becomes the text as it leaves the chain (for a dropped document, the text the dropping stage saw) and these columns are set: keep (bool), reason (None if kept, else "lid", "<stage>:<datatrove reason>" or "null"), language and language_score. Drop with ds.filter(hf.keep_mask(), batched=True); MinHash dedup needs the whole corpus: see fineweb.

Parameters:

  • column (str, default: 'text' ) –

    The text column.

  • lid_model (LidModel, default: None ) –

    As for langid.

  • threads (int | None, default: None ) –

    pyroid's threads=.

Returns:

fineweb

fineweb(
    ds: Dataset,
    column: str = "text",
    *,
    lid_model: LidModel = None,
    threads: int | None = None,
    batch_size: int = 1000,
    num_proc: int | None = None,
    **map_kwargs: Any,
) -> FineWebResult

Run the FineWeb recipe on a dataset, MinHash dedup included.

  1. ds.map(annotate(...), batched=True) on Arrow batches: ftfy, LID and the four quality filters (annotate).
  2. MinHash (datatrove's FineWeb settings) over the survivors, read back in dataset order: each cluster of near-duplicates keeps one document, as in datatrove, and the others are dropped (reason="minhash"). This step always runs in this process over the whole dataset, so it is global and its result does not depend on batch_size or num_proc.

On the same documents, the result equals datatrove's FineWeb pipeline (bench/chain.py's digest).

Parameters:

  • ds (Dataset) –

    A datasets.Dataset (any format; the results get the same format type).

  • column (str, default: 'text' ) –

    The text column.

  • lid_model (LidModel, default: None ) –

    As for langid.

  • threads (int | None, default: None ) –

    pyroid's threads= for every stage. With num_proc worker processes, about os.cpu_count() // num_proc avoids oversubscription.

  • batch_size (int, default: 1000 ) –

    Documents per Dataset.map batch (and per dedup read).

  • num_proc (int | None, default: None ) –

    Dataset.map's worker processes for step 1 (each loads the fastText model). pyroid already runs every batch on all cores, so this rarely helps; lid_model must be a path (or None).

  • **map_kwargs (Any, default: {} ) –

    Passed to step 1's Dataset.map (keep_in_memory, load_from_cache_file, cache_file_name, desc, ...).

Returns:

Raises:

  • ValueError –

    ds is empty, or num_proc > 1 with a loaded pyroid.fasttext.Model.

pyroid.integrations.recipes

Cleaning recipes: the FineWeb chain, natively in pyroid.

FineWeb is the chain the pyroid benchmark gates against datatrove (bench/chain.py), over Arrow batches, as a reusable object:

ftfy.fix_text (ftfy's default config)
-> LanguageFilter(languages=["en"], language_threshold=0.65), fastText lid.176.bin
-> GopherRepetitionFilter() -> GopherQualityFilter()
-> C4QualityFilter(filter_no_terminal_punct=False) -> FineWebQualityFilter()
-> MinHash dedup (sha1/64, 14 buckets x 8 hashes, word 5-grams) over the survivors, in order

Per batch, FineWeb.annotate runs every stage but the dedup and returns Arrow columns (no Python str is built for the texts). Dedup is a property of the whole corpus: feed the survivors of each batch, in order, to the index from FineWeb.new_dedup and read its keep mask at the end. This is what pyroid clean --recipe fineweb does.

FineWebWarc is datatrove's examples/fineweb.py from WARC files (pyroid clean --recipe fineweb-warc): pyroid.warc reads the pages, then the URL filter, TaskExtractor (datatrove's Trafilatura step), FineWeb without ftfy, MinHash and PII replacement.

Needs pyarrow and numpy (pip install 'pyroid[cli]').

LID_176_URL module-attribute

LID_176_URL = "https://dl.fbaipublicfiles.com/fasttext/supervised-models/lid.176.bin"

LID_176_MIRRORS module-attribute

LID_176_MIRRORS: tuple[str, ...] = (
    "https://huggingface.co/julien-c/fasttext-language-id/resolve/main/lid.176.bin",
)

LID_176_SHA256 module-attribute

LID_176_SHA256 = "7e69ec5451bc261cc7844e49e4792a85d7f09c06789ec800fc4a44aec362764e"

MINHASH_CONFIG module-attribute

MINHASH_CONFIG: dict[str, Any] = {
    "n_grams": 5,
    "num_buckets": 14,
    "hashes_per_bucket": 8,
    "seed": 1,
    "hash_fc": "sha1",
    "precision": 64,
}

FINEWEB_TRAFILATURA module-attribute

FINEWEB_TRAFILATURA: dict[str, Any] = {
    "compat": "1.11.0",
    "favor_precision": True,
    "include_comments": False,
}

Annotation dataclass

Annotation(
    text: ChunkedArray,
    keep: ChunkedArray,
    reason: ChunkedArray,
    language: ChunkedArray,
    language_score: ChunkedArray,
)

What FineWeb.annotate returns: one value per input document.

Attributes:

  • text (ChunkedArray) –

    The text as it leaves the chain (after ftfy and C4's line removal); for a dropped document, the text the dropping stage saw.

  • keep (ChunkedArray) –

    True if the document survived every stage but the dedup.

  • reason (ChunkedArray) –

    None if kept, else "lid" or "<stage>:<datatrove reason>" (e.g. "gopher_quality:gopher_short_doc"); "null" for a null text.

  • language (ChunkedArray) –

    fastText's top label, without __label__ (datatrove's language metadata).

  • language_score (ChunkedArray) –

    Its probability (datatrove's language_score).

FineWeb

FineWeb(
    *,
    lid_model: Any = None,
    languages: Sequence[str] | None = ("en",),
    language_threshold: float = 0.65,
    ftfy_config: Any = None,
    filters: Sequence[Any] | None = None,
    minhash: Any = MINHASH_CONFIG,
    threads: int | None = None,
)

The FineWeb chain (see the module docstring), stage for stage as datatrove.

Parameters:

  • lid_model (Any, default: None ) –

    A pyroid.fasttext.Model, a path to lid.176.bin, or None for lid_model_path's default (loaded on first use).

  • languages (Sequence[str] | None, default: ('en',) ) –

    Languages to keep (None: any).

  • language_threshold (float, default: 0.65 ) –

    datatrove LanguageFilter's language_threshold.

  • ftfy_config (Any, default: None ) –

    A TextFixerConfig (pyroid.ftfy's or ftfy's); None is ftfy's default config, as in the benchmark chain. False skips ftfy.

  • filters (Sequence[Any] | None, default: None ) –

    The quality filters, in order (pyroid.filters configs or datatrove filter instances); None is the FineWeb set above.

  • minhash (Any, default: MINHASH_CONFIG ) –

    MinHash settings for new_dedup (a dict of MinhashConfig fields, or a MinhashConfig); None disables dedup.

  • threads (int | None, default: None ) –

    Default threads= for every stage (None: the whole pool).

Language decisions are datatrove's: keep iff some requested language's probability is above the threshold. Above 0.501 that can only be the top label (fastText's probabilities sum to at most 1 + 176e-5), so the model is asked for its top label only; at lower thresholds it is asked for all labels.

model property

model: Any

The pyroid.fasttext.Model (loaded on first use).

default_filters staticmethod

default_filters() -> list[Any]

FineWeb's quality filters, in datatrove examples/fineweb.py's order.

stages

stages() -> list[str]

Stage names, in order ("null" is not a stage: it marks null texts).

new_dedup

new_dedup(
    *, threads: int | None = None
) -> MinHashDedup | None

A fresh MinHash index for this recipe's survivors (None if dedup is off).

fix

fix(
    texts: Any, *, threads: int | None = None
) -> ChunkedArray

The ftfy stage alone.

annotate

annotate(
    texts: Any, *, threads: int | None = None
) -> Annotation

Run ftfy, LID and the filters on one batch (see Annotation).

Parameters:

  • texts (Any) –

    A list of str, or an Arrow string column (pyarrow, a pyroid.Column, an HF dataset column, ...). Arrow nulls are dropped with reason "null".

  • threads (int | None, default: None ) –

    Overrides the recipe's threads.

FineWebWarc

FineWebWarc(
    *,
    lid_model: Any = None,
    threads: int | None = None,
    url_filter: Any = None,
    trafilatura: dict[str, Any] | None = None,
)

datatrove examples/fineweb.py from WARC files, stage for stage as datatrove 0.10.1.

Per WARC file (one datatrove task each):

WarcReader (pyroid.warc.iter_documents: warcio's parsing, datatrove's process_record)
-> URLFilter() -> Trafilatura(favour_precision=True), trafilatura 1.11.0
-> LanguageFilter(languages=["en"], language_threshold=0.65)
-> GopherRepetitionFilter() -> GopherQualityFilter()
-> C4QualityFilter(filter_no_terminal_punct=False) -> FineWebQualityFilter()

then MinHash dedup (MINHASH_CONFIG) over the survivors of every file, in file order, and PIIFormatter() on what is left, a fresh rotation per file. documents, annotate (with a new_task extractor per file), new_dedup and new_pii are the pieces; pyroid clean --recipe fineweb-warc runs them.

Parameters:

  • lid_model (Any, default: None ) –

    As FineWeb's.

  • threads (int | None, default: None ) –

    Default threads= for every stage (None: the whole pool).

  • url_filter (Any, default: None ) –

    A pyroid.filters.URLFilter (default: URLFilter(), built at first use: it needs tldextract and datatrove's block lists).

  • trafilatura (dict[str, Any] | None, default: None ) –

    trafilatura.extract arguments over FINEWEB_TRAFILATURA.

url_filter property

url_filter: URLFilter

The pyroid.filters.URLFilter (built on first use).

stages

stages() -> list[str]

Stage names, in order.

documents staticmethod

documents(
    source: Any,
    *,
    compression: Literal["infer", "gzip"] | None = "infer",
    limit: int = -1,
) -> Iterator[Document]

The documents of one WARC file, as datatrove's WarcReader makes them.

{"text", "id", "url", "date"} per document, from pyroid.warc.iter_documents; documents with an empty text are skipped and at most limit are read (-1: all), as WarcReader does within one task.

new_task

new_task() -> TaskExtractor

The Trafilatura step of one task (one per WARC file, in the file's order).

new_dedup

new_dedup(
    *, threads: int | None = None
) -> MinHashDedup | None

A fresh MinHash index for the survivors of every file, in file order.

new_pii staticmethod

new_pii() -> PIIReplacer

PIIFormatter() for one file (one task): its replacement rotation starts at 0.

annotate

annotate(
    docs: Sequence[Mapping[str, Any]],
    task: TaskExtractor,
    *,
    threads: int | None = None,
) -> WarcAnnotation

Run the URL filter, Trafilatura, LID and the quality filters on one batch.

Parameters:

WarcAnnotation dataclass

WarcAnnotation(
    text: list[str],
    keep: list[bool],
    reason: list[str | None],
    language: list[str | None],
    language_score: list[float | None],
)

What FineWebWarc.annotate returns: one value per input document.

Attributes:

  • text (list[str]) –

    The text as it leaves the chain; for a dropped document, the text the dropping stage saw (the HTML for the URL filter and Trafilatura).

  • keep (list[bool]) –

    True if the document survived every stage but MinHash.

  • reason (list[str | None]) –

    None if kept, else "url_filter:<reason>", "trafilatura" (no text extracted), "trafilatura:broken_process" (the extraction raised), "lid" or "<stage>:<datatrove reason>".

  • language (list[str | None]) –

    fastText's top label (None before the LID stage).

  • language_score (list[float | None]) –

    Its probability.

TaskExtractor

TaskExtractor(*, threads: int | None = None, **kwargs: Any)

datatrove's Trafilatura step over the pages of one task, on pyroid.trafilatura.

Make one per datatrove task (in FineWeb, one per WARC file): it starts with an empty dedup cache, as the step's child process does. Called on a list of HTML pages, it returns for each the extracted text (None or "" when there is none: the step drops those), or the exception its extraction raised. datatrove's child process dies on such a page (the step counts broken_process and drops it) and the next child starts with an empty cache, so the cache is emptied after it here too. Pages are extracted in order: the dedup cache depends on it.

Parameters:

  • threads (int | None, default: None ) –

    threads= of pyroid.trafilatura.extract_batch (None: the whole pool; with deduplication the pages are still extracted in order; threads parse ahead).

  • **kwargs (Any, default: {} ) –

    trafilatura.extract arguments over FINEWEB_TRAFILATURA. With compat="1.11.0" the dedup cache is pyroid.trafilatura.LRU_TEST, as trafilatura 1.11.0 has only its module-level cache; with "2.3.1", an LRUCache of the task.

reset

reset() -> None

Empty the dedup cache (a new datatrove child process).

__call__

__call__(htmls: Sequence[str]) -> list[Any]

The extracted text of each page (str/None), or the exception it raised.

lid_model_path

lid_model_path(
    path: str | PathLike[str] | None = None,
    *,
    download: bool = True,
) -> str

The fastText lid.176.bin file to use.

Parameters:

  • path (str | PathLike[str] | None, default: None ) –

    An explicit model file; returned as is.

  • download (bool, default: True ) –

    When neither path nor $PYROID_LID_MODEL is set and the cached copy is missing, download it from LID_176_URL (else from LID_176_MIRRORS) into the cache ($PYROID_CACHE_DIR, else $XDG_CACHE_HOME/pyroid, else ~/.cache/pyroid) and check its sha256.

Returns:

  • str –

    The model path.

Raises: