> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hollerith.monarcha.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Python SDK reference

> Python client reference

```python theme={null}
from hollerith import Hollerith

clf = Hollerith()                      # reads HOLLERITH_API_KEY + HOLLERITH_BASE_URL
clf.fit(train, target="species")
clf.predict(test)                      # ['setosa', 'setosa', 'versicolor']
```

## Install

```sh theme={null}
pip install https://hollerith.monarcha.ai/sdk/hollerith-<version>-py3-none-any.whl
```

Hollerith is not on PyPI. It is served as a wheel from your own deployment's origin under
`/sdk/`, and the console Quickstart tab shows the exact current URL.

The package requires Python 3.11 or 3.12. It depends on `httpx>=0.27`, `numpy>=1.26` and
`pandas>=2.2`.

## Configure

```sh theme={null}
export HOLLERITH_API_KEY="hk_live_..."
export HOLLERITH_BASE_URL="https://hollerith.monarcha.ai"
```

Those are the only two environment variables the SDK reads. Both are required unless you pass
`api_key=` and `base_url=` to the constructor.

```python theme={null}
import hollerith
hollerith.__version__          # the version of the wheel you installed
```

`__version__` is derived from the git tag the wheel was built at, so it names the commit you
installed. In an unbuilt source tree with no installed metadata it reads `'0+unknown'`.

## Hollerith

```python theme={null}
Hollerith(
    api_key: str | None = None,
    *,
    base_url: str | None = None,
    transport: Transport | None = None,
    server_context: bool = True,
    sleep: Callable[[float], None] = time.sleep,
    monotonic: Callable[[], float] = time.monotonic,
)
```

| Parameter        | Type                      | Default          | What it is for                                                                                                     |
| ---------------- | ------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------ |
| `api_key`        | `str \| None`             | `None`           | Your key. Falls back to `HOLLERITH_API_KEY`. Surrounding whitespace is stripped.                                   |
| `base_url`       | `str \| None`             | `None`           | The origin the SDK talks to. Falls back to `HOLLERITH_BASE_URL`. Trailing slashes are stripped.                    |
| `transport`      | `Transport \| None`       | `None`           | A substitute control-plane client. When you pass one, `base_url` is ignored entirely.                              |
| `server_context` | `bool`                    | `True`           | Whether `fit` prepares a reusable server-side context. `False` makes every `predict` re-upload the training table. |
| `sleep`          | `Callable[[float], None]` | `time.sleep`     | The poll loop's sleep function. Injected by tests.                                                                 |
| `monotonic`      | `Callable[[], float]`     | `time.monotonic` | The poll loop's clock. Injected by tests.                                                                          |

Only `api_key` is positional. Everything else is keyword-only.

**`base_url` has no default.** If neither the argument nor `HOLLERITH_BASE_URL` is set, the
constructor raises a plain `ValueError` — not a `HollerithError`, so an `except HollerithError`
block will not catch it.

The key is resolved first. A missing key therefore raises
`AuthenticationError(code="missing_api_key")` before the `base_url` check ever runs.

## Shared keyword arguments

These five recur across most methods, with two exceptions to hold on to. `fit` does not take
`on_progress`, and `FitHandle.wait()` retries a warming worker only when you pass `on_warming`.

| Argument          | Type                                        | Default | Behaviour                                                                                                                            |
| ----------------- | ------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `idempotency_key` | `str \| None`                               | `None`  | Sent with the submit. A replay returns the same job; reuse with a different payload raises `ConflictError` (`idempotency_conflict`). |
| `poll_interval`   | `float`                                     | `1.0`   | Seconds between status polls.                                                                                                        |
| `timeout`         | `float \| None`                             | `900.0` | Wall-clock deadline in seconds. Raises `TimeoutError`. `None` polls without a deadline.                                              |
| `on_progress`     | `Callable[[PredictionJob], None]`           | `None`  | Called with the latest job view on submit and on every poll.                                                                         |
| `on_warming`      | `Callable[[ServiceUnavailableError], None]` | `None`  | Called each time a poll hits a retryable outage, typically a cold worker.                                                            |

`on_warming` is called with one argument — the `ServiceUnavailableError` that was swallowed.
A hook written as `lambda: print("warming")` raises `TypeError` on the first cold start.

