chunk-engine
API Reference

Python — py-chunks

Every name py-chunks exports, with exact signatures, return types and the exceptions each raises.

View raw
pip install py-chunks

The package is py-chunks on PyPI and imports as py_chunks. It ships py.typed and a complete stub for the compiled module, so mypy and pyright see full annotations. py_chunks.__version__ reads the installed distribution's metadata, so it cannot drift from what pip installed.

What the package exports

py_chunks.__all__ contains 55 names, in five groups:

GroupCountNames
Package metadata1__version__
Result types2ChunksResult, MarkdownResult
Source-agnostic entry points13get_chunks, get_markdown, stream_chunks, and the _from_path / _from_bytes / _from_fileobj / _from_upload / _from_s3_presigned_url families
Per-format chunkers3618 chunk_* / stream_chunk_* pairs

Source-agnostic entry points

These are the 13 functions most callers use. All keyword arguments after * are keyword-only.

get_chunks

def get_chunks(
    source,
    *,
    filename: str | None = None,
    mode: str = "default",
    window_size: int = 3,
    overlap: int = 1,
    sentences_per_chunk: int = 3,
    paragraphs_per_page: int = 15,
    delimiter: str | None = None,
    encoding: str = "auto",
    list_images: bool = False,
) -> list[dict] | ChunksResult

source may be a path (str or os.PathLike), an http(s) URL string, bytes / bytearray / memoryview, a file-like object with .read(), or a framework upload object with a .filename attribute. Returns list[dict], or a ChunksResult when list_images=True (typed via @overload, so a checker narrows the return for a literal True/False).

get_markdown

def get_markdown(
    source,
    *,
    filename: str | None = None,
    list_images: bool = False,
) -> str | MarkdownResult

get_markdown takes no chunking parameters — it produces the document's Markdown, not chunks. Returns str, or a MarkdownResult when list_images=True.

stream_chunks

def stream_chunks(
    source,
    *,
    filename: str | None = None,
    mode: str = "default",
    window_size: int = 3,
    overlap: int = 1,
    sentences_per_chunk: int = 3,
    paragraphs_per_page: int = 15,
    delimiter: str | None = None,
    encoding: str = "auto",
)  # -> an iterator yielding chunk dicts

No list_images — streaming yields chunks only. See Streaming for which formats are genuinely incremental.

The explicit source helpers

Ten more functions skip source detection. Each mirrors the corresponding get_chunks / stream_chunks signature above, differing only in the leading positional parameters:

FunctionLeading parametersReturns
get_chunks_from_pathfile_path: strlist[dict] | ChunksResult
get_chunks_from_bytesdata: bytes, filename: strlist[dict] | ChunksResult
get_chunks_from_fileobjfile_obj, filename: str | None = Nonelist[dict] | ChunksResult
get_chunks_from_uploadupload_filelist[dict] | ChunksResult
get_chunks_from_s3_presigned_urlurl: str, filename: str | None = None, timeout: int = 60list[dict] | ChunksResult
stream_chunks_from_pathfile_path: strIterator[dict]
stream_chunks_from_bytesdata: bytes, filename: strIterator[dict]
stream_chunks_from_fileobjfile_obj, filename: str | None = NoneIterator[dict]
stream_chunks_from_uploadupload_fileIterator[dict]
stream_chunks_from_s3_presigned_urlurl: str, filename: str | None = None, timeout: int = 60Iterator[dict]

Every one of them then takes the same keyword-only block: mode="default", window_size=3, overlap=1, sentences_per_chunk=3, paragraphs_per_page=15, delimiter=None, encoding="auto" — plus list_images=False on the get_chunks_* five.

get_chunks_from_path takes file_path positionally and does not accept a filename override; the extension comes from the path.

Bytes never touch the disk

Since 0.6.1, get_chunks_from_bytes and the file-object / upload / URL paths that funnel into it call the engine's no-filesystem API directly. The one exception is stream_chunks_from_bytes, which still writes a temporary file because the engine's streaming surface is path-based; the temp file is deleted when the iterator is exhausted, closed, or garbage-collected.

