# Fixing a fastai DataBlock that silently yields mismatched labels

Concrete gotchas in fastai's DataBlock API that silently misbehave: get_x/get_y vs blocks mismatches via n_inp, splitter indices not matching get_items output (unreset DataFrame index), getter/item-type mismatches, and item_tfms vs batch_tfms stage ordering. Gives real Python examples and a debugging order using DataBlock.summary, Datasets indexing, show_batch, and decode.

Exact reference: {"kind":"skill_version","skill_id":"skl_05rTGXWXgxND2k5xhQ8jcQ","version_id":"skv_u8nO_i2HKTKOhret_MXjtw"}

Applicability: [{"constraint":"fastai v2 DataBlock API (fastai.data.block)","technology":"fastai","version_scheme":"unknown"},{"constraint":"any Python supported by fastai v2","technology":"python","version_scheme":"unknown"}]

# Debugging a fastai DataBlock That Silently Produces Wrong Batches

When a `DataBlock` misbehaves it rarely raises. It yields batches whose images and
labels are mismatched, whose split is wrong, or whose transforms ran at the wrong
stage. This skill gives a systematic debugging order: verify the getter/block
contract, verify the splitter indices, verify transform stages, then verify the
batch.

## The contract fastai actually enforces

`DataBlock` builds one tuple per item from `blocks`. From the fastai data.block
docs:

- `get_items` collects the raw items (files, DataFrame rows).
- `splitter` is a callable that receives those `items` and returns a tuple of
  iterables of **indices** — `(train_idx, valid_idx)`.
- `get_x` / `get_y` (or a `getters` list) are applied to each item, one getter per
  block.
- `n_inp` decides which blocks are inputs: the first `n_inp` blocks are inputs,
  the rest are targets. Default is 1.

The getters must line up 1:1 with the blocks. The docs show this failing fast:

```python
from fastai.vision.all import *

mnist = DataBlock((ImageBlock, ImageBlock, CategoryBlock),
                  get_items=get_image_files,
                  splitter=GrandparentSplitter(),
                  n_inp=2,
                  get_y=[parent_label, noop])  # 2 getters for 1 target block -> error
```

## Gotcha 1: `n_inp` silently flips your inputs and targets

With `blocks=(ImageBlock, CategoryBlock)` the default `n_inp=1` is right. But if
you reorder blocks without setting `n_inp`, the first block becomes the input:

```python
# WRONG: CategoryBlock is now the input, ImageBlock the target. No error.
dblock = DataBlock(blocks=(CategoryBlock, ImageBlock),
                   get_items=get_image_files,
                   splitter=GrandparentSplitter(),
                   get_y=parent_label)
```

Fix: set `n_inp` explicitly whenever the input is not the first block, or keep
inputs first:

```python
dblock = DataBlock(blocks=(ImageBlock, CategoryBlock),
                   get_items=get_image_files,
                   splitter=GrandparentSplitter(),
                   get_x=noop, get_y=parent_label)
```

## Gotcha 2: splitter indices do not match the items

The splitter returns indices **into the output of `get_items`**. Anything that
reorders or filters items after splitting — or a splitter computed from a
different index space — silently pairs the wrong x with the wrong y.

Concrete failure: a DataFrame whose index was never reset after filtering.

```python
df = df[df['is_valid'].notna()]          # index is now [5, 9, 14, ...]
dblock = DataBlock(blocks=(ImageBlock, CategoryBlock),
                   get_x=ColReader('fname'),
                   get_y=ColReader('label'),
                   splitter=IndexSplitter(range(0, 100)))  # positions 0..99,
                   )                                     # NOT df.index values
```

`IndexSplitter` positions are positional; `df.index` values are not. The
DataLoader quietly serves mismatched rows. Fix: `df = df.reset_index(drop=True)`
before building the DataLoader, or build the splitter from `df.index`:

```python
valid_idx = df.index[df['is_valid']].tolist()
splitter = IndexSplitter(valid_idx)
```

Same class of bug with custom splitters: compute indices from the exact `items`
object the splitter receives, e.g.:

```python
def splitter(items):
    train = [i for i, o in enumerate(items) if 'train' in str(o)]
    valid = [i for i, o in enumerate(items) if 'valid' in str(o)]
    return train, valid
```

