Hugging Face Datasets Library: Loading and Processing Data
1. Overview
When working with NLP models or fine-tuning LLMs, you always need a prior step: preparing data in the right format, making it clean, and loading it into your training pipeline. Hugging Face’s datasets library solves exactly this problem — it gives you a unified API to load datasets from the Hub, batch-process them with map/filter, and save them in a format optimized for repeated reads.
After reading this guide, you’ll be able to load datasets from Hugging Face Hub, understand the Dataset/DatasetDict structure, preprocess text with tokenizers, handle large datasets with streaming, and write tests to validate your data processing pipeline.
2. Installation
pip install datasets transformers
The datasets library doesn’t strictly depend on transformers, but the tokenization section later will need it.
import datasets
print(datasets.__version__)
3.0.1
3. Loading datasets from Hugging Face Hub
3.1. Basic load_dataset
from datasets import load_dataset
reviews = load_dataset("imdb")
print(reviews)
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 25000
})
test: Dataset({
features: ['text', 'label'],
num_rows: 25000
})
unsupervised: Dataset({
features: ['text', 'label'],
num_rows: 50000
})
})
load_dataset fetches the script/metadata for the imdb dataset from the Hub, caches the compressed files locally (~/.cache/huggingface/datasets), and returns a DatasetDict — essentially a dictionary of Dataset objects organized by split (train, test, unsupervised).
3.2. Loading a specific split
train_reviews = load_dataset("imdb", split="train")
print(type(train_reviews), len(train_reviews))
<class 'datasets.arrow_dataset.Dataset'> 25000
Passing split="train" returns a single Dataset directly instead of a DatasetDict, which is convenient when you only need one split.
Note: If a dataset on the Hub requires a config (e.g.,
glue), you must pass the config name as well:load_dataset("glue", "mrpc"). Without this argument, you’ll get an error asking you to select a config.
4. Exploring Dataset structure
sample = train_reviews[0]
print(sample.keys())
print(sample["label"], sample["text"][:60])
dict_keys(['text', 'label'])
0 I rented I AM CURIOUS-YELLOW from my video store because of...
Accessing by index returns a regular Python dict. Dataset also supports slicing, which returns a dict of lists:
batch = train_reviews[0:3]
print(batch["label"])
[0, 0, 0]
View the schema using features:
print(train_reviews.features)
{'text': Value(dtype='string', id=None),
'label': ClassLabel(names=['neg', 'pos'], id=None)}
ClassLabel tells you that 0 corresponds to the neg label and 1 to pos — useful when mapping back to label names.
5. Processing data with map, filter, select
5.1. Using filter to filter rows
positive_only = train_reviews.filter(lambda row: row["label"] == 1)
print(len(positive_only))
12500
filter takes a function that returns True/False for each row, iterates through the entire dataset, and keeps only rows that satisfy the condition.
5.2. Using map to transform data
def add_length(row):
row["text_length"] = len(row["text"])
return row
train_with_length = train_reviews.map(add_length)
print(train_with_length[0]["text_length"])
1640
map applies a function to each row, returning a dict with new fields (or updated existing fields) — the results are merged back into the original dataset.
5.3. Using map with batches for speed
def add_length_batched(batch):
batch["text_length"] = [len(t) for t in batch["text"]]
return batch
train_fast = train_reviews.map(add_length_batched, batched=True, batch_size=1000)
With batched=True, the function receives an entire batch dict (each field is a list) instead of individual rows — this avoids calling the Python function 25,000 times and is significantly faster with large datasets.
5.4. Using select to get a subset
small_train = train_reviews.select(range(100))
print(len(small_train))
100
Use this when you want to quickly test your pipeline on a subset before running it on the full dataset.
6. Text preprocessing with tokenizer
The most common real-world use case: tokenizing text to feed into a model.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("distilbert-base-uncased")
def tokenize_batch(batch):
return tokenizer(batch["text"], truncation=True, max_length=256)
tokenized = train_reviews.map(tokenize_batch, batched=True)
print(tokenized.column_names)
['text', 'label', 'input_ids', 'attention_mask']
tokenize_batch takes a batch and returns a dict with input_ids and attention_mask — map automatically merges these new fields into the dataset while keeping existing fields. To drop unnecessary fields (e.g., raw text to save RAM), use remove_columns:
tokenized = train_reviews.map(
tokenize_batch, batched=True, remove_columns=["text"]
)
print(tokenized.column_names)
['label', 'input_ids', 'attention_mask']
7. Splitting, shuffling, and sharding
7.1. Creating your own train/test split
If a dataset has only one split, you can split it yourself with train_test_split:
split_result = train_reviews.train_test_split(test_size=0.2, seed=42)
print(split_result)
DatasetDict({
train: Dataset({
features: ['text', 'label'],
num_rows: 20000
})
test: Dataset({
features: ['text', 'label'],
num_rows: 5000
})
})
seed=42 ensures reproducible splitting across runs.
7.2. Shuffle and shard
shuffled = train_reviews.shuffle(seed=42)
part = shuffled.shard(num_shards=4, index=0)
print(len(part))
6250
shard splits the dataset into num_shards equal parts — useful when distributing data across multiple workers for distributed training.
8. Streaming for large datasets
When a dataset is too large to fit on disk or in RAM, use streaming=True:
streamed = load_dataset("imdb", split="train", streaming=True)
first_three = list(streamed.take(3))
print(len(first_three))
3
With streaming, datasets doesn’t download the entire file but reads records sequentially through an iterator. You can’t use len() or direct indexing, but you can still map/filter lazily (applied only when iterating).
Note: Streaming is best for datasets with hundreds of GB, like pretraining sets. For small datasets (a few hundred MB), regular loading (
streaming=False) is faster because it leverages caching and random access.
9. Saving and reloading datasets
After processing, save the result so you don’t have to re-run map every time:
tokenized.save_to_disk("data/imdb_tokenized")
from datasets import load_from_disk
reloaded = load_from_disk("data/imdb_tokenized")
print(len(reloaded))
25000
save_to_disk writes the dataset in Arrow format — reloading is very fast because no reparsing from the original source is needed. If you need to interact with pandas, convert directly:
df = tokenized.select(range(5)).to_pandas()
print(df.columns.tolist())
['label', 'input_ids', 'attention_mask']
10. Writing tests for your data processing pipeline
Your data processing pipeline should have tests to ensure the map/filter logic doesn’t silently break when you modify code. Here’s a test suite using pytest.
import pytest
from datasets import Dataset
def add_length(row):
row["text_length"] = len(row["text"])
return row
def test_add_length_computes_correct_value():
data = Dataset.from_dict({"text": ["hello", "hi there"]})
result = data.map(add_length)
assert result[0]["text_length"] == 5
assert result[1]["text_length"] == 8
def test_filter_keeps_only_positive_labels():
data = Dataset.from_dict({"label": [0, 1, 1, 0]})
positive = data.filter(lambda row: row["label"] == 1)
assert len(positive) == 2
assert all(row["label"] == 1 for row in positive)
Dataset.from_dict creates a small in-memory dataset without needing to fetch anything from the Hub — perfect for unit tests. The two tests above verify that map works correctly (creating new fields) and filter keeps only matching rows, without depending on real datasets from the internet.
pytest test_dataset_pipeline.py -v
test_dataset_pipeline.py::test_add_length_computes_correct_value PASSED
test_dataset_pipeline.py::test_filter_keeps_only_positive_labels PASSED
11. Common pitfalls and when not to use it
- Forgetting
batched=Truefor large datasets: Non-batchedmapis slow because it calls Python for each row individually. With hundreds of thousands of rows, always preferbatched=Trueif your logic allows vectorization. - Using random indexing on streamed datasets:
streamed[5]will raise an error because streamed datasets are iterators that don’t support random access. Use only.take(),.skip(), or sequential iteration. - Not setting
seedwithshuffle/train_test_split: results won’t be reproducible across runs, making it hard to debug when comparing metrics between experiments. - Uncontrolled cache bloat: each time you
mapwith a different function,datasetswrites a new cache file. If your machine has limited disk space, useload_dataset(..., keep_in_memory=True)or periodically clear~/.cache/huggingface/datasets. - Don’t use
datasetsfor complex relational data (multiple tables with joins, transactions). This library is optimized for flat tabular/text data used in ML, not as a database replacement.
12. Conclusion
The datasets library gives you a clean API to go from “raw dataset from the Hub” to “tokenized dataset ready for training” in just a few load_dataset, map, and filter calls. For small datasets, load normally and leverage Arrow caching; for huge datasets, switch to streaming=True to avoid downloading everything to disk. When building real data pipelines, always write tests for your map/filter functions using Dataset.from_dict — this helps you catch unintended changes to your preprocessing logic early.