Per-format chunkers

18 modules, each exporting a chunk_* / stream_chunk_* pair. They take a path (never bytes), skip source detection, and return a (chunks, timing) tuple where timing is {"rust_ms": float, "python_ms": float}. The streaming half returns an iterator of dict.

PairExtensionsSignature after file_path
chunk_csv / stream_chunk_csv.csvmode="row", rows_per_chunk=10, window_size=5, overlap=1, include_headers=True, delimiter=None, encoding="auto", skip_empty_rows=True
chunk_tsv / stream_chunk_tsv.tsvsame as CSV, but delimiter="\t"
chunk_doc / stream_chunk_doc.docstandard
chunk_docx / stream_chunk_docx.docx, .docm, .dotx, .dotmstandard
chunk_eml / stream_chunk_eml.eml, .mboxstandard
chunk_epub / stream_chunk_epub.epubstandard
chunk_html / stream_chunk_html.html, .htmstandard
chunk_ipynb / stream_chunk_ipynb.ipynbstandard
chunk_json / stream_chunk_json.json, .jsonl, .ndjsonstandard
chunk_md / stream_chunk_md.mdstandard
chunk_msg / stream_chunk_msg.msgstandard
chunk_odf / stream_chunk_odf.odt, .odpstandard
chunk_pdf / stream_chunk_pdf.pdfstandard
chunk_ppt / stream_chunk_ppt.pptstandard
chunk_pptx / stream_chunk_pptx.pptx, .potx, .potm, .ppsx, .ppsmstandard, but paragraphs_per_page=5 and an extra slides_per_chunk: int | None = None
chunk_rtf / stream_chunk_rtf.rtfstandard
chunk_txt / stream_chunk_txt.txtstandard
chunk_xlsx / stream_chunk_xlsx.xlsx, .xls, .xlsm, .xlsb, .ods, .xltx, .xltmsee below

standard means mode="default", window_size=3, overlap=1, sentences_per_chunk=3, paragraphs_per_page=15.

There is no chunk_xls: .xls is one of the seven spreadsheet extensions routed through chunk_xlsx.

The delimited and spreadsheet signatures in full

def chunk_csv(
    file_path: str,
    mode: str = "row",
    rows_per_chunk: int = 10,
    window_size: int = 5,
    overlap: int = 1,
    include_headers: bool = True,
    delimiter: str | None = None,
    encoding: str = "auto",
    skip_empty_rows: bool = True,
) -> tuple[list[dict], dict]

def stream_chunk_csv(...)   # identical signature, returns an iterator

Modes accepted: row, default, sliding_window, page_aware. delimiter accepts only None, ",", "\t", ";" or "|".

def chunk_xlsx(
    file_path: str,
    mode: str = "row",
    rows_per_chunk: int = 1,
    window_size: int = 3,
    overlap: int = 1,
    include_headers: bool = True,
    sheet_names: list[str] | None = None,
    skip_empty_rows: bool = True,
    serialize_as: str = "key_value",
    max_chunk_chars: int = 2000,
) -> tuple[list[dict], dict]

def stream_chunk_xlsx(
    file_path: str,
    mode: str = "row",
    rows_per_chunk: int = 1,
    window_size: int = 3,
    overlap: int = 1,
    include_headers: bool = True,
    sheet_names: list[str] | None = None,
    skip_empty_rows: bool = True,
    max_chunk_chars: int = 2000,
)

Modes accepted: row, table, sheet, sliding_window, page_aware, semantic. serialize_as currently accepts only "key_value", and it exists on chunk_xlsx alone — the streaming half hard-codes it.

window_size defaults differ

window_size defaults to 5 in chunk_csv / stream_chunk_csv (and in the TSV wrappers, which inherit it) and to 3 in chunk_xlsx / stream_chunk_xlsx and in all sixteen other pairs. The source-agnostic get_chunks always uses 3. If you switch from get_chunks to chunk_csv without passing window_size, sliding_window output changes.