```python theme={null}
clf.predict(test, on_warming=lambda err: print(err.code))   # 'worker_unavailable'
```

`on_progress` is accepted by `predict`, `predict_proba`, `evaluate`, `forecast` and both
handles' `wait()`. It is not accepted by `fit`, which passes only `on_warming` down to the
fit handle.

## fit

```python theme={null}
fit(
    data,
    y=None,
    *,
    target: str | None = None,
    task: Task | None = None,
    evaluate: bool = False,
    wait: bool = True,
    idempotency_key: str | None = None,
    poll_interval: float = 1.0,
    timeout: float | None = 900.0,
    on_warming: WarmingHook | None = None,
) -> "Hollerith | FitHandle"
```

| Parameter  | Type                                               | Default | What it is for                                                                                                                                      |
| ---------- | -------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data`     | DataFrame or array-like                            | —       | A labeled frame when you pass `target=`, otherwise the feature matrix `X`. At most 1,000,000 rows and 2,000 columns, and at most 100,000,000 cells. |
| `y`        | Series, single-column DataFrame, or 1-D array-like | `None`  | Labels, when you are not passing `target=`. Must have the same length as `data`.                                                                    |
| `target`   | `str \| None`                                      | `None`  | The label column in `data`. Mutually exclusive with `y`.                                                                                            |
| `task`     | `"classification" \| "regression" \| None`         | `None`  | Overrides the inferred task.                                                                                                                        |
| `evaluate` | `bool`                                             | `False` | Also run an evaluation and set `evaluation_`. This is a separate billed job.                                                                        |
| `wait`     | `bool`                                             | `True`  | Block until the context is ready. `False` returns a `FitHandle` immediately.                                                                        |

Pass either `fit(df, target="col")` or `fit(X, y)`. Passing both raises `ValueError`, and
passing neither raises `ValueError`.

**Returns** `self` when `wait=True`, so you can chain. Returns a `FitHandle` when
`wait=False`.

* **The task is inferred from the target** unless you override it. A non-numeric or boolean
  target is classification, and so is an integral numeric target with 20 or fewer distinct
  values.
* **Classification reads at most 160 distinct labels.** A 161st raises `ValidationError`
  (`dataset_too_large`) client-side, before anything is uploaded.
* **The 5,000,000,000-byte compressed upload ceiling is not checked locally.** An oversize
  payload is serialized and gzipped, then rejected at the presign step with
  `payload_too_large`. Nothing reaches storage.

Two things `wait=False` changes:

* **No context is bound.** `fitted_context_id_` stays `None` and the next `predict` re-uploads
  the whole training table inline.
* **`evaluate=True` is silently dropped.** The early return happens before the evaluation is
  started. There is no error and no warning.

**Raises** `ValidationError` for a missing target column (`missing_target_column`), an
all-null target (`unsupported_task`), no feature columns (`schema_mismatch`), an empty frame
(`malformed_csv`) or any limit breach (`dataset_too_large`). It raises `ValueError` for the
calling-convention mistakes above, `TimeoutError` on deadline, and the typed `HollerithError`
for the code the fit job failed with.

## predict

```python theme={null}
predict(
    data,
    *,
    wait: bool = True,
    quantiles: Sequence[float] | None = None,
    idempotency_key: str | None = None,
    poll_interval: float = 1.0,
    timeout: float | None = 900.0,
    on_progress: ProgressHook | None = None,
    on_warming: WarmingHook | None = None,
) -> Any
```

| Parameter   | Type                      | Default | What it is for                                                           |
| ----------- | ------------------------- | ------- | ------------------------------------------------------------------------ |
| `data`      | DataFrame or array-like   | —       | The rows to score. At most 200,000 rows per call, checked before upload. |
| `wait`      | `bool`                    | `True`  | Block to completion. `False` returns a `PredictionHandle`.               |
| `quantiles` | `Sequence[float] \| None` | `None`  | Probability levels for prediction intervals. Regression only.            |

A DataFrame must carry every column named in `feature_names_in_`; extra columns are dropped
and the rest are reordered to match. An array-like is assumed to be in the fitted column
order.

**Returns a plain Python `list`** when `wait=True` and `quantiles` is `None`. Not an ndarray,
not a Series — `preds[0]` is the label or number for the first row.

```python theme={null}
preds = clf.predict(test)
preds                      # ['setosa', 'setosa', 'versicolor']
```

With `quantiles=`, it returns a `pandas.DataFrame` with a `prediction` column holding the mean,
plus one column per level the engine scored.

```python theme={null}
frame = clf.predict(test, quantiles=[0.1, 0.5, 0.9])
list(frame.columns)        # ['prediction', '0.1', '0.5', '0.9']
frame["0.1"]               # the 10th-percentile column
```

**The quantile column labels are strings.** `frame[0.1]` raises `KeyError`; you have to index
with `"0.1"`.

With `wait=False` you get a `PredictionHandle` in every case, including with `quantiles=`. The
handle's result carries `.quantiles` and `.quantile_levels` as raw lists, and no DataFrame is
ever assembled for you.

Raised before anything is submitted:

* `RuntimeError("call fit() or Hollerith.from_fitted_context() before predict()")` when nothing
  is fitted.
* `ValueError` when `quantiles` is passed for a classification task.
* `ValidationError` for rows missing a fitted feature column or of the wrong width
  (`schema_mismatch`), an empty frame (`malformed_csv`), or more than 200,000 rows
  (`dataset_too_large`).

Raised while waiting:

* `TimeoutError` on the deadline.
* `NotFoundError` (`training_ref_expired`) if the job was purged before its result was read.
* The typed `HollerithError` carried by a failed job.

## predict\_proba

```python theme={null}
predict_proba(
    data,
    *,
    idempotency_key: str | None = None,
    poll_interval: float = 1.0,
    timeout: float | None = 900.0,
    on_progress: ProgressHook | None = None,
    on_warming: WarmingHook | None = None,
) -> np.ndarray
```

| Parameter | Type                    | Default | What it is for                                    |
| --------- | ----------------------- | ------- | ------------------------------------------------- |
| `data`    | DataFrame or array-like | —       | The rows to score. At most 200,000 rows per call. |

**Returns** a `numpy.ndarray` of shape `(n_rows, n_classes)` and dtype `float64`. Columns are
ordered by `classes_`, so `proba[:, clf.classes_.index("fraud")]` is one label's column.

This method always blocks. There is no `wait=False`.

**It overwrites `classes_`** with the engine's ordering before returning, so the array and the
attribute stay aligned. Read `classes_` after the call, not before.

**Raises** `ValueError("predict_proba is only available for classification")` for a regression
task, plus everything `predict` raises.

## evaluate

```python theme={null}
evaluate(
    *,
    idempotency_key: str | None = None,
    poll_interval: float = 1.0,
    timeout: float | None = 900.0,
    on_progress: ProgressHook | None = None,
    on_warming: WarmingHook | None = None,
) -> EvaluationResult
```

Takes no positional arguments. It re-sends the labeled training table, label column included,
and the service splits it and scores it.

The whole table is re-checked before the upload, against 1,000,000 rows, 2,000 columns,
100,000,000 cells and 160 classes. A breach raises `ValidationError` (`dataset_too_large`).

**Returns** an `EvaluationResult`, a frozen dataclass, and also stores it on `evaluation_`.

| Field            | Type          | What it holds                                                                                    |
| ---------------- | ------------- | ------------------------------------------------------------------------------------------------ |
| `metric`         | `str`         | `"accuracy"` for classification, `"RMSE"` for regression.                                        |
| `value`          | `float`       | The score.                                                                                       |
| `method`         | `str`         | `"kfold"` at 10,000 rows or fewer, `"holdout"` above that.                                       |
| `rows`           | `int`         | Rows the evaluation covered.                                                                     |
| `folds`          | `int \| None` | Fold count for k-fold, starting at 5 and trimmed toward 2 to fit a cost cap. `None` for holdout. |
| `engine_version` | `str \| None` | The engine the score is keyed to, read off the finished job.                                     |

```python theme={null}
print(clf.evaluate())
# Evaluation(accuracy=0.9733, method=kfold, folds=5, rows=150)
```

**Raises** `RuntimeError("call fit() before predict()")` if `fit` was never called on this
client. A client built by `from_fitted_context` has no training table in memory, so it can
never evaluate.

It otherwise raises the same set as `predict`: `ValidationError` on a limit breach,
`TimeoutError` on deadline, `NotFoundError` on a purged job, and the typed error a failed job
carried.

## submit

```python theme={null}
submit(
    data,
    *,
    quantiles: Sequence[float] | None = None,
    idempotency_key: str | None = None,
) -> PredictionHandle
```

Uploads the payload, enqueues one prediction job, and returns its handle without polling. This
is what `predict(wait=False)` calls.

At most 200,000 scored rows, checked client-side before the upload. A breach raises
`ValidationError` (`dataset_too_large`).

**Returns** a `PredictionHandle`. It raises the same submit-time errors as `predict` —
`RuntimeError`, `ValueError` for classification quantiles, and `ValidationError` for a schema
or limit problem.

## forecast

```python theme={null}
forecast(
    context: pd.DataFrame,
    *,
    prediction_length: int | None = None,
    future: pd.DataFrame | None = None,
    quantiles: Sequence[float] | None = None,
    timestamp: str = "timestamp",
    target: str = "target",
    item_id: str | None = None,
    idempotency_key: str | None = None,
    poll_interval: float = 1.0,
    timeout: float | None = 900.0,
    on_progress: ProgressHook | None = None,
    on_warming: WarmingHook | None = None,
) -> pd.DataFrame
```

| Parameter           | Type                      | Default       | What it is for                                                                             |
| ------------------- | ------------------------- | ------------- | ------------------------------------------------------------------------------------------ |
| `context`           | `pd.DataFrame`            | —             | The history. At most 1,000,000 rows, and rows × (covariates + 1) at most 100,000,000.      |
| `prediction_length` | `int \| None`             | `None`        | Steps to forecast past the end of `context`. Must be positive.                             |
| `future`            | `pd.DataFrame \| None`    | `None`        | Horizon timestamps plus known covariates. Must carry every covariate column `context` has. |
| `quantiles`         | `Sequence[float] \| None` | `None`        | Probability levels, each strictly between 0 and 1. Defaults to `(0.1, 0.5, 0.9)`.          |
| `timestamp`         | `str`                     | `"timestamp"` | The time column's name in your frames.                                                     |
| `target`            | `str`                     | `"target"`    | The column to forecast.                                                                    |
| `item_id`           | `str \| None`             | `None`        | The series column, for many series in one call.                                            |

Pass exactly one of `prediction_length` or `future`. Every column that is not the timestamp,
target or series column is treated as a covariate.

**Returns** a `pandas.DataFrame` with a `mean` column plus one string-labelled column per
quantile level. A single series is indexed by `timestamp`; with `item_id=` the index is a
MultiIndex of `(item_id, timestamp)`, and the timestamp level is datetime-typed either way.

* **Output rows are capped at 200,000.** They are counted as `prediction_length × series
  count`, or as `len(future)` when you pass a future frame.
* **Covariate columns are capped at 2,000.**
* **No `fit` is involved.** `forecast` never uses a fitted context, so nothing about fit-once,
  predict-many applies to it.

It raises `TypeError` if `context` or `future` is not a DataFrame, and `ValueError` for a
horizon that is not exactly one of the two options, a non-positive `prediction_length`, a
quantile outside `(0, 1)`, a missing timestamp or target column, or a `future` frame missing a
covariate.

## from\_fitted\_context

```python theme={null}
Hollerith.from_fitted_context(
    context_id: str,
    *,
    api_key: str | None = None,
    base_url: str | None = None,
    transport: Transport | None = None,
    sleep: Callable[[float], None] = time.sleep,
    monotonic: Callable[[], float] = time.monotonic,
) -> "Hollerith"
```

| Parameter    | Type  | Default | What it is for                                             |
| ------------ | ----- | ------- | ---------------------------------------------------------- |
| `context_id` | `str` | —       | An id captured from a previous fit's `fitted_context_id_`. |

The other arguments are the constructor's and mean the same thing. There is no
`server_context` argument here, because a resumed client is context-backed by definition.

This is a network call. It fetches the context, checks its status, and binds its schema onto
the new client.

**Returns** a `Hollerith` with `feature_names_in_`, `n_features_in_`, `n_rows_in_`,
`target_name_`, `task_`, `fitted_context_id_` and `fitted_context_expires_at_` already set.

* **Raises `NotFoundError`** if the context is not ready. The code is
  `fitted_context_expired` when its status is `expired` or `deleted`, and
  `fitted_context_not_found` for any other non-ready status.
* **Contexts live for 7 days.** After that, fit again — there is no way to extend one.

```python theme={null}
clf = Hollerith.from_fitted_context(saved_id)
clf.predict(batch)          # works
clf.evaluate()              # RuntimeError: call fit() before predict()
clf.classes_                # AttributeError, until predict_proba runs
```

## fitted

```python theme={null}
fitted -> bool
```

A read-only property. It is `True` once `fit` has staged a training table or a context is
bound, and `False` on a fresh client.

## Instance attributes

sklearn convention: a trailing underscore means the attribute appears as a result of fitting,
not at construction. Reading one before it is set raises `AttributeError`.

| Attribute                    | Type                               | Set when                                                                                                                                                                       |
| ---------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `api_key`                    | `str`                              | At construction, always.                                                                                                                                                       |
| `fitted_context_id_`         | `str \| None`                      | `None` at construction. Set by `fit(wait=True)` once the context is ready, and by `from_fitted_context`. Stays `None` under `server_context=False` or after `fit(wait=False)`. |
| `fitted_context_expires_at_` | `str \| None`                      | Alongside `fitted_context_id_`. An ISO-8601 timestamp.                                                                                                                         |
| `feature_names_in_`          | `list[str]`                        | By `fit`, from your frame's columns minus the target. By `from_fitted_context`, from the context's stored column list.                                                         |
| `n_features_in_`             | `int`                              | With `feature_names_in_`.                                                                                                                                                      |
| `n_rows_in_`                 | `int`                              | With `feature_names_in_`. Rows in the frame you fit on, or the context's training row count.                                                                                   |
| `target_name_`               | `str`                              | With `feature_names_in_`. For `fit(X, y)` with an unnamed `y`, it is `"target"`.                                                                                               |
| `task_`                      | `"classification" \| "regression"` | With `feature_names_in_`.                                                                                                                                                      |
| `classes_`                   | `list`                             | By `fit` on a classification task only, sorted, from the training labels. Never set for regression.                                                                            |
| `evaluation_`                | `EvaluationResult`                 | Only by `evaluate()`, directly or via `fit(evaluate=True, wait=True)`.                                                                                                         |

Two of these surprise people.

* **A resumed client has no `classes_`.** `from_fitted_context` binds the schema, which does
  not carry the label set. The attribute appears the first time `predict_proba` runs.
* **`predict_proba` overwrites `classes_`.** It replaces the sorted list from `fit` with the
  engine's ordering, so the probability columns and the labels stay aligned.

`fitted_context_id_` is a raw document id. It carries no `ctx_` prefix and you should treat it
as opaque.

Use `hasattr(clf, "evaluation_")` to test for an evaluation. It is `False` after a plain `fit`,
and `False` after `fit(evaluate=True, wait=False)`.

## PredictionHandle

Returned by `submit()` and by `predict(wait=False)`.

| Member      | Type               | What it is                                                                 |
| ----------- | ------------------ | -------------------------------------------------------------------------- |
| `id`        | `str`              | The job id.                                                                |
| `job`       | `PredictionJob`    | The most recent job view. No network call.                                 |
| `status`    | `str`              | The cached status: `queued`, `running`, `succeeded`, `failed` or `purged`. |
| `done`      | `bool`             | Whether the cached status is terminal.                                     |
| `refresh()` | `-> PredictionJob` | One round-trip; updates and returns the job view.                          |

```python theme={null}
wait(
    *,
    poll_interval: float = 1.0,
    timeout: float | None = 900.0,
    on_progress: ProgressHook | None = None,
    on_warming: WarmingHook | None = None,
) -> PredictionResult
```

`wait()` blocks to a terminal state and returns a `PredictionResult`.

## PredictionResult

What a finished prediction job scored. It carries `predictions`, `task`, `classes`,
`probabilities`, `quantiles` and `quantile_levels`, and fields the task did not produce are
`None`.

`predictions` is row-aligned with the rows you submitted. `classes` is the label order the
engine used, which is what makes `probabilities` interpretable.

## PredictionJob

The status view you get from `handle.job`, `handle.refresh()` and every `on_progress` call. It
is a frozen dataclass of hashes and counts, never data.

It carries `id`, `status`, `task`, `train_rows`, `output_rows`, `cols`, `target_column`,
`engine_version`, `created_at`, `duration_ms`, `input_hash` and `error`. It also exposes
`rows`, the sum of `train_rows` and `output_rows`.

## FitHandle

Returned by `fit(wait=False)`.

| Member      | Type               | What it is                                            |
| ----------- | ------------------ | ----------------------------------------------------- |
| `id`        | `str`              | The fitted context id.                                |
| `context`   | `FittedContext`    | The most recent context view. No network call.        |
| `refresh()` | `-> FittedContext` | One round-trip; updates and returns the context view. |

```python theme={null}
wait(
    *,
    poll_interval: float = 1.0,
    timeout: float | None = 900.0,
    on_warming: WarmingHook | None = None,
) -> FittedContext
```

`FitHandle.wait()` takes no `on_progress`. It returns the `FittedContext` once its status is
`ready`, and raises the context's typed error otherwise.

* **Waiting does not bind the context to your client.** To predict against a context you
  polled yourself, pass its id to `from_fitted_context`.
* **`FitHandle.wait()` only retries a warming worker when you pass `on_warming`.** Without the
  hook, a retryable `ServiceUnavailableError` propagates instead of being waited out.
  `PredictionHandle.wait()` retries either way.

## FittedContext

The server-side artifact a fit produces, read through `FitHandle.context` or
`FitHandle.refresh()`.

It carries `id`, `status`, `task`, `train_rows`, `cols`, `target_column`, `feature_columns`,
`engine_version`, `artifact_schema_version`, `train_hash`, `artifact_hash`, `created_at`,
`expires_at`, `last_used_at`, `created_by_job_id` and `error`, plus a `done` property.

## read\_csv and read\_csv\_text

```python theme={null}
from hollerith import read_csv, read_csv_text