## Gotcha 3: `get_x` and `get_y` see different things than you assume

Both getters receive the same raw item. A classic silent mismatch pairs a
path-reading `get_x` with a label function that expects something else:

```python
# get_x extracts the filename from a row, but parent_label expects a Path
dblock = DataBlock(blocks=(ImageBlock, CategoryBlock),
                   get_x=ColReader('fname'),
                   get_y=parent_label,   # crashes or misbehaves: item is a str
                   splitter=RandomSplitter())
```

Rule: match the getter to the item type. `get_image_files` yields paths, so
`parent_label` works. A DataFrame yields rows, so use `ColReader('label')` and
make sure the label column holds what `CategoryBlock` expects (a string, or a
`;`-separated string for `MultiCategoryBlock`).

## Gotcha 4: `item_tfms` vs `batch_tfms` run at different pipeline stages

fastai applies transforms in three stages, in order: **type transforms** (create
the tuple, e.g. `PILImage.create`, `Categorize`), then **item transforms**, then
**batch transforms**. `item_tfms` run per item before collation; `batch_tfms` run
per batch after collation.

Consequences:

- Put resizing that must precede augmentation in `item_tfms`, and the random
  crop/augment plus normalization in `batch_tfms` (the standard presizing
  pattern):

```python
dblock = DataBlock(blocks=(ImageBlock, CategoryBlock),
                   get_items=get_image_files,
                   splitter=GrandparentSplitter(),
                   get_y=parent_label,
                   item_tfms=Resize(460),
                   batch_tfms=[*aug_transforms(size=224),
                               Normalize.from_stats(*imagenet_stats)])
```

- A batch transform that needs setup statistics (e.g. `Normalize`) fails or
  behaves oddly if forced into `item_tfms`.
- Each `TransformBlock` can carry its own `item_tfms`/`batch_tfms`; those run
  **before** the DataBlock-level ones for the same stage. Duplicating a transform
  in both places applies it twice — a common cause of over-augmented or
  double-normalized batches.

## Debugging order

1. `dblock.summary(source)` — steps through the pipeline for one sample and
   shows each transform applied. Read it top to bottom.
2. `dsets = dblock.datasets(source)` then `dsets.train[0]` — check the raw
   decoded tuple before batching.
3. `dls = dblock.dataloaders(source)` then `dls.show_batch()` or
   `dls.train.one_batch()` — check what the model actually receives.
4. Decode a batch back: `dls.decode(dls.one_batch())` shows whether labels
   survived the transforms.
5. Check splits directly: `len(dls.train.items)`, `len(dls.valid.items)`, and
   `dls.train.items` vs the indices you expected.


## Supporting basis and limitations

All claims verified against the official fastai data.block documentation (docs.fast.ai/data.block.html): DataBlock signature showing blocks/getters/n_inp/item_tfms/batch_tfms keyword args with get_items, splitter, get_y, get_x as keyword-only; the statement that splitter is a callable receiving items and returning a tuple of iterables of train/valid indices; getters applied to results of get_items; n_inp semantics (first n_inp blocks are inputs, default 1) including the documented expect_fail example where get_y holds 2 functions for 1 output block; the three transform stages (type, item, batch) with the MNIST example listing type_tfms, default_item_tfms (ToTensor), default_batch_tfms (IntToFloatTensor); and the Debugging section documenting DataBlock.summary(source, bs, show_batch). Supporting examples use documented splitter names (GrandparentSplitter, RandomSplitter, IndexSplitter), getters (parent_label, ColReader, noop), and blocks (ImageBlock, CategoryBlock, MultiCategoryBlock).

## Change and rationale

New skill covering silent DataBlock misbehavior: n_inp input/target flipping, splitter index misalignment, get_x/get_y item-type mismatches, and item_tfms vs batch_tfms pipeline-stage ordering, with a five-step debugging order.

fastai DataBlock failures are mostly silent: batches with wrong labels, wrong splits, or double-applied transforms produce no error. Agents writing fastai data pipelines need the enforced contract (getters 1:1 with blocks, splitter returns indices into get_items, n_inp partitions inputs/targets, three transform stages) and a deterministic debugging order grounded in the official docs.
