Optimize File and Data Processing in Python

Learn to speed up file I/O and data processing in Python with buffering, binary modes, streaming, and memory-mapped files.

7 min read

File and data processing bottlenecks usually come from I/O, not CPU. The disk or network is slow, and your Python code spends most of its time waiting. Optimizing file processing is about reducing how much data you read and how you read it.

The single biggest mistake is loading an entire file into memory when you only need it one piece at a time. A 2 GB log file does not need to be in RAM to count error lines. Streaming processes the file in chunks and keeps memory usage constant.

Read in chunks, not all at once

Reading an entire file with f.read() is simple but dangerous for large files. Chunked reading processes a fixed-size block at a time.

pythonpython
CHUNK = 64 * 1024
 
with open("large_file.bin", "rb") as f:
    while chunk := f.read(CHUNK):
        process(chunk)

The walrus operator (:=) reads a chunk and checks if it is empty in one expression. Memory usage stays at CHUNK bytes regardless of file size.

Line-by-line for text files

Python's file object is an iterator over lines. Use it directly instead of calling readlines(), which creates a list of all lines in memory.

pythonpython
with open("large.log") as f:
    for line in f:
        if "ERROR" in line:
            print(line.rstrip())

Each line is read and discarded as the loop advances. A 10 GB log file uses memory proportional to the longest line, not the file size.

For CSV files, pass the file object to the csv module:

pythonpython
import csv
 
with open("data.csv") as f:
    reader = csv.DictReader(f)
    for row in reader:
        process(row)

Binary is faster than text

Text mode decodes bytes into strings. If you do not need decoded text, binary mode skips this step and is measurably faster.

pythonpython
# Binary: fast, no decoding.
with open("source.bin", "rb") as src, open("dest.bin", "wb") as dst:
    while chunk := src.read(64 * 1024):
        dst.write(chunk)
 
# Text: slower, decodes and encodes.
with open("source.txt") as src, open("dest.txt", "w") as dst:
    for line in src:
        dst.write(line)

For file copy operations, use shutil.copyfileobj which handles buffering efficiently. For data processing where every byte matters, binary mode is the right choice.

Buffer size matters

Python buffers I/O by default: typically 8 KB for binary files. Larger buffers mean fewer system calls, which improves throughput for large sequential reads.

pythonpython
with open("large.bin", "rb", buffering=256 * 1024) as f:
    while chunk := f.read(256 * 1024):
        process(chunk)

The buffering parameter sets the internal buffer size. Matching the read size to the buffer size avoids double-copying. For modern SSDs, 64 KB to 256 KB is a good range. Beyond that, returns diminish.

Handle formats efficiently

JSON parsing is expensive. If you process a large JSON array, do not load it all at once. Use a streaming parser like ijson.

pythonpython
import ijson
 
with open("large.json", "rb") as f:
    for record in ijson.items(f, "item"):
        process(record)

Each record is yielded one at a time. The file is never fully materialized. This works for multi-gigabyte JSON files that would crash json.load().

For tabular data, CSV is simple but slow for large datasets. If you process the same file repeatedly, consider Parquet or a database. The time spent converting formats often pays back quickly.

Use the right tool for the data shape

Not all data needs Python. When you need to filter, count, or aggregate structured text, command-line tools are faster because they are written in C and optimized over decades.

texttext
grep "ERROR" large.log | wc -l
cut -d',' -f3 data.csv | sort | uniq -c | sort -rn | head -10

When Python is the right tool, let built-in functions do the heavy work. The sum, max, min, sorted, and collections.Counter functions are implemented in C and run faster than manual loops.

pythonpython
from collections import Counter
 
with open("access.log") as f:
    ips = (line.split()[0] for line in f)
    top = Counter(ips).most_common(10)
 
print(top)

The generator expression (line.split()[0] for line in f) extracts IPs one at a time. Counter counts them in C. No intermediate list is created.

For the broader performance picture, the article on Python performance optimization covers how I/O bottlenecks fit into the profiling workflow. When processing speed is the problem rather than I/O, the article on optimizing Python loops covers the CPU side.

Rune AI

Rune AI

Key Insights

  • Read files in binary mode for raw speed; text mode adds decoding overhead.
  • Use chunked or line-by-line reading instead of loading entire files into memory.
  • Larger buffer sizes reduce system calls; 64 KB is a good starting point.
  • Streaming parsers like ijson handle JSON files too large for memory.
  • Plain Python can process gigabytes of data if you never materialize the whole dataset at once.
RunePowered by Rune AI

Frequently Asked Questions

Should I read files in text mode or binary mode for performance?

Binary mode is faster because Python skips the decoding step. Use binary mode when you are processing raw bytes, copying files, or doing operations that do not need decoded text. Use text mode when you need to work with the content as strings. If text mode is necessary, specify the encoding explicitly instead of relying on the system default.

What buffer size should I use when reading files in Python?

Python's default buffering (usually 8 KB for binary, line-buffered for text) is fine for most cases. If you are processing very large files, try larger buffers like 64 KB or 256 KB. The optimal size depends on your storage hardware. Too small means more system calls; too large wastes memory without further benefit. Experiment with your specific workload.

Conclusion

File and data processing bottlenecks are usually I/O, not CPU. Use binary mode and chunked reads for raw speed. Stream line by line for text processing. Choose the right format for the job: CSV for tables, JSON for nested data, Parquet or binary for scale. The biggest win comes from reading only what you need, when you need it, rather than loading everything into memory upfront.