train = read_csv("train.csv")
rows  = read_csv_text("a,b\n1,2\n")
```

| Function        | Signature                                  | Accepts                                                    |
| --------------- | ------------------------------------------ | ---------------------------------------------------------- |
| `read_csv`      | `read_csv(source) -> pd.DataFrame`         | A path string, a `Path`, or an open text or binary buffer. |
| `read_csv_text` | `read_csv_text(text: str) -> pd.DataFrame` | CSV text already in memory.                                |

The reader is forgiving about form and strict about structure. It sniffs the delimiter from
the header line among comma, tab, semicolon and pipe, with comma winning ties.

* **Encoding** is resolved from the byte-order mark first, which pins UTF-8 or UTF-16
  unambiguously. With no mark it tries UTF-8, then Windows-1252, and stops there rather than
  silencing a real encoding problem.
* **Blank lines** are dropped. A row of empty fields is kept and width-checked, and line
  numbers stay correct across quoted multiline fields.

**Every column comes back as Python `str`.** No dtype inference is performed, which means a
numeric target read this way infers as classification. Cast the columns you need, or pass
`task="regression"` to `fit`.

```python theme={null}
df = read_csv("houses.csv")
df["price"] = df["price"].astype(float)
clf.fit(df, target="price")
```

## CSV parse errors

Every failure raises `ValidationError` with code `malformed_csv`, and the `problem` field names
the 1-based source line or the column position at fault. Dataset contents never appear in the
message — only structure.

* **Ragged row** — `Line 3 has 2 fields but the header defines 3 columns.`
* **Duplicate header name** — names the repeated column.
* **Blank header name** — names the 1-based column position.
* **Empty input, or a header with no data rows.**
* **Undecodable bytes** — names the byte position that failed under UTF-8.
* **Unreadable path** — a missing file or a permissions problem, not an `OSError`.

## Exceptions

```python theme={null}
from hollerith import HollerithError, ValidationError

