# Quick Start

Chunk your first document, pick a mode, stream large files, and convert to Markdown — in Python, JavaScript, or Rust.

**Prerequisite:** the SDK for your language, installed. Everything on this page
works with a default install and no configuration.

- **Python** (PyPI): `pip install py-chunks`
- **JavaScript** (npm): `npm install js-chunks`
- **Rust** (crates.io): `cargo add rs-chunks`

## Your first chunks

Point the engine at a file. That is the whole API for the common case — there is
nothing to configure and no pipeline to assemble.

```python
from py_chunks import get_chunks

chunks = get_chunks("notes.md")
for c in chunks[:3]:
    print(c["content_type"], "|", c["content"][:40])

# heading | Chunking Notes
# plain_paragraph | Chunk-engine splits a document into retr
# heading | Why structure matters
```

```ts
import { getChunks } from "js-chunks";

const chunks = await getChunks("./notes.md");
for (const c of chunks.slice(0, 3)) {
  console.log(c.contentType, "|", c.content.slice(0, 40));
}

// heading | Chunking Notes
// plain_paragraph | Chunk-engine splits a document into retr
// heading | Why structure matters
```

```rust
use chunks_rs::get_chunks;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let chunks = get_chunks("notes.md", "default", 3, 1, 3, 15)?;
    for c in chunks.iter().take(3) {
        println!("{} | {}", c.content_type, c.content.chars().take(40).collect::<String>());
    }
    Ok(())
}

// heading | Chunking Notes
// plain_paragraph | Chunk-engine splits a document into retr
// heading | Why structure matters
```

Three fields come back on every chunk: `content` (the text), `content_type`
(what kind of block it was), and `metadata` (headings, indices, counts). See
[Output Schema](/docs/output-schema) for the full shape.

## Pick a mode

The `mode` argument decides how the document is cut up. It is the one knob worth
setting deliberately.

| I want to… | Mode | Typical use |
| --- | --- | --- |
| Feed coherent passages to an embedding model | `semantic` | RAG over prose |
| Keep everything under a heading together | `section` | Document search |
| Index each paragraph, heading and table separately | `default` / `structural` | Fine-grained retrieval |
| Fixed sentence count per chunk | `sentence` | Uniform chunk sizes |
| Overlapping windows | `sliding_window` | Dense retrieval, recall-first |
| Preserve pages or slides for citations | `page_aware` | "see page 4" answers |

Spreadsheets have their own modes (`row`, `table`, `sheet`); CSV and TSV take
`row`, `sliding_window` and `page_aware` (and `default`, an alias for `row`),
but not `table` or `sheet`. Start with
`semantic` for LLM input and `section` for a search index; the full comparison,
with real input→output examples, is in
[Chunking Modes](/docs/chunking-modes).

```python
from py_chunks import get_chunks

# Adjacent blocks merge when they look like the same idea. Every chunk records
# WHY it starts where it does, in primary_merge_reason (+ merge_reasons).
for c in get_chunks("notes.md", mode="semantic"):
    print(c["content_type"], "|", c["metadata"]["primary_merge_reason"])

# heading | initial
# semantic | initial
# heading | initial
# semantic | initial
# table | structural_boundary
# heading | initial
# semantic | initial
```

```ts
import { getChunks } from "js-chunks";

for (const c of await getChunks("./notes.md", { mode: "semantic" })) {
  console.log(c.contentType, "|", c.metadata.primary_merge_reason);
}

// heading | initial
// semantic | initial
// heading | initial
// semantic | initial
// table | structural_boundary
// heading | initial
// semantic | initial
```

```rust
use chunks_rs::get_chunks;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    for c in &get_chunks("notes.md", "semantic", 3, 1, 3, 15)? {
        println!("{} | {}", c.content_type, c.metadata["primary_merge_reason"]);
    }
    Ok(())
}

// heading | "initial"
// semantic | "initial"
// heading | "initial"
// semantic | "initial"
// table | "structural_boundary"
// heading | "initial"
// semantic | "initial"
```

## Streaming

Consume chunks one at a time instead of waiting for the whole list.

```python
from py_chunks import stream_chunks

# Yields one chunk at a time
for chunk in stream_chunks("data.csv", mode="row"):
    handle(chunk)
```

```ts
import { streamChunks } from "js-chunks";

// Async iterable — one chunk at a time
for await (const chunk of streamChunks("./data.csv", { mode: "row" })) {
  handle(chunk);
}
```

```rust
use chunks_rs::formats::csv;

// stream() is a native Iterator yielding Result<Chunk>
fn main() -> Result<(), Box<dyn std::error::Error>> {
    for c in csv::stream("data.csv", "row", 10, 5, 1, true, None, "utf-8", true)? {
        let c = c?;
        println!("{}", c.content_type);
    }
    Ok(())
}
```

  "Constant memory" is true for some formats and some runtimes — not all of
  them. Where it does not hold you still get lazy *delivery*, which lets you
  overlap embedding and upsert work with iteration; you do not get a lower
  memory ceiling.

  
