---
title: "Classifying documents | Nutrient Python SDK"
canonical_url: "https://www.nutrient.io/guides/python/extraction/classify-document/"
md_url: "https://www.nutrient.io/guides/python/extraction/classify-document.md"
last_updated: "2026-07-10T00:00:00.000Z"
description: "Classify documents using zero-shot classification with Nutrient Python SDK."
---

# Classifying documents

Sorting incoming documents — is this an invoice, a contract, a resume, a bank statement? — is the first step in most document workflows. When you don't have a fixed, pre-trained set of categories, you need *zero-shot* classification: you supply the candidate labels at call time and the model scores the document against exactly those.

This sample shows how to classify a document against your own set of candidate labels using Nutrient Python SDK. Classification combines two signals — the document's *text* and its *visual layout* — and fuses them into a single ranked prediction. Both run locally.

[Download sample](https://www.nutrient.io/downloads/samples/python/classify-document.zip)

## How Nutrient helps

Nutrient Python SDK runs the full zero-shot classification pipeline behind a single method call. The SDK handles:

- Rendering the page to a bitmap at the resolution the image model expects

- Scoring the document text against your candidate labels

- Scoring the page image against your candidate labels

- Fusing the two branches by the weights you choose into one ranked prediction

- Serializing the result to JSON

The result is a predicted label plus the full ranked list of candidates with their probabilities — including the per-branch (text and image) contributions, so you can see *why* a label won.

## Why two branches

Some document types are obvious from their text (a contract's legal language), others from their layout (an invoice's table of line items, an ID card's photo block). Scoring both and fusing them is more robust than either alone. You control the balance with two weights — lean on text, lean on image, or weight them equally (the default).

## Choosing where the text comes from

The text branch needs the document's text. By default it derives the text from the document by running the extraction pipeline selected by [Vision Settings](https://www.nutrient.io/api/python/settings/vision/vision-settings.md#engine) (`AdaptiveOcr` for fast OCR, `Icr` for local models, `VlmEnhancedIcr` for VLM-enhanced); multi-page documents are extracted in full. This is the zero-config path.

To classify text you already have (from your own pipeline, a database, or an email body), set `text` directly — it's scored as-is and extraction is skipped. To run image-only and skip the text extraction cost entirely, set `text_weight = 0`.

Classification requires the vision image classification feature in your license.

## Multi-page documents

When you classify a multi-page document, every page contributes. The image branch scores each page independently; the text branch reads text from every page. Pages with a confident, decisive result count for more, while pages that look like boilerplate are softly silenced, so the most distinctive page drives the prediction.

When the text is derived from the document, the text branch reads the full structure of every page — paragraphs, tables, key-value regions, and form fields — in reading order, not just running paragraphs. This matters for invoices, statements, and forms, where the identifying signal often lives in tabular numbers or filled fields rather than prose.

## Complete implementation

Import the classes used in the sample:

```python

from nutrient_sdk import ClassificationRequest, Document, Vision, NutrientException

```

## Loading the document

Open the document in a [context manager](https://docs.python.org/3/reference/datamodel.html#context-managers) so resources are cleaned up after processing:

```python

def main():
    try:
        with Document.open("input_forms_detection.pdf") as document:

```

## Configuring classification

Build a `ClassificationRequest` carrying your candidate labels (at least two are required), and tune the behavior on the document's classification settings. This example derives the text from the document via the extraction pipeline and weights both branches equally:

```python

            classification = document.settings.document_classification_settings

            # Balance the text and image branches (defaults are 0.5 each).

            classification.text_weight = 0.5
            classification.image_weight = 0.5

            # Required: at least two candidate classes to score against (zero-shot).

            request = ClassificationRequest()
            request.add_candidate_label("Invoice")
            request.add_candidate_label("Contract")
            request.add_candidate_label("Resume")
            request.add_candidate_label("Bank statement")
            request.add_candidate_label("ID card")

```

By default the text is derived from the document by the extraction pipeline at the tier set by [Vision Settings](https://www.nutrient.io/api/python/settings/vision/vision-settings.md#engine) (`AdaptiveOcr` for fast OCR, `Icr` for local models, `VlmEnhancedIcr` for VLM-enhanced). To classify text you already have, set `text` directly — it's scored as-is and extraction is skipped:

```python

            classification.text = "INVOICE  Bill to:...  Amount due: $1,240.00"

```

To run image-only (and skip the text extraction cost entirely), set `text_weight = 0`.

## Describing each class

A bare label like "Invoice" works, but ambiguous or domain-specific classes match better when you describe them. Pass an optional description as the second argument — the label is the short name shown in your results; the description is longer context used only to improve matching. Adding a label that's already on the request replaces its entry, so you can start bare and upgrade to descriptions:

```python

            request.add_candidate_label("Invoice", "An itemized bill listing goods or services and the total amount due")
            request.add_candidate_label("Contract", "A legal agreement between parties stating terms and obligations")
            request.add_candidate_label("Resume", "A summary of a person's work experience, skills, and education")
            request.add_candidate_label("Bank statement", "A periodic summary of account transactions and balances")
            # "ID card" keeps its bare label — descriptions are optional per class.

```

Labels and descriptions are plain strings with no delimiter rules — commas, pipes, and newlines are all fine. The result still reports only the label; the description never appears in the output.

## Choosing the label language

Classification runs fully offline in any language. The text branch picks a multilingual model automatically when the document or the labels aren't English, and a dedicated English model when both are English — there's nothing to configure, the language is detected from the text and the labels. A document that mixes languages is treated as multilingual and uses the multilingual model too.

Predictions are sharpest when the document and its labels share a language, so:

- **Prefer English labels.** English documents with English labels take the dedicated English model (the strongest path), and English labels stay reliable even on non-English documents.

- **When the source text isn't English,** give the labels in the document's language *or* fall back to English labels.

- **An English document with non-English labels is the lowest-confidence pairing** — prefer one of the two options above.

A language mismatch — say an English document with non-English labels — still classifies correctly (the multilingual model is selected automatically), but with lower confidence. Keeping the document and labels in the same language, or using English labels for non-English documents, keeps the prediction sharp.

## Classifying the document

Create a vision instance bound to the document with `Vision.set(document)`, then call `classify(request)`:

```python

            vision = Vision.set(document)
            result_json = vision.classify(request)

```

Write the JSON result to a file for downstream routing:

```python

            with open("output.json", "w") as f:
                f.write(result_json)
    except NutrientException as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    main()

```

## Understanding the output

`classify(request)` returns JSON with a top-level `classification` object:

- **`predictedLabel`** — The highest-ranked label.

- **`confidence`** — The fused probability of the predicted label (0.0 to 1.0).

- **`predictions`** — Every candidate, ranked by fused probability descending. Each entry carries:
  - **`label`** — The candidate label.
  - **`probability`** — The fused probability.
  - **`textProbability`** — The text branch's probability for this label (0 when the text branch didn't run).
  - **`imageProbability`** — The image branch's probability for this label (0 when the image branch didn't run).

Because the per-branch contributions are included, you can tell whether a prediction was driven by the text, the layout, or both — useful for debugging and for tuning the weights.

## Text-only classification

When you already have the text — an email body, a database field, your own extraction pipeline — and no file to open, classify it directly with the static `Vision.classify_text(request, text)`. It takes the same `ClassificationRequest` as `classify()` — build it with `add_candidate_label` exactly as above. No document is opened, nothing is rendered, and only the text branch runs; it returns the same JSON shape as `classify()`.

When you already have a document open and only want to skip the extraction cost, the equivalent is to set `classification.text` directly and turn off the image branch with `classification.image_weight = 0` (text branch only). To classify on layout alone instead, set `classification.text_weight = 0`. Either way the disabled branch is skipped and the fused probability equals the remaining branch's probability.

## Error handling

Vision API raises `VisionException` (a `NutrientException`) when classification fails.

Common failure scenarios include:

- The document can't be read due to path or permission issues

- The page produces no renderable image

- A classification model is missing or inaccessible, or the feature isn't licensed

- Fewer than two candidate labels were supplied (classification needs an output space to choose from)

In production code:

- Catch `NutrientException`.

- Return a clear error message.

- Log failure details for debugging.

## Conclusion

The workflow for zero-shot document classification is:

1. Open the source document using a [context manager](https://docs.python.org/3/reference/datamodel.html#context-managers) for automatic resource cleanup.

2. Build a `ClassificationRequest` with at least two candidate labels (optionally with descriptions), and tune `text_weight`/`image_weight` (or supply `text` directly) on the classification settings.

3. Create a vision instance with `Vision.set()`.

4. Call `classify(request)` to score the document and export the ranked result as JSON.

5. Write the JSON to a file for routing or downstream processing.

6. Handle `NutrientException` for robust error recovery.

For related image extraction workflows, refer to the [Python SDK guides](https://www.nutrient.io/guides/python.md).

Download [this ready-to-use sample package](https://www.nutrient.io/downloads/samples/python/classify-document.zip) to explore document classification.
---

## Related pages

- [Nutrient Python SDK extraction guides](/guides/python/extraction.md)
- [Applying OCR to a PDF page](/guides/python/extraction/apply-ocr-to-pdf-page.md)
- [Applying OCR to a PDF document](/guides/python/extraction/apply-ocr-to-pdf.md)
- [Generating image descriptions using Claude](/guides/python/extraction/describe-image-with-claude.md)
- [Generating image descriptions using local AI](/guides/python/extraction/describe-image-with-local-ai.md)
- [Generating image descriptions using OpenAI](/guides/python/extraction/describe-image-with-openai.md)
- [Detecting document language](/guides/python/extraction/detect-document-language.md)
- [Extracting data from images using ICR](/guides/python/extraction/extract-data-from-image-icr.md)
- [Extracting data from images using OCR](/guides/python/extraction/extract-data-from-image-ocr.md)
- [Extracting data from images using vision language models](/guides/python/extraction/extract-data-from-image-vlm.md)
- [Extracting data from specific PDF pages](/guides/python/extraction/extract-data-from-specific-pages.md)
- [Extracting form fields from images](/guides/python/extraction/extract-form-fields-from-image.md)
- [Extracting structured data from documents](/guides/python/extraction/extract-structured-data.md)
- [Generating extraction schemas](/guides/python/extraction/generate-extraction-schema.md)
- [Extracting structured JSON data from PDF documents](/guides/python/extraction/json-data-extraction.md)
- [Labeling form fields with a vision language model](/guides/python/extraction/label-form-fields-with-vlm.md)
- [Opening password-protected PDFs](/guides/python/extraction/open-password-protected-pdf.md)
- [Extracting text from PDF documents](/guides/python/extraction/pdf-to-text.md)
- [Reading barcodes with vision extraction](/guides/python/extraction/read-barcodes-with-vision.md)
- [Extracting text from multilingual images](/guides/python/extraction/read-text-from-image-multi-language.md)
- [Extracting text from images](/guides/python/extraction/read-text-from-image.md)
- [Later, in another process — no document needed:](/guides/python/extraction/search-document-text.md)
- [Speeding up first ICR operation by predownloading models](/guides/python/extraction/speed-up-first-icr-by-downloading-requirements.md)
- [Split documents](/guides/python/extraction/split-document.md)

