Data Collection — APIs, SQL, Files and Scraping
Where ML data actually comes from and how to pull it reliably. REST APIs with pagination, SQL queries at scale, Parquet pipelines, and scraping — all with production-grade error handling.
Nobody hands you a clean CSV. Data has to be pulled, negotiated with, and earned.
Every ML tutorial starts with a dataset already loaded — iris.csv, mnist, titanic. The real world does not. At DoorDash, the order data lives in a PostgreSQL database behind an internal API. At Stripe, transaction records are in a Redshift warehouse partitioned by date. At Instacart, inventory data is a stream of events in Kafka. At a startup, it might be a Google Sheet someone exports manually.
Before you can train a model, you have to collect the data. This means making HTTP requests to APIs, running SQL queries, reading from cloud storage, and sometimes scraping a website when there is no API. Each source has its own format, its own failure modes, its own rate limits, and its own quirks.
This module covers every major data source an ML engineer encounters — with real error handling, pagination, retry logic, and performance patterns that make the difference between a pipeline that works once and one that runs reliably every day.
What this module covers:
REST APIs — pulling data over HTTP
A REST API is the most common way to get data from any modern service. You send an HTTP request — GET, POST, PUT, DELETE — to a URL. The server returns JSON. You parse it into a DataFrame or dictionary. The Python requests library handles this in 3 lines. The hard parts are authentication, pagination, rate limiting, and handling failures gracefully.
Basic GET request — the foundation
Authentication — API keys, Bearer tokens, OAuth
Retry logic — handle transient failures automatically
APIs fail. Networks drop. Servers restart. A data collection pipeline that crashes on the first 503 response is not production-ready. You need automatic retry with exponential backoff — wait longer after each failure to avoid hammering a struggling server. The requests library's HTTPAdapter with Retry handles this cleanly.
Pagination — fetching all pages of a large dataset
Most APIs don't return all records at once — they paginate. You get page 1 (100 records), then request page 2, then page 3, until there are no more pages. There are three pagination styles in the wild, and you'll encounter all of them.
SQL — querying databases for ML data
Most company data lives in a relational database — PostgreSQL, MySQL, SQLite, or a cloud warehouse like BigQuery, Redshift, or Snowflake. For ML, you typically need to write a SQL query that joins multiple tables, filters by date range, and aggregates features — then load the result into a Pandas DataFrame. SQLAlchemy is the standard Python library for database connections, and it works with every database.
Chunked reading — large tables that don't fit in RAM
Cloud warehouses — BigQuery, Redshift, Snowflake
Reading files — local, S3, GCS and Azure Blob
In many companies, data is deposited into cloud storage as files — CSV exports from operational databases, Parquet files from data pipelines, JSON dumps from event systems. Cloud storage (S3, GCS, Azure Blob) is cheap, scalable, and Python can read from it almost as easily as from local disk.
Web scraping — extracting data from HTML pages
Some data sources have no API — competitor pricing, job listings, salary data, product reviews, public datasets published as web tables. Web scraping extracts structured data from HTML. Always check the site's robots.txt and Terms of Service before scraping. Scrape politely — add delays, use session caching, and never scrape faster than a human would browse.
BeautifulSoup — static HTML pages
Playwright — JavaScript-rendered dynamic pages
Many modern sites render content with JavaScript — the HTML you get from requests.get() is just a shell with no data. You need a real browser. Playwright controls a real Chromium browser from Python, waits for JavaScript to load, then extracts the rendered HTML.
Kafka — reading streaming event data for ML
High-throughput ML systems — fraud detection, real-time recommendations, delivery ETA prediction — often need to consume data as it streams in, not from batch queries. Apache Kafka is the standard event streaming platform. For ML, you typically read from a Kafka topic, process events, extract features, and either update a model or score against one.
A reusable data collection pipeline
Production data collection is not one-off scripts — it's a pipeline that runs on a schedule, handles failures, logs progress, and stores results in a consistent location. Here's the structure every ML data pipeline follows.
Every common data collection error — explained and fixed
Production data collection is four separate systems, not one script
Everything in this module — pagination, retry logic, chunked SQL reads, scraping — is the mechanics. In a real company, those mechanics get wired into four distinct collection systems that rarely share code, because they solve different problems on different timelines. Understanding which system a given dataset comes from tells you what kind of data quality problems to expect before you have even opened the file.
The other thing that differs sharply from a tutorial is where validation happens. A tutorial collects data, then cleans it. A production ingestion pipeline puts a quality gate directly at the point of collection — before a single row lands in the warehouse — checking that the response schema matches what was expected, that row counts are within a normal range of the last run, and that no field's null rate jumped overnight. Catching a broken upstream event schema at ingestion, within minutes, is the difference between losing one day of data and silently training on three weeks of corrupted labels before anyone notices.
On most teams, no single person owns all four systems above. Event logging is usually owned by product/backend engineering, with the ML or data engineering team as a consumer who requests new events and reviews schemas. Third-party API integrations are typically owned directly by whichever data engineer built the pipeline, since they carry the retry and rate-limit logic. Labeling vendor relationships are frequently owned by a dedicated data operations or annotation team, not by ML engineers at all. The skill this module teaches — reliable pagination, retries, and validation — is what lets one ML engineer safely consume data from all four without needing to own any of them end to end.
Five things people get wrong about data collection
More rows only help if they are drawn from the same distribution the model will actually see in production and are collected with reasonably consistent quality. Doubling a training set by scraping a source with a very different population, or by including years of data from before a product redesign changed user behaviour entirely, can make a model worse, not better, because it dilutes the signal that matches current reality with signal that does not. The right question is never "how much data" in isolation — it is whether this additional data is representative of what the model needs to predict.
A model trained once on a static export is already stale the moment user behaviour, pricing, or the product itself changes. Every pipeline in this module — the pagination loops, the retry logic, the DataCollector class with checkpointing — is built to run repeatedly on a schedule, not once. Production data collection is closer to a standing service than a script: it runs daily or hourly, indefinitely, for as long as the model it feeds stays in production.
A SQL table looks identical whether it was populated by a rigorously tested internal pipeline or by a one-off script someone wrote for a demo two years ago and never revisited. The warehouse gives every table the same clean tabular appearance regardless of how reliable its upstream source actually is. Before trusting a table, it is worth finding out who owns the pipeline that fills it, how often it runs, and whether anyone monitors it — the same question this module asks about your own collection code applies just as much to data someone else already collected for you.
Third-party APIs change response schemas, deprecate fields, tighten rate limits, and occasionally shut down entirely, usually with a changelog email nobody on the ML team is subscribed to. The retry and error-handling code in this module protects against transient failures — a dropped connection, a momentary 500 — but it does nothing against a vendor silently renaming a field or changing units. That requires an explicit validation step that checks the shape of what came back, not just whether the request succeeded.
Scraping carries real costs that a "free" API-less data source hides at first glance: legal and terms-of-service risk if the target site prohibits it, an ongoing maintenance burden every time the site's HTML structure changes and silently breaks your selectors, and an ethical obligation to scrape at a pace that does not degrade the target site for its actual users. It is a legitimate last resort when no API exists, not a default choice to prefer over a documented, rate-limited, officially supported API.
Data collection — 5 questions interviewers actually ask
I would start by identifying the source type — internal database, third-party API, event stream, or a source with no API at all — since that decides the whole shape of the pipeline. For an API source, I would build in pagination and retry with exponential backoff from day one, not as an afterthought, because transient failures are the norm at any real volume. I would add a validation step immediately after collection that checks schema and row-count sanity before the data reaches storage, and wrap the whole thing in a class with logging and checkpointing so a failure partway through a large pull does not require starting over from zero. Finally I would schedule it to run repeatedly, since almost no real collection pipeline is a one-time job.
First I would try to characterise the bias concretely rather than treat it as a vague worry — compare the collected population against a known ground truth, like overall traffic logs, to see which segments are over- or under-represented. If the gap traces back to the collection mechanism itself, such as an API that only surfaces recent records or a scraper that only reaches paginated results up to a vendor-imposed limit, I would look for a way to close that gap directly, for example by adding a second collection path for the missing segment. If closing the gap is not feasible, I would document the known bias explicitly for whoever trains on the data next, rather than let it get treated as a neutral, representative dataset.
I treat this as a cost curve rather than an all-or-nothing decision. Early on, a simple script with basic retry logic is enough to validate whether a data source is even useful for the model. Once a source proves valuable and the pipeline is expected to run indefinitely, the calculus changes: the cost of an unnoticed silent failure — training on three weeks of corrupted or missing data before anyone catches it — usually dwarfs the engineering cost of adding proper validation, alerting, and checkpointing. I would invest reliability engineering in proportion to how expensive a silent failure would actually be, not apply the same bar to every source uniformly.
For well-understood source types — SQL databases, common cloud storage formats, standard REST APIs — I lean toward existing, well-tested libraries like SQLAlchemy, boto3, or requests with a retry adapter rather than writing bespoke connection handling. I reserve custom code for the parts that are genuinely specific to the business: the pagination style of one particular internal API, the exact validation rules for this dataset, or the checkpointing and logging structure that fits the team's existing pipeline conventions. The goal is to spend engineering effort on the 10 percent that is actually unique, not reinvent the 90 percent that a mature library already solved.
Ideally I find out from an automated schema check that runs immediately after every collection run and fails loudly the moment a field disappears, a type changes, or values fall outside an expected range — the same principle as the response validation shown in this module's retry logic, extended to check structure, not just HTTP status. Without that check in place, the realistic failure mode is discovering it downstream when a training job crashes or a feature is unexpectedly null. Once caught, I would pin the pipeline to whatever version of the API contract still works, patch the parsing code, and use the incident to justify adding the schema check that should have caught it automatically the first time.
You can now pull data from any source a real ML project will encounter.
APIs with pagination and retry logic. SQL databases with chunked reading and parameterised queries. Cloud storage on S3 and GCS. Dynamic web pages with Playwright. Kafka event streams. A reusable pipeline class that handles failures, logging, and checkpointing. These are the building blocks every ML data engineer uses.
Module 16 moves to data cleaning and validation — the step that comes after collection. Raw data from any of these sources will have nulls, wrong types, duplicate records, schema drift, and outliers. Cleaning it systematically — with validation rules that catch problems before they reach the model — is what separates a reliable pipeline from a fragile one.
Schema validation, duplicate detection, type coercion, outlier handling, and building validation rules that run automatically every time new data arrives.
🎯 Key Takeaways
- ✓Always set a timeout on every HTTP request — requests.get(url, timeout=30). A missing timeout can block a pipeline indefinitely. Use a requests.Session for repeated calls to the same host — it reuses the TCP connection and stores headers.
- ✓Retry logic is not optional for production pipelines. Use exponential backoff: delay = min(base * 2^attempt, max_delay). Always respect the Retry-After header on HTTP 429 responses — it tells you exactly how long to wait.
- ✓Three pagination styles exist: offset/limit (increment page number), cursor-based (use next_cursor from response), and link header (follow rel="next" URL). Always check the API docs to identify which style before writing pagination code.
- ✓Use SQLAlchemy for database connections — it works with every database using the same interface. Always use parameterised queries (text() with bind params) — never format Python variables directly into SQL strings.
- ✓For large SQL tables, use chunksize in pd.read_sql() to process in batches. This prevents MemoryError on multi-million row tables and lets you apply transformations incrementally.
- ✓BeautifulSoup works for static HTML. Playwright is required when content is rendered by JavaScript. Always add delays between requests, check robots.txt, and cache scraped HTML locally during development to avoid re-scraping.
- ✓Wrap every production data collection job in a class with logging, retry tracking, checkpointing, and consistent output format. A pipeline that silently fails and produces empty output is worse than one that fails loudly.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.