> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://docs.ampdb.io/api/mcp` to find what you need.

# Labels

Chain data identifies things by their addresses: a token transfer's log says which contract emitted it, not that it was
USDC. Labels add what you know about those addresses as you load data. A label is a CSV file, like a list of tokens
with their symbols, that the SDK joins to each batch of rows on a key column, before a [loader](/sdks/python/loaders)
writes them. The rows that match get the label's columns; the rows that don't are dropped, so a label also filters
the data down to what it lists.

The join happens in your Python process: Amp never sees the label. To join with data that's in Amp, use SQL
[joins](/querying/joins) instead.

## Prerequisites

* The Python SDK, installed (see [Installation](/sdks/python#installation)).
* Amp's query server, which [solo mode](/running-amp/solo-mode) serves at `grpc://localhost:1602`, and the
  `acme/eth_mainnet` dataset from the [Quickstart](/get-started/quickstart).
* A loader. The examples load into a [Delta Lake](/sdks/python/loaders/delta-lake) table in a local directory, which
  needs the `deltalake` package:

  :::code-group
  ```bash [pip]
  pip install deltalake
  ```

  ```bash [uv]
  uv add deltalake
  ```
  :::

## Write a label file

A label file is a CSV file with a header row. One column is the key, the values you join on, like token addresses. Every
other column is added to the rows that match. Save this one as `tokens.csv`:

```csv [tokens.csv]
token_address,symbol,decimals,name
0xdac17f958d2ee523a2206206994597c13d831ec7,USDT,6,Tether USD
0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48,USDC,6,USD Coin
0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2,WETH,18,Wrapped Ether
0x6b175474e89094c44da98b954eedeac495271d0f,DAI,18,Dai Stablecoin
```

The SDK reads every column as text, so `decimals` arrives as the string `'6'`, not a number. Columns whose name contains
`address`, like `token_address`, are the exception: the SDK converts their hexadecimal values, with or without `0x`, to
bytes, the form addresses have in Amp's tables, so they can be joined.

## Load with a label

Register the file under a name with `client.configure_label()`, then pass a `LabelJoinConfig` to `load()`. Its
`label_key_column` is the key column in the label file, and its `stream_key_column` is the column of the query's rows to
match it with. This script loads the token transfers of the Quickstart's blocks, with each token's symbol, decimals, and
name:

```python [load_transfers.py]
from amp import Client
from amp.loaders.types import LabelJoinConfig

client = Client('grpc://localhost:1602')
client.configure_label('tokens', 'tokens.csv')
client.configure_connection('lake', 'deltalake', {'table_path': './token_transfers'})

query = '''
SELECT block_num, tx_hash, log_index, address AS token_address
FROM "acme/eth_mainnet".logs
WHERE topic0 = evm_topic('Transfer(address indexed from, address indexed to, uint256 value)')
'''

result = client.sql(query).load(
    connection='lake',
    destination='token_transfers',
    label_config=LabelJoinConfig(
        label_name='tokens',
        label_key_column='token_address',
        stream_key_column='token_address',
    ),
    read_all=True,
)
print(result)
```

:::terminal
```bash
python load_transfers.py
```

```
⏱️  Label join: 8192 → 1709 rows in 110.44ms (join=110.37ms, 15475 rows/sec) [label=tokens, retained=20.9%]
⏱️  Label join: 8192 → 1346 rows in 0.57ms (join=0.55ms, 2355890 rows/sec) [label=tokens, retained=16.4%]
…
⏱️  Label join: 4473 → 490 rows in 0.48ms (join=0.44ms, 1030585 rows/sec) [label=tokens, retained=11.0%]
✅ Loaded 13910 rows to token_transfers in 0.40s
```
:::

For each batch, the SDK prints how many rows matched the label, on standard error. Of the 110,969 transfers, the 13,910
of the four tokens in `tokens.csv` were loaded. The table has the query's columns, then the label's:

```python [summarize.py]
import polars as pl

transfers = pl.read_delta('./token_transfers')
print(transfers.columns)
print(transfers.group_by('symbol').len().sort('len', descending=True))
```

:::terminal
```bash
python summarize.py
```

```
['block_num', 'tx_hash', 'log_index', 'token_address', 'symbol', 'decimals', 'name']
shape: (4, 2)
┌────────┬──────┐
│ symbol ┆ len  │
│ ---    ┆ ---  │
│ str    ┆ u32  │
╞════════╪══════╡
│ USDT   ┆ 5459 │
│ USDC   ┆ 4158 │
│ WETH   ┆ 4135 │
│ DAI    ┆ 158  │
└────────┴──────┘
```
:::

## How the join works

* **It's an inner join.** A row whose key isn't in the label file isn't loaded. To keep every row, list every key, or
  load without a label and join later, in the destination.
* **Keys must have the same type.** Addresses and hashes are bytes in Amp's tables. The SDK converts the label's
  `address` columns to bytes, and, when the query returns a key as a `0x…` string instead, converts that string to
  bytes. A key column without `address` in its name stays text, and the join fails (see
  [Troubleshooting](/sdks/python/labels#troubleshooting)): name it so it's converted, or pass `binary_columns`.
* **The label's columns are added after the row's.** The key column appears once, under the query's name for it.
* **Labels are kept in memory**, for as long as the `Client` exists. A label file of a million rows takes about 100 MB.
  Registering a name again replaces its label.
* **Every loader supports labels**, since the SDK joins before the loader writes. The table the loader creates has the
  label's columns.
* **Labels apply to one-off loads**, `load()` without `stream=True`, **and to the catch-up phase of
  [parallel streaming](/sdks/python/parallel-streaming)**. Continuous streaming, with `stream=True`, ignores
  `label_config`: it loads every row, without the label's columns.

## Manage labels

`client.configure_label()` registers labels with the client's `label_manager`, which can also list and remove them:

```python
client.configure_label('tokens', 'tokens.csv')
print(client.label_manager.list_labels())  # ['tokens']

table = client.label_manager.get_label('tokens')  # A pyarrow.Table, or None
print(table.schema)

client.label_manager.remove_label('tokens')  # True if it existed
client.label_manager.clear()  # Removes every label
```

`get_label()` returns the label as the SDK joins it, after converting its `address` columns:

```
token_address: fixed_size_binary[20]
symbol: string
decimals: string
name: string
```

## Reference

### `client.configure_label()`

```python
client.configure_label(name: str, csv_path: str, binary_columns: list[str] | None = None) -> None
```

Reads a CSV file and registers it as a label.

| Parameter        | Type            | Default | Description                                                                                                                |
| ---------------- | --------------- | ------- | -------------------------------------------------------------------------------------------------------------------------- |
| `name`           | string          | none    | The name to register the label under, which `LabelJoinConfig` refers to, like `'tokens'`.                                  |
| `csv_path`       | string          | none    | The path to the CSV file, relative to the current directory or absolute, like `'tokens.csv'`.                              |
| `binary_columns` | list of strings | `None`  | The columns of hexadecimal values to convert to bytes, like `['contract']`. `None` converts the columns named `…address…`. |

Raises `FileNotFoundError` when the file doesn't exist, and `ValueError` when it can't be read as CSV.
`client.label_manager.add_label()` takes the same arguments.

### `LabelJoinConfig`

```python
from amp.loaders.types import LabelJoinConfig

LabelJoinConfig(label_name: str, label_key_column: str, stream_key_column: str)
```

| Field               | Type   | Description                                                  |
| ------------------- | ------ | ------------------------------------------------------------ |
| `label_name`        | string | The name you registered the label under.                     |
| `label_key_column`  | string | The label file's column to match, like `'token_address'`.    |
| `stream_key_column` | string | The query's column to match it with, like `'token_address'`. |

Pass it to `load()` as `label_config`. See [Loaders](/sdks/python/loaders) for `load()`'s other options.

### `client.label_manager`

| Method                                           | Returns                   | Description                                               |
| ------------------------------------------------ | ------------------------- | --------------------------------------------------------- |
| `add_label(name, csv_path, binary_columns=None)` | `None`                    | Same as `client.configure_label()`.                       |
| `get_label(name)`                                | `pyarrow.Table` or `None` | The label, or `None` if no label has that name.           |
| `list_labels()`                                  | list of strings           | The names of the registered labels.                       |
| `remove_label(name)`                             | boolean                   | Removes a label. `True` if it existed, `False` otherwise. |
| `clear()`                                        | `None`                    | Removes every label.                                      |

## Troubleshooting

When a label can't be joined, the load stops at its first batch, and its result's `error` says why, after
`FATAL: Permanent error loading batch (not retryable).`:

### `FileNotFoundError: Label CSV file not found: tokens.csv`

`configure_label()` couldn't find the file. Relative paths start from the directory you run Python in: run it from the
file's directory, or give an absolute path.

### `Label 'tokens' not found`

No label is registered under the `label_name` you passed. Call `client.configure_label()` with that name first, on the
same `Client` you load with.

### `Label key column 'address' not found in label table`

The label file has no column named `label_key_column`. Check the file's header row.

### `Stream key column 'addr' not found in batch schema`

The query returns no column named `stream_key_column`. Check the query's column names, including the ones you rename
with `AS`.

### `Incompatible data types for corresponding join field keys: … of type fixed_size_binary[20] and … of type string`

The label's key column is still text, because its name doesn't contain `address`. Pass it in `binary_columns`, like
`client.configure_label('tokens', 'tokens.csv', binary_columns=['contract'])`.

### Fewer rows than expected, or none

Only rows whose key is in the label file are loaded. The lines the SDK prints show how many rows of each batch matched.
Check that the label's keys are the full values, like a 40-digit address.