**Python**

    Genuinely incremental for **PDF and XLSX**: chunks are produced as the
    document is read, and breaking early skips the rest of the parse. Every
    other format — CSV included — is chunked in full by the binding and drained
    through the same iterator, so peak memory matches `get_chunks`.
  

  
**JavaScript**

    **Never** incremental. `streamChunks` awaits `getChunks` and then yields, so
    the whole document and the whole chunk list exist before the first value.
    The WASM boundary is a synchronous full parse; this is async-iteration
    ergonomics.
  

  
**Rust**

    Genuinely incremental for **CSV, XLSX and PDF** (PDF in `"default"` mode —
    the other modes rank heading sizes document-wide, so the parse completes
    behind the iterator). The prose formats' `stream` calls `chunk` eagerly and
    iterates the result.
  

  [Streaming](/docs/streaming) has the per-format, per-runtime matrix.

## Markdown conversion

Convert any supported document to a Markdown string.

```python
from py_chunks import get_markdown

md = get_markdown("report.docx")                       # -> str
md = get_markdown(file_bytes, filename="report.pdf")   # bytes also supported

# get_markdown does NOT accept URLs — a URL string is treated as a path and
# raises FileNotFoundError. Download it first, then pass the bytes.
```

```ts
import { getMarkdown } from "js-chunks";

const md = await getMarkdown("./report.docx");   // -> string

// With images: { markdown, images: { name, data }[] }
const withImages = await getMarkdown("./report.docx", { listImages: true });
```

```rust
use chunks_rs::{get_markdown, get_markdown_from_bytes};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let md = get_markdown("report.docx")?;                       // -> String
    let data = std::fs::read("report.docx")?;
    let md2 = get_markdown_from_bytes(&data, "report.docx")?;    // no filesystem
    println!("{} {}", md.len(), md2.len());
    Ok(())
}
```

## Image extraction

Ask for embedded images alongside the chunks.

```python
from py_chunks import get_chunks

result = get_chunks("deck.pptx", list_images=True)   # -> ChunksResult
result.chunks   # text chunks + image chunks (content_type="image")
result.images   # {"<hash>.jpeg": b"..."} — name matches the ![](name) reference
```

```ts
import { getChunks } from "js-chunks";

const { chunks, images } = await getChunks("./deck.pptx", { listImages: true });
// images: { name: string; data: Uint8Array }[]  — name matches the ![](name) ref
```

```rust
use chunks_rs::formats::pptx;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Returns (chunks, images); images are (name, bytes) pairs
    let (chunks, images) = pptx::chunk_with_images("deck.pptx", "default", 3, 1, 3, 15)?;
    println!("{} {}", chunks.len(), images.len());
    Ok(())
}
```

Image extraction is supported for DOC, DOCX (family), PPT, PPTX (family), XLSX
(family, including ODS/XLSB — not XLS), HTML/HTM, PDF, EPUB, IPYNB, EML/MBOX,
**MSG**, and ODT/ODP — see [Supported Formats](/docs/supported-formats). Asking
a format that has no image support returns an empty image collection rather than
an error.

## When it fails

Failures are typed, so you can tell "this file is not supported" from "this file
is broken" without string matching.

```python
from py_chunks import get_chunks

try:
    chunks = get_chunks("report.xyz")
except FileNotFoundError as e:
    ...   # File not found: report.xyz
except ValueError as e:
    ...   # unsupported extension, bad mode, bad window/overlap
except RuntimeError as e:
    ...   # the document could not be parsed

# Real messages:
# ValueError: Unsupported file type '.xyz'. Supported: .csv, .doc, .docm, ...
# ValueError: mode must be one of ['default', 'page_aware', 'section',
#             'semantic', 'sentence', 'sliding_window', 'structural'] for MD,
#             got: 'nope'
# ValueError: overlap must be less than window_size
# ValueError: filename is required when source is bytes
```

```ts
import { getChunks, ChunkError } from "js-chunks";

try {
  const chunks = await getChunks("./report.xyz");
} catch (e) {
  if (e instanceof ChunkError) {
    // kind: "unsupported" | "invalid-arg" | "parse" | "io" | "unknown"
    console.error(e.kind, e.message);
  } else {
    throw e;   // defensive: every js-chunks failure is a ChunkError
  }
}

// unsupported   Unsupported file type '.xyz'
// invalid-arg   A filename is required for byte sources (pass opts.filename
//               or a named Blob) so the engine can route by extension.
// invalid-arg   overlap must be less than window_size
// io            ENOENT: no such file or directory, open './report.xyz'
```

