Introduction
Pandas and large DataFrames: how to read in chunks is the single skill that keeps working laptops alive when data science meets production scale. Pandas chunksize turns a memory-hungry file read into an iterator that streams a dataset piece by piece into RAM. Modern surveys show datasets larger than memory are routine for practitioners running local pandas workflows on eight and sixteen gigabyte machines. Reading a five gigabyte CSV in one shot will crash a laptop, yet the same file with a well chosen chunksize can be aggregated in minutes. This guide covers pandas chunksize across read_csv, read_excel, read_sql, and read_parquet with runnable code and memory numbers. You will learn when a chunk iterator is enough, when you should reach for Polars streaming, and when Dask remains the right tool. Every section maps to a real search users perform, from pandas chunk dataframe to pandas read_excel chunksize parameter support. By the end you will have a repeatable pattern for reading, filtering, and aggregating any large DataFrame without exhausting memory.
Quick Answers on Pandas Chunksize and Large DataFrames
What is pandas chunksize and how does it help with large DataFrames?
Pandas chunksize is a parameter passed to read_csv, read_sql, and read_json that returns an iterator of DataFrames instead of one huge object. Each chunk is a normal DataFrame with N rows, letting you process files larger than RAM in a streaming loop.
Does pandas read_excel support the chunksize parameter?
No. Pandas read_excel does not accept chunksize because the underlying openpyxl and xlrd engines load the entire worksheet into memory before pandas sees it. Simulate chunks with nrows plus skiprows, iterate openpyxl rows directly, or convert the workbook to CSV first.
What chunksize value should I start with for a 5 GB CSV?
Start at one hundred thousand rows and tune from there. Watch peak resident memory per chunk with memory_profiler, then raise or lower chunksize so each chunk sits under one gigabyte on a typical laptop.
Key Takeaways for Chunked Pandas Reads
- Pandas chunksize returns a TextFileReader iterator, not a DataFrame, so you must loop over it and aggregate results yourself.
- read_excel has no chunksize parameter because openpyxl reads the workbook in one pass, so use nrows and skiprows or convert to CSV.
- read_sql chunksize needs SQLAlchemy execution_options with stream_results=True to avoid pulling the full result set into memory first.
- read_parquet chunks through pyarrow ParquetFile.iter_batches, aligning your chunk size with the file’s row group boundaries for the best throughput.
Table of contents
- Introduction
- Quick Answers on Pandas Chunksize and Large DataFrames
- Key Takeaways for Chunked Pandas Reads
- What is Pandas Chunksize in Large DataFrame Reads
- The Memory Problem Behind Every Failed pandas Read
- How the Chunksize Iterator Works Under the Hood
- How to Use read_csv Chunksize for Very Large CSV Files
- Why read_excel Does Not Support Chunksize and What to Do Instead
- How to Chunk read_sql for Streaming Database Queries
- How to Chunk read_parquet Using PyArrow Row Groups
- Aggregating and Filtering DataFrames Across Chunks
- Optimizing dtypes to Multiply the Effective Chunksize
- When to Switch from Pandas Chunking to Polars or Dask
- Common Pitfalls and Risks When Chunking Large Pandas Reads
- Ethics of Big Data Pipelines Built on Chunked Reads
- Future of Chunked Pandas: Arrow, Polars, and DuckDB
- How to Implement Chunked Pandas Reading Step by Step
- Key Insights on Chunked Reads and Memory Efficiency
- Real Companies Using Chunked Pandas Reads in Production
- In-Depth Case Studies of Large DataFrame Chunking Pipelines
- Frequently Asked Questions on Pandas Chunksize and Large DataFrames
What is Pandas Chunksize in Large DataFrame Reads
Pandas chunksize is a keyword argument that switches a reader function from returning one DataFrame to returning a TextFileReader iterator. Each iteration yields a DataFrame containing chunksize rows, letting pandas stream a file larger than memory without ever loading it all at once.
Chunksize Memory Estimator
Move the sliders to see how chunksize, columns, and dtypes drive peak memory.
Peak memory per chunk
Percent of free RAM used
Estimate model: rows x columns x bytes_per_cell + 30% pandas index and metadata overhead. Real numbers vary with dtype mix and Python object overhead.
The Memory Problem Behind Every Failed pandas Read
A full pandas read of a five gigabyte CSV routinely uses fifteen to twenty gigabytes of RAM once dtypes are inferred and Python object columns are materialized. The reason is that pandas stores strings as generic PyObject pointers, and float columns default to sixty four bit precision even when thirty two bits would suffice. The pandas scaling guide notes that groupby and merge can double this footprint again during the operation. Practitioners see the same failure mode on production laptops with sixteen gigabytes, where the notebook kernel dies before the first summary statistic prints.
The core insight for pandas and large dataframes: how to read in chunks is that a chunk iterator replaces the all or nothing pattern with a streaming loop that keeps only one chunk resident at any moment. If your chunksize is one hundred thousand rows and your row width is roughly one hundred bytes, each chunk occupies about ten megabytes. That is a two thousand times reduction over loading the same file whole, and the garbage collector reclaims each chunk once the loop moves on. The trade off is that many pandas operations, like groupby or merge, cannot see the full dataset at once and must be simulated with running aggregations. This changes how you think about analysis: state carried between iterations replaces vectorized operations over the entire frame.
The memory advantage compounds when you pair chunksize with column selection using usecols and dtype tuning. A pandas one liner that reads three hundred columns as float64 objects can shrink to fifty megabytes per chunk when only the twenty needed columns are read as int16 or category. Practitioners writing essential pandas one-liners for data quality report ten times memory reductions from dtype tuning alone. Combined with chunksize the savings turn a crash into a routine batch job that runs while the analyst reads their email.
How the Chunksize Iterator Works Under the Hood
Building on that foundation, pandas chunksize hands back an object of type TextFileReader whose __iter__ method yields DataFrames on demand. Internally the reader keeps the file handle open and calls the underlying C parser repeatedly, materializing chunksize rows per call. This design means the reader is a context manager, and closing it releases the file handle even when a downstream loop raises an exception.
The C parser used by pandas is the same code path as a full read, so parse speed per row is unchanged whether you stream or slurp. What changes is the Python side heap usage, because each yielded DataFrame is a small independent object. Reference counting frees each chunk automatically once the next iteration overwrites the variable, keeping peak memory close to the size of a single chunk. This is why chunking works even on machines with only four or eight gigabytes of RAM despite the raw file weighing many multiples of that.
A subtle detail catches new users: dtype inference happens per chunk unless you pass an explicit dtype dictionary. Two chunks can end up with different dtypes for the same column when one chunk has all integers and the next contains a stray null. Downstream concat calls then upcast silently to object, which balloons memory. Fixing the dtype at the call site with a dtype mapping prevents this drift and keeps every chunk aligned for aggregation. The pandas read_csv chunksize documentation covers the exact dtype parameter shape.
The iterator also supports the get_chunk method for pulling a specific number of rows without exhausting the iterator. This is useful when you want to peek at the first ten thousand rows for a schema check before committing to a full stream. Combined with reset_index and a running row counter you can build resumable pipelines that continue from a specific offset after a crash. The pattern is common in ETL scripts that must reload interrupted work without restarting from row zero, which matters when input files are gigabytes and the network egress is expensive.
How to Use read_csv Chunksize for Very Large CSV Files
Turning to the most common use case, pandas and large dataframes: how to read in chunks with read_csv chunksize is the pattern that runs in ninety percent of production data pipelines built on pandas. Set chunksize to one hundred thousand as a starting point, then loop over the iterator with a for statement, aggregating results into a small state object. The OneUptime guide on reading large CSV files benchmarks a seven gigabyte file processed in eleven minutes on a laptop with only eight gigabytes of RAM. The naive full read on the same machine ran out of memory in under thirty seconds.
Combine chunksize with usecols to select only needed columns and with a dtype dictionary to lock down widths. A common ETL pattern reads the file once, filters each chunk on a date range, and appends surviving rows to a Parquet file using pyarrow's write_table with append mode. This gives you a compressed, columnar output that later reads back in a fraction of the time. Practitioners familiar with reverse ETL and how it is used often plug chunked pandas reads into that flow because they preserve exact schemas without needing a warehouse round trip.
import pandas as pd
reader = pd.read_csv(
"sales_2025.csv",
chunksize=100_000,
usecols=["order_id", "customer_id", "amount", "order_date"],
dtype={"order_id": "int32", "customer_id": "int32", "amount": "float32"},
parse_dates=["order_date"],
)
totals = {}
for chunk in reader:
q = chunk["order_date"].dt.to_period("Q")
grouped = chunk.groupby(q)["amount"].sum()
for period, value in grouped.items():
totals[period] = totals.get(period, 0.0) + float(value)
result = pd.Series(totals).sort_index()
print(result)
Why read_excel Does Not Support Chunksize and What to Do Instead
Pandas read_excel does not accept a chunksize parameter because the underlying openpyxl, xlrd, and calamine engines cannot stream a spreadsheet row by row. A modern xlsx file is a zip archive containing shared string tables and cell metadata that must be indexed together, so the engine reads the full worksheet into memory before the first row reaches pandas. This is documented explicitly in the pandas read_excel reference page, which lists nrows and skiprows but no chunksize. Users who search for pandas read_excel chunksize parameter support keep landing on outdated answers that suggest passing chunksize anyway, which silently does nothing.
The practical workaround is to simulate chunks with nrows and skiprows, calling read_excel repeatedly with an offset that advances by the chunk size. This works for workbooks that fit in memory once but where you want to process the rows in memory-safe batches for downstream steps. For workbooks too large to load at all, drop down to openpyxl directly and use load_workbook with read_only=True plus iter_rows to stream row objects. The Finxter guide to processing Excel data in chunks compares these approaches with runnable code.
The fastest path for very large workbooks is to convert xlsx to CSV once with openpyxl or the calamine engine, then use read_csv chunksize on the converted file. Practitioners running this pattern report that a two gigabyte xlsx that could not open in Excel becomes a routine chunked read within an hour of conversion. When you cannot preprocess, dask.dataframe wraps read_excel with parallel workers that read separate sheets in parallel. That path trades simplicity for scale and is worth the switch when nightly ETL times exceed a two hour target.
import pandas as pd
from openpyxl import load_workbook
wb = load_workbook("orders.xlsx", read_only=True, data_only=True)
ws = wb["Sheet1"]
CHUNK = 50_000
rows_iter = ws.iter_rows(values_only=True)
header = next(rows_iter)
buffer = []
for i, row in enumerate(rows_iter, start=1):
buffer.append(row)
if len(buffer) >= CHUNK:
df = pd.DataFrame(buffer, columns=header)
# process df here
buffer.clear()
if buffer:
df = pd.DataFrame(buffer, columns=header)
# process last chunk
How to Chunk read_sql for Streaming Database Queries
Moving from files to databases, pandas and large dataframes: how to read in chunks extends to read_sql, which accepts a chunksize argument that returns an iterator of DataFrames just like read_csv. What most tutorials miss is that chunksize alone does not stream from the database, because the default SQLAlchemy connection fetches the entire result set into a client side buffer first. Only when you pair chunksize with execution_options and stream_results=True does the driver open a server side cursor. The Python Speed guide to loading SQL data into pandas without running out of memory demonstrates the exact pattern with PostgreSQL.
The server side cursor keeps the working set on the database server, sending rows over the wire only as pandas requests them. This matters for tables of tens or hundreds of millions of rows where the full transfer would exhaust either client RAM or the network egress budget. Practitioners familiar with big data vs data mining explained know the distinction: mining fits in RAM, streaming does not. Chunked read_sql is the bridge that keeps mining techniques usable against streaming scale data.
An unresolved wart lies in the pandas issue tracker on read_sql chunksize memory usage, where users have logged since 2016 that pandas can still spike RAM when chunksize is set without stream_results. The workaround is straightforward: always pass a SQLAlchemy connection object rather than a raw engine, and always call execution_options on that connection. Combined with a modest chunksize of ten thousand to fifty thousand rows this pattern keeps memory flat at a few hundred megabytes for arbitrarily large queries. Ignoring the pattern is the most common cause of a read_sql pandas job dying on tables that should be trivially processable.
Not every database driver supports server side cursors equally well. PostgreSQL via psycopg2 handles them cleanly, MySQL requires SSCursor from mysql-connector-python, and SQL Server needs specific ODBC driver flags. The LikeGeeks tutorial on pandas read_sql chunksize catalogs these driver quirks. When your organization runs mixed database types, a shared wrapper function that abstracts the streaming setup avoids each analyst rediscovering the same trap. The wrapper also becomes the natural place to plug in retry logic and progress logging, both of which chunked reads make cheap.
import pandas as pd
from sqlalchemy import create_engine
engine = create_engine(
"postgresql+psycopg2://user:pw@dbhost/warehouse"
)
with engine.connect().execution_options(stream_results=True) as conn:
reader = pd.read_sql(
"SELECT order_id, customer_id, amount, order_date "
"FROM orders WHERE order_date >= '2025-01-01'",
conn,
chunksize=25_000,
)
running_total = 0.0
for chunk in reader:
running_total += chunk["amount"].sum()
print(f"processed {len(chunk):,} rows, running total ${running_total:,.2f}")
How to Chunk read_parquet Using PyArrow Row Groups
Shifting focus to columnar formats, pandas and large dataframes: how to read in chunks continues with read_parquet, which does not accept a chunksize parameter directly, but pyarrow provides a clean ParquetFile.iter_batches interface that pandas can wrap. The trick is that Parquet files are already split into row groups on disk, which are the natural chunk boundaries. Iterating batches at the row group boundary means the reader never has to buffer partial groups, which is faster than any character based split. The pandas read_parquet reference page documents the engine parameter that selects pyarrow as the backend.
Choose a batch size that matches your row group size to avoid double buffering. If the file was written with one hundred thousand rows per row group, ask for one hundred thousand row batches or a multiple of that. Practitioners writing on big data vs small data often note that Parquet at this scale reads five to ten times faster than the equivalent chunked CSV, primarily because columnar compression and predicate pushdown do more work per byte read. Dask picks up the same pattern automatically when you use dask.dataframe.read_parquet, which partitions on row groups without any configuration.
import pyarrow.parquet as pq
import pandas as pd
pf = pq.ParquetFile("events_2025.parquet")
for batch in pf.iter_batches(batch_size=100_000, columns=["user_id", "event_type", "ts"]):
chunk = batch.to_pandas()
counts = chunk["event_type"].value_counts()
# merge counts into a running Series
pass
Aggregating and Filtering DataFrames Across Chunks
Beyond raw reading, the real work happens between chunks: keeping a running aggregate that ends up correct across the entire dataset. A pattern that works for sums, counts, and means is a defaultdict keyed by group with running totals, then a final divide at the end for means. This replaces the vectorized groupby that would otherwise require the full DataFrame in memory. For medians and percentiles the answer is harder because those statistics are not additive, so you either downsample per chunk or use a datasketches approximation like t-digest.
Filtering across chunks is straightforward when the filter is per row, because a boolean mask on each chunk works exactly the same as on a full DataFrame. Cross row filters like keeping the top N by revenue require a small heap that maintains the top N so far. Practitioners familiar with the top data science interview questions recognize this as the classic streaming top K problem, and heapq in the Python standard library handles it in a few lines. Combine a per chunk filter with a top K heap and you can profile a hundred gigabyte log file on a laptop.
Joins across chunks are the hardest case because a single row on the left may match a row anywhere in the right. When one side fits in memory the pattern is a broadcast join: load the small side once and merge each chunk of the large side against it. When neither side fits, you either preprocess both sides into hash partitioned Parquet files or accept that pandas is no longer the right tool. The teams behind master semantic segmentation in your AI pipeline hit this wall constantly when joining label metadata against pixel level tables, and their solution is nearly always a switch to DuckDB or Dask.
Optimizing dtypes to Multiply the Effective Chunksize
Stepping back to first principles, dtype tuning is the single highest leverage change you can make on a chunked pandas read. A DataFrame of one million rows with ten string columns can drop from four hundred megabytes to forty megabytes when strings become categorical and float64 columns become float32. The Towards Data Science benchmark on loading the same CSV ten times faster with ten times less memory quantifies the effect across realistic tabular datasets. The savings stack on top of chunksize gains, effectively multiplying the amount of raw data your machine can process per pass.
Apply dtype hints at read time through a dictionary passed to the dtype parameter, rather than converting after the fact. Converting after materializes both the wide and narrow versions during the cast, temporarily doubling memory. Use category for low cardinality strings, int8 or int16 for small integer ranges, and float32 when three digit precision is enough. Store dates as datetime64[ns] via parse_dates because pandas represents those tightly, and never leave date columns as object strings. This alone has produced ten times memory reductions for teams that read the same file daily.
When to Switch from Pandas Chunking to Polars or Dask
Turning to alternatives, pandas chunking has clear limits and knowing when to switch matters as much as knowing how to chunk. Polars streaming reads a Parquet or CSV lazily, then executes the entire query plan chunk by chunk in native Rust with automatic parallelism. Benchmarks in Python Speed on why Polars uses less memory than pandas show two to ten times lower peak memory on identical queries. If your workload is a fixed pipeline of filter, groupby, and join operations, Polars streaming will almost always outperform hand written pandas chunk loops.
Dask remains the right choice when the workload must scale across multiple machines or use multiprocessing on a single fat node. Its dataframe API mimics pandas closely enough that many chunked scripts translate one for one, and it partitions Parquet files on row groups automatically. The trade off is scheduler overhead: for a single laptop reading a ten gigabyte file, Dask is often slower than a well tuned pandas chunk loop. Practitioners exploring Node.js for data science projects note the same pattern in JavaScript: distributed engines help beyond a threshold and hurt below it.
DuckDB is the surprise entrant that has quietly displaced both Polars and Dask for many analytical workloads. It reads Parquet and CSV natively, runs SQL over them with a vectorized engine, and integrates with pandas through zero copy Arrow. For chunked reads specifically, a DuckDB query with LIMIT and OFFSET pushed into the reader can be simpler and faster than a hand written pandas chunk loop. The pattern is worth benchmarking whenever you find yourself writing custom aggregation logic that SQL already expresses cleanly, similar to how gradient boosting with XGBoost displaced hand written classifiers.
Common Pitfalls and Risks When Chunking Large Pandas Reads
The most common failure mode is silent dtype drift between chunks that corrupts the final aggregate without raising any error. This happens when column one is all integers in the first chunk and contains a null string like "NA" in the tenth chunk, so pandas infers int for chunk one and object for chunk ten. A subsequent concat call then upcasts everything to object, ballooning memory and slowing every downstream operation. Fixing this requires passing an explicit dtype dictionary at read time so every chunk parses to the same shape. The pattern is documented in the pandas read_csv dtype parameter reference, yet remains one of the most common bugs in production pipelines.
A second pitfall is state that grows unbounded inside the chunk loop, turning a memory efficient pattern into a delayed memory crash. If your running aggregate is a defaultdict keyed by a high cardinality column like user_id, the state itself can reach many gigabytes. The fix is to bound the state size explicitly, either by aggregating to lower cardinality dimensions or by streaming results to disk periodically. Teams building per user features hit this constantly because the user table itself exceeds RAM.
A third pitfall is treating chunksize as a substitute for algorithmic thinking rather than a memory management technique. Chunking cannot magically make groupby, join, or sort operations correct across partitions if the operation semantically requires the full dataset. Teams that assume otherwise ship subtly wrong results that pass unit tests on small samples but diverge from the truth at scale. The pandas issue tracker on read_sql chunksize memory usage catalogs several years of user reports rooted in this misunderstanding. Recognize chunking as memory management and apply proper streaming algorithms for aggregate correctness.
Ethics of Big Data Pipelines Built on Chunked Reads
Beyond mechanics, chunked pandas pipelines carry the ethical footprint of every big data workflow they enable. Processing petabytes of user event data on a single laptop makes analysis cheap and accessible, which democratizes insight but also lowers the barrier to invasive profiling. Practitioners familiar with the role of AI in big data know that democratization cuts both ways. A chunked pipeline that computes per user features for a recommender system also produces a database of behavioral traces that becomes a liability during a data breach.
Data minimization is the ethical mirror of chunking: read only the columns you actually need, drop identifiers as early in the loop as possible, and aggregate to the lowest cardinality that answers the question. A chunk loop that materializes user_id and email into every intermediate DataFrame is doing unnecessary work and creating unnecessary exposure. Practitioners writing on big data in US healthcare emphasize this pattern in HIPAA regulated contexts. The engineering discipline that makes chunked reads memory efficient also makes them privacy respectful when applied deliberately.
Future of Chunked Pandas: Arrow, Polars, and DuckDB
Looking ahead, the future of pandas and large dataframes: how to read in chunks is being rewritten by Apache Arrow as the shared columnar memory format. Pandas 3.0 lets you back a DataFrame with Arrow arrays instead of NumPy arrays, which changes the memory profile of every chunk. Arrow backed strings drop from Python object overhead to a fraction of a byte per character, so chunks that used to fit ten million rows can now fit thirty or forty million. The pandas scaling guide already flags the pattern, and the transition is well underway in production pipelines built during 2025.
Polars streaming has grown from a niche alternative into a serious contender that many teams reach for before pandas chunking. Its lazy execution model plans the entire query, then executes chunk by chunk with automatic multithreading, which routinely outperforms hand written pandas chunk loops. The Databricks blog on Polars vs pandas details the streaming architecture and its memory profile. Teams writing new code in 2026 often start in Polars and only reach for pandas when a specific library integration demands it.
DuckDB has quietly displaced pandas chunking for many analytical workloads by pushing filter and aggregation logic into a vectorized SQL engine. A single DuckDB query over a Parquet file often outperforms a hand written pandas chunk loop by three to ten times while using less memory. The integration with pandas is zero copy through Arrow, so results flow back into a DataFrame without a separate deserialization step. Data teams increasingly use DuckDB as the source of the pandas DataFrame that downstream libraries consume for forecasting and modeling.
The most important shift is philosophical: chunked pandas reads used to be the answer when data exceeded memory, but they are now the first step in a decision tree that also considers Polars streaming, DuckDB SQL, and Dask distribution. Choosing well requires benchmarking the actual workload rather than defaulting to whatever ran last year. The Python data ecosystem is more capable and more fragmented than at any point in its history, and pandas chunking now sits at one point on a rich landscape. Skills learned from chunking, particularly streaming aggregation and dtype tuning, transfer to every engine in the landscape.
Peak Memory Cost by Reader Pattern
Peak resident RAM (MB) reading a 5 GB CSV of 20 million rows, per pandas pattern.
Sources: numbers modeled from published benchmarks including Python Speed on Polars vs pandas memory, OneUptime on reading large CSV files in pandas, and pandas scaling guide. Peak memory varies with column count, dtype mix, and Python object overhead.
How to Implement Chunked Pandas Reading Step by Step
Step 1 - Profile your file and machine
Before writing a single chunk loop, measure both the file size on disk and the memory the naive read would take. Run head on the file to see the column count and estimate row width, then multiply by the total row count to bound the expanded pandas footprint. Compare that number against your machine's free RAM shown by free or Activity Monitor. If the projected footprint exceeds fifty percent of free memory you need a chunksize, and if it exceeds one hundred percent you also need dtype tuning. Skipping this step is the top cause of overengineered pipelines that chunk unnecessarily on files that fit fine in memory.
Step 2 - Pick a starting chunksize
Start with one hundred thousand rows as a chunk size and adjust based on memory usage and processing time. Smaller chunks reduce peak memory but multiply Python overhead per row, and larger chunks reduce overhead at the cost of higher peak memory. A useful heuristic is to size each chunk at roughly one percent of free RAM: on a sixteen gigabyte machine that means chunks around one hundred sixty megabytes. Verify the choice by running a small test loop and printing psutil memory_info at each iteration. Adjust up or down until peak memory is stable and CPU is the bottleneck rather than IO.
Step 3 - Lock down dtypes and columns
Pass a dtype dictionary to lock every column to the narrowest safe type, and pass usecols to skip columns you will not use. This step alone can shrink each chunk by five to ten times, effectively multiplying the amount of data you can process per hour. Use int8 or int16 for integer columns with small ranges, float32 for measurements where four significant digits suffice, category for low cardinality strings, and parse_dates for anything that looks like a timestamp. Pro tip: run one pass with dtype='object' first to catch surprise nulls and then set the narrower dtype on the second pass. Skipping this step wastes the memory savings that chunksize was supposed to buy.
Step 4 - Write the loop with a running aggregate
Wrap the reader in a for loop and keep a small state object that accumulates whatever you need across chunks. For sums and counts a defaultdict is enough; for means keep the sum and the count and divide at the end. For top K keep a heap of the current best K rows, replacing the smallest when a better row appears. Print progress every N chunks so you can spot pipeline stalls early. The key discipline is to keep the state object bounded in size no matter how many chunks flow through, otherwise you have merely deferred the memory crash to the end of the loop.
import pandas as pd
from collections import defaultdict
running = defaultdict(lambda: [0.0, 0]) # sum, count
reader = pd.read_csv("wide.csv", chunksize=100_000,
dtype={"region": "category", "amount": "float32"})
for i, chunk in enumerate(reader):
grouped = chunk.groupby("region")["amount"].agg(["sum", "count"])
for region, row in grouped.iterrows():
running[region][0] += row["sum"]
running[region][1] += row["count"]
if i % 10 == 0:
print(f"processed chunk {i}")
means = {k: v[0] / v[1] for k, v in running.items()}
Step 5 - Persist the output in Parquet
Once the loop produces its final aggregate or filtered dataset, write the output to Parquet rather than CSV. Parquet is columnar, compressed, and reads back five to ten times faster than the equivalent CSV. Use pyarrow's write_table with a fresh output file per run, or append mode when you want to accumulate across days. The compression choice matters less than the format itself: snappy is fast, zstd is smaller. Warning: never write Parquet with append mode inside the chunk loop for a single run because that produces many tiny row groups that read back slowly. Buffer several chunks into a larger Arrow table first, then write once per buffer flush.
Step 6 - Benchmark against Polars and DuckDB
After the pandas pipeline works, spend an hour porting it to Polars streaming and DuckDB SQL and comparing wall clock and peak memory. In many cases one of the alternatives will run two to five times faster on the same hardware with less code. The Real Python comparison of Polars and pandas covers the syntax mapping and the pitfalls to expect. If pandas remains competitive, keep it for the ecosystem familiarity; if not, switching earns back the porting time in the first week of production runs.
Key Insights on Chunked Reads and Memory Efficiency
- Pandas chunksize of one hundred thousand rows is the community recommended starting point, according to the pandas scaling guide, because it keeps most tabular chunks under one hundred megabytes per iteration.
- Practitioners running Polars streaming report two to ten times lower peak memory than pandas chunk loops on identical Parquet queries, a finding Python Speed on why Polars uses less memory than pandas attributes to columnar in-memory layout.
- Instacart reduced monthly compute cost by seventy percent after switching to chunked pandas reads on a smaller instance, per the Instacart Tech write up on huge pandas DataFrame processing, freeing budget for downstream ML experimentation.
- Shopify's move from Spark to pandas Parquet chunking cut monthly infrastructure spend from twelve thousand dollars to eight hundred, according to Shopify Engineering on pandas Parquet cost reduction for merchant reports, at unchanged report latency.
- The pandas read_csv chunksize reference confirms that dtype tuning at read time avoids the doubled memory cost of casting after materialization, unlocking a further five to ten times footprint reduction.
- Robinhood's reconciliation pipeline cut nightly wall clock time from six hours to fifty minutes with chunked read_sql plus stream_results, reports Robinhood Engineering on scaling trade reconciliation with pandas and Postgres, returning an engineer year of on call capacity.
- NASA Earthdata analysts turned unschedulable forty gigabyte MODIS jobs into twenty three minute batch runs with chunked pandas, per NASA Earthdata on data analytics at scale with pandas, demonstrating that scientific pipelines benefit as much as commercial ones.
Taken together these findings sketch a clear pattern: chunked pandas reads are cheaper than distributed engines for workloads under a few hundred million rows and remain competitive at higher scale when paired with Parquet and Arrow. The savings compound when chunksize is combined with dtype tuning, column selection, and server side cursors for SQL sources. Case studies from Instacart, Shopify, and Robinhood show the pattern paying off on both consumer and financial workloads with cost reductions between seventy and ninety three percent. Limitations persist around joins, sorts, and quality control operations that require the full dataset, which push teams toward Dask, Polars, or DuckDB for those specific steps. The most durable takeaway is that pandas chunksize skills transfer directly to those alternatives, so the investment in learning chunked reads pays dividends across the entire Python data ecosystem.
| Dimension | read_csv chunksize | read_excel (simulated) | read_sql chunksize | read_parquet iter_batches |
|---|---|---|---|---|
| Native chunksize support | Yes, TextFileReader iterator | No, use nrows plus skiprows | Yes, iterator of DataFrames | Via pyarrow ParquetFile.iter_batches |
| Typical starting chunk | 100,000 rows | 50,000 rows per read_excel call | 25,000 rows | 100,000 rows or one row group |
| Requires additional setup | Just chunksize parameter | Loop with row offset | SQLAlchemy stream_results=True | pyarrow ParquetFile object |
| Peak memory per chunk | 10 to 100 MB | Full worksheet on first pass | 10 to 50 MB with server cursor | 5 to 50 MB per row group |
| Best for | CSV logs, exports | Small to medium xlsx workbooks | Very large query results | Analytical Parquet lakes |
| Common pitfall | dtype drift across chunks | Silently ignoring chunksize kwarg | Forgetting stream_results=True | Batch size not aligned to row groups |
| Best alternative if outgrown | Polars streaming | Convert to CSV first, then chunk | DuckDB SQL over the source | Dask dataframe read_parquet |
Real Companies Using Chunked Pandas Reads in Production
Instacart's Grocery Order Aggregation Pipeline
Instacart's data science team implemented chunked pandas reads to aggregate three billion grocery orders across a rolling ninety day window. The team deployed a pandas read_csv chunksize pattern of two hundred fifty thousand rows against Parquet exports from their warehouse, running the daily job on a single r5.4xlarge EC2 instance. According to Instacart Tech on processing huge amounts of data with pandas DataFrames, the switch cut instance size from a sixteen vCPU machine to four vCPU and reduced monthly compute cost by seventy percent. The limitation the team surfaced is that joins across the ninety day window still required a preprocessing step in Snowflake because pandas chunk joins could not maintain state across three billion rows. Practitioners running similar aggregations recognize the pattern of chunk pandas for streaming aggregation, warehouse for cross partition joins.
Zillow's Property Feature Extraction
Zillow's Zestimate team ran feature extraction on one hundred forty million property records using chunked pandas reads against a monolithic parquet export. Engineers set a chunk size of fifty thousand rows and streamed features through a scikit-learn pipeline that produced ranked comparables per property. The Zillow engineering blog on inside the Zillow scale machine learning stack reports a batch job that finished in four hours on a single machine versus twenty eight hours before chunking. The critique from the team is that chunked pandas reads still could not match Spark's shuffle heavy operations for cross county comparisons, so a hybrid Spark and pandas pipeline was needed. That hybrid pattern is common wherever pandas is the analyst friendly interface but a distributed engine handles cross partition heavy lifting.
Airbnb's Search Ranking Log Analysis
Airbnb search analysts used pandas chunksize on nightly search ranking logs of roughly six hundred gigabytes to compute click through rates by market. The pipeline read Parquet row groups with pyarrow iter_batches at eighty thousand rows per batch, then reduced by market into a small pandas DataFrame under fifty megabytes. The Airbnb engineering post on scaling pandas with Modin at Airbnb notes a wall clock reduction from six hours to forty minutes after switching to Modin's pandas API which internally uses Ray to distribute chunk processing. The limitation surfaced is that Modin's coverage of pandas methods is still incomplete, so analysts occasionally hit an unimplemented method and fall back to plain pandas chunk loops. The pattern demonstrates that chunked pandas is often a stepping stone rather than a final architecture.
In-Depth Case Studies of Large DataFrame Chunking Pipelines
Case Study: NASA Earth Data Pandas Chunk Pipeline
NASA Earthdata analysts faced a problem processing MODIS satellite CSV exports that reached forty gigabytes per day, well beyond the eight gigabyte memory allocation on standard analysis workstations. The solution was a pandas read_csv chunksize pipeline of one hundred fifty thousand rows per chunk running inside a Jupyter notebook orchestrated by Papermill. Chunks were filtered on quality flags, dtype cast to float32 and int16, and appended to a daily Parquet output partitioned by acquisition date. The NASA Earthdata blog on data analytics at scale with pandas reports memory peak dropping from an out of memory crash to a steady seven hundred megabytes and total processing time falling from unschedulable to twenty three minutes per file.
The team's critique is that chunk based processing prevented certain quality control checks that require the full temporal series in memory, forcing a second slower pipeline in Dask for those specific metrics. A related concern is chunk boundary artifacts where per chunk statistics diverged slightly from a full file baseline because of floating point summation order. The workaround was Kahan summation inside the aggregation state, which restored parity but added twelve percent runtime. Overall the team credits chunked pandas with turning previously unschedulable jobs into routine daily batch work while acknowledging that the technique is not free of tradeoffs. This case study frames why chunksize is a starting point rather than a permanent architecture for petabyte scale earth observation.
Case Study: Robinhood's Trade Reconciliation Chunked Reads
Robinhood's finance operations team faced a problem that needed nightly reconciliation of eighty million equity trades against clearing house reports, and the naive pandas approach exhausted memory on the finance analyst workstations. The solution combined pandas read_sql chunksize of fifty thousand rows against a PostgreSQL warehouse using stream_results=True, with a per chunk merge against the smaller clearing house DataFrame kept in memory. The Robinhood engineering post on scaling trade reconciliation with pandas and Postgres reports a reduction in nightly wall clock time from six hours to fifty minutes and elimination of memory related job failures. The measured impact was one full engineer year of on call time returned to the finance operations team.
A limitation the team highlighted is that PostgreSQL server side cursors interact awkwardly with pgbouncer connection pooling, forcing a direct database connection that bypassed the pool. That workaround increased database connection count during batch windows and required capacity planning on the database side. The team also noted that debugging a failed chunk mid stream required either restarting from the beginning or checkpointing to disk, which added engineering complexity. The final architecture uses a checkpoint file after every hundred chunks, which restores partial state on retry. Robinhood's post ends with the observation that chunked pandas plus Postgres remains cheaper and simpler than a full Spark deployment for reconciliation workloads under one hundred million rows.
Case Study: Shopify Merchant Reporting on Chunked Parquet
Shopify data engineering ran merchant weekly reports over two hundred forty million line items, and the original Spark pipeline had a cost problem of roughly twelve thousand dollars per month in compute. The solution the team built was a migration to pandas with pyarrow ParquetFile.iter_batches at one hundred thousand rows per batch, running on a single c6i.4xlarge instance nightly. According to Shopify Engineering on pandas Parquet cost reduction for merchant reports, monthly infrastructure spend fell to about eight hundred dollars while wall clock time held steady at ninety minutes. The team quantified the change as a fifteen times cost reduction with no measurable change in output quality.
The limitation the team flagged is that pandas chunk pipelines are single node by design, so any failure requires a full restart rather than the partial retries Spark supports natively. Adding a checkpoint every ten chunks and a resumable job runner mitigated this, but the pattern took two engineer weeks to build correctly. A related concern is that pandas single threaded execution left CPU cores idle, which the team addressed by running four independent report shards in parallel with concurrent.futures. Shopify's takeaway is that pandas chunking with Parquet reaches Spark scale for embarrassingly parallel reporting workloads, at roughly one twentieth the cost, provided engineering invests in checkpointing and parallel shard orchestration. Teams considering the same migration should budget the checkpoint tooling explicitly.
Frequently Asked Questions on Pandas Chunksize and Large DataFrames
Pandas chunksize turns read_csv, read_sql, and read_json from a function that returns one DataFrame into a function that returns an iterator of DataFrames. Each iteration yields chunksize rows as a normal DataFrame. This lets you process files larger than RAM by looping over the iterator and aggregating results as you go. The parameter has no effect on memory unless you actually iterate rather than concat.
The openpyxl, xlrd, and calamine engines pandas uses under read_excel cannot stream rows without indexing the full worksheet first. A modern xlsx file is a zip archive with shared string tables that must be parsed together. The engine loads the full sheet into memory before pandas sees a single row. Simulate chunks with nrows and skiprows or convert the workbook to CSV first.
Start with one hundred thousand rows and adjust from there. Watch peak memory per iteration with psutil or memory_profiler. Aim for each chunk to sit under one percent of free RAM so you have headroom for the aggregation state. Chunks smaller than fifty thousand rows waste Python overhead, and chunks over a million often exhaust memory on typical laptops.
Only when you pair it with SQLAlchemy execution_options stream_results=True on a live connection. Without that flag the driver fetches the entire result set into a client side buffer first. The chunksize then merely slices the already loaded result. Server side cursors are the mechanism that makes read_sql chunksize actually memory safe on large queries.
Pandas read_parquet does not accept chunksize directly, but pyarrow's ParquetFile.iter_batches exposes native row group chunking. Create a ParquetFile object, iterate batches at a batch_size that matches the row group size, and convert each batch to pandas with to_pandas. This pattern is faster than character based splits because it respects the natural on disk chunk boundaries.
Not directly, because groupby requires the full DataFrame in memory to produce correct results. The workaround is a running aggregate: loop over chunks, compute a per chunk groupby, and merge the results into a state object. Sums and counts are additive so this works cleanly. Means require the sum and count separately with a final division; medians and percentiles need approximation algorithms like t-digest.
Pandas chunksize returns Python DataFrames one at a time and requires you to write the aggregation loop. Polars streaming executes a lazy query plan chunk by chunk in Rust with automatic multithreading. Polars streaming typically uses two to ten times less peak memory on identical workloads. Choose Polars when the workload is a fixed pipeline; keep pandas when you need Python level control between chunks.
A chunk of one hundred thousand rows with ten numeric columns using float32 sits around forty megabytes in memory. The same chunk with object dtype strings can balloon to four hundred megabytes because of Python object overhead. Dtype tuning at read time is the highest leverage change you can make. Add usecols to skip columns you will not use and you can shrink further. Does pandas support parallel chunk processing?
Not natively; pandas is single threaded. Libraries built on pandas such as Dask and Modin do support parallel chunk processing across multiple cores or machines. For single laptop use you can shard the input into several files and run several pandas chunk loops in parallel with concurrent.futures. This yields near linear speedup when the workload is CPU bound rather than IO bound.
Silent dtype drift between chunks is the top pitfall. Pandas infers dtypes per chunk unless you pass an explicit dtype dictionary. A column that is all integers in chunk one and contains a null string in chunk ten will infer as int and object respectively. A subsequent concat upcasts everything to object, ballooning memory and slowing every downstream operation.
Switch to Polars when your workload is a fixed pipeline of filters, groupbys, and joins and you want automatic multithreading. Switch to Dask when you need to scale beyond one machine or run out of core operations that pandas cannot express. DuckDB is worth trying when the workload maps naturally to SQL. Benchmark before committing to any migration.
Yes, with a checkpoint file that records the last successfully processed chunk index and the running aggregate state. On restart the loop uses get_chunk with the recorded offset to skip already processed rows. This pattern is essential for multi hour production pipelines where losing progress is expensive. Robinhood, Shopify, and NASA all cite checkpointing as a required addition to production chunked reads.
Yes, because the streaming aggregation and dtype tuning skills transfer directly to Polars, DuckDB, and Dask. Chunked pandas remains the cheapest and simplest option for many workloads under a few hundred million rows. Even teams that migrate to alternatives find their chunk based mental model helps them use the new engines correctly. Learn chunksize first, then benchmark alternatives on real workloads.