Result types

Both are dataclasses, not tuples — access by attribute.

@dataclass
class ChunksResult:
    chunks: list[dict]
    images: dict[str, bytes] = field(default_factory=dict)

@dataclass
class MarkdownResult:
    markdown: str
    images: dict[str, bytes] = field(default_factory=dict)

images is a dict keyed by image name — the JavaScript SDK returns the same data as an array of { name, data }. See Output Schema.

Exceptions

py-chunks defines no exception classes of its own; every failure is a Python built-in. The table below covers the ones you will see in practice, but it is not a closed set — treat any Exception subclass as possible.

ExceptionRaised when
FileNotFoundErrorThe path does not exist — checked in the Python layer before the engine is called.
ValueErrorUnsupported extension; unknown mode; an invalid parameter (overlap >= window_size, a zero count, an unsupported delimiter); empty data; a byte or file-object source with no resolvable filename.
TypeErrorsource is a type the package does not accept; .read() returned something that is not bytes; an upload object whose read() is a coroutine.
NotImplementedErrorStreaming not yet supported for {ext} files — the extension has no streaming entry point. All 36 supported extensions have one, so this is currently unreachable. A mis-typed mode no longer lands here: the Word family used to report Streaming for <mode> mode coming soon, which was doubly misleading (all seven DOCX modes stream fine), and it now raises ValueError like every other format.
RuntimeErrorThe engine failed to parse or decode the document.
OSErrorAn I/O failure inside the engine.
OverflowErrorA negative count reached the Rust boundary before any Python-layer range check could reject it — can't convert negative int to unsigned. Seen on the formats whose validation lives in the engine: .json, .jsonl, .ndjson, .eml, .mbox, .msg, .rtf, .ipynb, .odt, .odp, .epub. The same negative value is a plain ValueError on .md, .docx, .pdf and friends.

RuntimeError, OSError and OverflowError come from the Rust boundary. The engine's error type has four variants, mapped as:

Engine variantPython exception
ChunkError::UnsupportedValueError
ChunkError::InvalidArgValueError
ChunkError::ParseRuntimeError
ChunkError::IoOSError
any future variantRuntimeError

For parse errors the engine raises, the message text is the engine's own, unprefixed — which is what makes it byte-identical to the message js-chunks reports for the same input.

Other checks run earlier, in py-chunks' own Python layer. Two of those messages are py-chunks-specific: the unsupported-extension message (which appends the full supported set) and the invalid-mode message (one uniform sorted-list template, where js-chunks reports the engine's per-format sentence).

The numeric-range messages are not among them any more. The formats whose range checks live in Python (DOCX, DOC, PPT, PPTX and the spreadsheet family) now use the engine's own wording verbatim, so py-chunks and js-chunks both say window_size must be greater than 0 — and so do the document and spreadsheet families as each other. Only where a format renames the parameter does the sentence change, and it changes identically in both SDKs (CSV/TSV report rows_per_chunk must be greater than 0 for a zero paragraphs_per_page).

The exception type matches js-chunks' kind in every case, so branch on the exception type, not on the message — that stays the right habit even now that the range wording agrees, because the two host-layer messages above still do not. See Error Handling.

fit_tokens — fit chunks to a token budget

fit_tokens(
    chunks, counter, budget, *,
    overlap=0,                    # int tokens, or a fraction of budget (Python: 0 <= f < 1)
    tokenizer_kwargs=None,        # forwarded to the tokenizer's encode()
    min_tokens=0,
    merge="forward",              # "forward" | "none"
    split="sentence",             # "sentence" | "paragraph" | "hard"
    merge_metadata="first",       # "first" | "union"
    oversize="split",             # "split" | "keep" | "error"
    respect_boundaries=True,
    boundary_keys=("section_heading", "page_number", "sheet_name"),
) -> list[dict]