try:
    clf.predict(test)
except ValidationError as err:
    print(err.code, err.fix)
except HollerithError as err:
    print(err.code, err.retryable)
```

There are nine classes: `HollerithError` and eight subclasses, one per error category. Catch a
subclass to handle one category, or `HollerithError` to handle them all.

| Class                     | Category      | What it means                                                       |
| ------------------------- | ------------- | ------------------------------------------------------------------- |
| `AuthenticationError`     | `auth`        | The key is missing, invalid or revoked.                             |
| `PermissionDeniedError`   | `permission`  | Authenticated, but not entitled to this resource.                   |
| `QuotaExceededError`      | `quota`       | A daily quota or rate limit was hit.                                |
| `ValidationError`         | `validation`  | The request or dataset is malformed or too large.                   |
| `NotFoundError`           | `not_found`   | The job, staged data or fitted context is gone.                     |
| `ConflictError`           | `conflict`    | An idempotency key collided with a different payload.               |
| `ServiceUnavailableError` | `unavailable` | The worker is cold or briefly down.                                 |
| `ServerError`             | `server`      | An unexpected internal failure.                                     |
| `HollerithError`          | —             | Base class, and the fallback for a code the SDK does not recognize. |

## Exception attributes

Every instance carries the same seven attributes.

| Attribute    | Type          | What it holds                                                  |
| ------------ | ------------- | -------------------------------------------------------------- |
| `problem`    | `str`         | What went wrong. Also the exception's message.                 |
| `code`       | `str`         | The stable identifier. The only field to branch on.            |
| `cause`      | `str`         | Why it happened. Rewritten per instance by the service.        |
| `fix`        | `str`         | What to do about it.                                           |
| `doc_url`    | `str`         | The documentation anchor for this code.                        |
| `retryable`  | `bool`        | `True` for four codes only.                                    |
| `request_id` | `str \| None` | The server-side correlation id, when the response carried one. |

`str(err)` renders the problem followed by indented `Cause`, `Fix`, `Docs` and `Request` lines,
so printing a caught error gives a readable report.

The category selects the class; the code is what you branch on. The full catalogue of codes,
their HTTP statuses and which four are retryable is in [Errors](/reference/errors).

* **Three exceptions are not `HollerithError` at all.** `ValueError` covers calling mistakes,
  `TypeError` a wrongly typed forecast frame, and `TimeoutError` a lapsed deadline.
* **There is no backoff-retry layer.** The SDK retries a retryable `ServiceUnavailableError`
  inside the poll loop at a fixed `poll_interval`, and nothing else.

## Also exported

The names above are what most code touches. `hollerith.__all__` carries more, and these are
supported.

* **Ingest and inference helpers** — `infer_task`, `prepare_training_data`, `TrainingData`,
  `Task`.
* **Request dataclasses** — `PredictionRequest`, `FitRequest`, `ContextPredictionRequest`,
  `EvaluationRequest`, `ForecastRequest`. The views they return are documented above.
* **Transport seam** — `Transport`, the protocol you implement to substitute one, and
  `HttpTransport`, the HTTPS implementation.
* **Config helpers** — `resolve_api_key`, `API_KEY_ENV_VAR`, `BASE_URL_ENV_VAR`.
* **Error helpers** — `error_from_payload` builds the typed exception for a wire envelope, and
  `raise_for_payload` raises it.

## Checklist

* Set `HOLLERITH_API_KEY` and `HOLLERITH_BASE_URL`, or pass both to the constructor.
* Check `task_` after `fit` if your target is numeric and low-cardinality.
* Index quantile columns with strings: `frame["0.5"]`, never `frame[0.5]`.
* Read `classes_` after `predict_proba`, not before.
* Give `on_warming` one parameter.
* Persist `fitted_context_id_` if you want to resume, and re-fit after 7 days.

## Next

* [Quickstart](/quickstart) — first prediction in about a minute
* [The fitted context](/concepts/fitted-context) — fit once, predict many
* [Errors](/reference/errors) — every code, status and fix
* [Limits and quotas](/reference/limits) — every ceiling and where it is checked
