# 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.