Takes get_chunks output and re-fits it so no chunk exceeds budget under your tokenizer. That guarantee holds for every input, including whitespace-free text — CJK prose, long URLs, base64 — because the split ladder bottoms out at a character-level bisection sized by your own counter. (Before 0.6.4 an indivisible whitespace-free run was silently emitted over budget.)

counter is anything you're likely to be holding — coercion is built in:

import py_chunks, tiktoken

enc = tiktoken.get_encoding("cl100k_base")
chunks = py_chunks.get_chunks("report.pdf")

fitted = py_chunks.fit_tokens(chunks, enc, 512)            # an Encoding
fitted = py_chunks.fit_tokens(chunks, "cl100k_base", 512)  # an encoding name
fitted = py_chunks.fit_tokens(chunks, "gpt-4", 512)        # a model name
fitted = py_chunks.fit_tokens(chunks, hf_tokenizer, 512)   # a HF tokenizer
fitted = py_chunks.fit_tokens(chunks, my_callable, 512)    # str -> int

Name strings resolve through tiktoken first, then transformers (each imported lazily, only if needed); HuggingFace counters default to add_special_tokens=False so BOS/EOS tokens are not billed to every chunk — override via tokenizer_kwargs. overlap shares tokens between the parts of a chunk that had to be split; it is deliberately scoped within one engine chunk (overlapping across chunks would smear their metadata and structural boundaries), and overlapped parts still respect budget — that is asserted, not assumed.

The same coercion is exported as py_chunks.coerce_counter(counter, tokenizer_kwargs=None) if you want the str -> int for your own use.

chunk_text — chunk a bare string

chunk_text(text, *, mode="default", window_size=3, overlap=1,
           sentences_per_chunk=3, paragraphs_per_page=15) -> list[dict]

No file, no extension: the string runs through the plain-text pipeline exactly as a .txt file's bytes would, so every mode and metadata key matches .txt behaviour. (get_chunks("some prose") deliberately raises — a path-taking API must never guess whether a string is a path or a document.) Composes with fit_tokens for a token budget:

fitted = py_chunks.fit_tokens(py_chunks.chunk_text(text), "cl100k_base", 512)

The chunks keep their content_type and metadata, including the repeated table headers that make a retrieved row interpretable — which is the difference between this and running a text splitter over the raw document.

Parity-exempt by design

Every other API in this library produces byte-identical output across py-chunks, js-chunks and rs-chunks. fit_tokens does not, and cannot. Its output depends on the token counter you pass, and tokenizers differ between languages — so putting a token budget in the engine would either vendor the wrong tokenizer for your embedding model or quietly break the byte-identical guarantee. Characters are parity-safe; tokens are not.

js-chunks ships the same helper as fitTokens — a separate, equally parity-exempt implementation, not a shared one.

Parameters worth knowing

parameterwhy you'd change it
min_tokensMerge chunks below this into the next one. 0 (default) disables merging entirely — only oversized chunks are touched.
respect_boundariesOn by default: never merge across a change of section_heading, page_number or sheet_name. Merging across them would make the surviving metadata a lie, and is the fastest way to destroy table-header repetition.
merge_metadata"first" keeps the leading chunk's values and never invents one. "union" collects differing values into a list, so a chunk spanning two pages reports both rather than claiming one.
oversizeWhat to do when a single indivisible piece still exceeds the budget: "split" cuts it, "keep" emits it whole, "error" raises naming the chunk.

An unrecognised value for merge, split, merge_metadata or oversize raises ValueError rather than falling through to a default — a typo like split="sentances" would otherwise quietly pick a different strategy.

Split chunks gain fit_part / fit_total in their metadata. The input list is never mutated.

Bigger chunks are not automatically better

It is tempting to raise min_tokens to pack chunks fuller. Measured on this project's own corpus, raising budget utilisation from 46% to 95% halved token-level precision with no gain in answerability. Use min_tokens to remove genuinely useless fragments, not to fill the budget.

On this page