```rust
use chunks_rs::{get_chunks, ChunkError};

fn main() {
    match get_chunks("report.xyz", "default", 3, 1, 3, 15) {
        Ok(chunks) => println!("{} chunks", chunks.len()),
        Err(ChunkError::Unsupported(m)) => eprintln!("unsupported: {m}"),
        Err(ChunkError::InvalidArg(m)) => eprintln!("bad argument: {m}"),
        Err(ChunkError::Parse(m)) => eprintln!("parse failed: {m}"),
        Err(ChunkError::Io(e)) => eprintln!("io: {e}"),
        // ChunkError is #[non_exhaustive] — a wildcard arm is required.
        Err(e) => eprintln!("other: {e}"),
    }
}

// unsupported: Unsupported file type '.xyz'
```

**Python**

Python maps engine failures onto built-in exceptions: `FileNotFoundError` for a
missing path, `ValueError` for an unsupported extension / bad mode / bad
window-overlap combination / missing filename on bytes, `TypeError` for a source
type it cannot use, and `RuntimeError` when a document cannot be parsed.

**JavaScript**

`ChunkError` carries a `kind` of `unsupported`, `invalid-arg`, `parse`, `io` or
`unknown`, and is a real `Error` subclass. Everything the **engine** rejects
arrives that way, and so does every host-side failure the wrapper raises —
including a path that does not exist, which surfaces as `kind: "io"` carrying
Node's own `ENOENT` message. Keeping a non-`ChunkError` branch in the catch is
still good practice for genuinely unexpected errors.

**Rust**

`ChunkError` is `#[non_exhaustive]`, so a `match` needs a wildcard arm. Every
dispatch entry point runs the parse behind a `catch_unwind` boundary: a panic in
a third-party parser surfaces as `ChunkError::Parse`, never as an unwind into
your code.

[Error Handling](/docs/error-handling) has the complete table.

## Language notes

**Python**

- **The source-agnostic surface is 13 functions.** `get_chunks` and
  `stream_chunks` sniff the source type; the five `*_from_path` /
  `*_from_bytes` / `*_from_fileobj` / `*_from_upload` /
  `*_from_s3_presigned_url` variants of each are exported too if you would
  rather be explicit.
- **Uploads and URLs are first-class.** `get_chunks_from_upload(file)` handles a
  FastAPI / Starlette / Django upload (read `upload_file.file` under the hood,
  so it is safe in a sync handler), and `get_chunks("https://…")` downloads and
  chunks in memory.
- **`get_markdown` is narrower than `get_chunks`**: no URLs, no upload objects.
  See [Input Sources](/docs/input-sources#urls-and-pre-signed-links).
- **Bytes never touch disk** (since 0.6.1) — but *streaming* from bytes writes a
  temp file, because the engine's streaming surface is path-based.
- **Options are keyword-only** after the source, and chunks are plain dicts:
  `chunk["content_type"]`.

**JavaScript**

- **One polymorphic function per operation.** `getChunks` accepts a path
  (Node/Bun only), a `Uint8Array`, an `ArrayBuffer`, a Node `Buffer`, a `Blob`
  or a `File`.
- **Byte sources need a filename.** `opts.filename` is required for anything
  that does not carry its own name — a `File` does, a bare `Blob` does not.
- **`streamChunks` is ergonomics, not bounded memory.** It resolves the full
  parse first and then yields; use it to overlap downstream work, not to survive
  a huge file.
- **Deno cannot use path sources** — pass bytes with `opts.filename`.
- **There is no `delimiter` or `encoding` option.** CSV knobs are Python and
  Rust only.
- **Markdown you already have** can be chunked directly with
  `chunkPdfMarkdown` / `chunkPdfMarkdownWithImages` / `normalizePdfMarkdown`.

**Rust**

- **Options are positional** on the six dispatch functions:
  `get_chunks(path, mode, window_size, overlap, sentences_per_chunk,
  paragraphs_per_page)`. For the format-specific knobs (`rows_per_chunk`,
  `delimiter`, `encoding`, `include_headers`, …) use `ChunkOptions` with a
  format module's `chunk_with_options(path, &opts)`, or that module's positional
  `chunk`.
- **Image extraction at the crate root is bytes-only.** There is no
  `get_chunks_with_images(path, …)` — read the file and call
  `get_chunks_with_images_from_bytes`, or use
  `formats::<fmt>::chunk_with_images(path, …)` (available for 12 formats).
- **Streaming is per-format.** `formats::<fmt>::stream(path, …)` for 17 formats;
  only `pdf` and `xlsx` also offer `stream_from_bytes`. Nothing streams at the
  dispatch layer.
- **The crate is `rs-chunks`; the import is `chunks_rs`.**

## Next steps

- [Input Sources](/docs/input-sources) — paths, bytes, uploads, URLs, Blobs
- [Chunking Modes](/docs/chunking-modes) — pick the right strategy
- [Streaming](/docs/streaming) — what streams incrementally, and what only looks like it
- [Framework Integration](/docs/framework-integration) — FastAPI, Express, Axum, and more
- [API Reference](/docs/api-reference) — full signatures per language
