Python
File Handling
Lazy Evaluation
Big Data
Iterators

Lazy Method for Reading Big File in Python?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

The lazy way to read a large file in Python is to avoid loading the whole thing at once. Instead, you iterate over the file object or read fixed-size chunks so memory stays bounded and the program can start processing data immediately.

The Simplest Lazy Pattern

For text files, the most Pythonic solution is often just iterating over the open file handle. File objects are iterators, so Python reads from the operating system as needed.

python
with open("huge.log", "r", encoding="utf-8") as handle:
    for line in handle:
        process(line)

This is already lazy. Python does not build a giant list of lines unless you ask it to. As each iteration runs, a new line is read, processed, and discarded if you do not store it.

That makes this style ideal for logs, CSV-like text, and any workflow where each line can be handled independently.

Wrapping the Logic in a Generator

If you want to reuse the reading logic, a generator is a clean way to expose lazy processing to the rest of your program.

python
1def filtered_lines(path, prefix):
2    with open(path, "r", encoding="utf-8") as handle:
3        for line in handle:
4            if line.startswith(prefix):
5                yield line.rstrip("\n")
6
7for item in filtered_lines("app.log", "ERROR"):
8    print(item)

The key point is yield. It returns one result at a time without building the full output in memory. Downstream code can consume the generator lazily too, which keeps the entire pipeline efficient.

Reading in Chunks for Binary or Structured Data

Line iteration is not always appropriate. Some files are binary, some have extremely long lines, and some need block-based parsing. In those cases, read a fixed chunk size in a loop.

python
1def read_in_chunks(path, chunk_size=1024 * 1024):
2    with open(path, "rb") as handle:
3        while True:
4            chunk = handle.read(chunk_size)
5            if not chunk:
6                break
7            yield chunk
8
9for chunk in read_in_chunks("archive.bin"):
10    process_chunk(chunk)

This pattern gives you explicit control over memory use. A one-megabyte chunk size is common, but the right number depends on file format, I/O speed, and how expensive your processing function is.

Lazy Parsing With csv

For structured text such as CSV, you can still stay lazy. The csv module reads one row at a time from the underlying file object.

python
1import csv
2
3with open("users.csv", "r", encoding="utf-8", newline="") as handle:
4    reader = csv.DictReader(handle)
5    for row in reader:
6        if row["status"] == "active":
7            print(row["email"])

This approach is better than calling list(reader) on a large file, because rows are parsed incrementally and memory does not grow with file size.

Composing Lazy Operations

Lazy file reading becomes more useful when the rest of the data flow is lazy too. Generator expressions work well here:

python
1with open("access.log", "r", encoding="utf-8") as handle:
2    errors = (
3        line for line in handle
4        if " 500 " in line
5    )
6
7    first_ten = []
8    for _, line in zip(range(10), errors):
9        first_ten.append(line.rstrip("\n"))
10
11print(first_ten)

Only as many lines as needed are read to produce the first ten matches. That is a good example of why “lazy” matters in practice. You can stop early without scanning the whole file into memory.

When readlines() Is the Wrong Choice

Beginners often reach for read() or readlines() because those functions are simple. They are fine for small files, but they materialize the entire contents:

python
with open("huge.log", "r", encoding="utf-8") as handle:
    lines = handle.readlines()

That can be expensive or completely impractical for multi-gigabyte inputs. If you only need sequential processing, iteration is both simpler and more scalable.

Common Pitfalls

One common pitfall is accidentally defeating laziness later in the pipeline. Converting a generator to list, sorting all results, or collecting every line in an array brings the memory cost back.

Another issue is forgetting encoding. Large-file processing jobs often run for a long time, so a late UnicodeDecodeError is painful. Specify the encoding when you know it, or open in binary mode when you need exact byte handling.

Very long lines can also surprise you. Line-by-line iteration is lazy, but a single gigantic line still has to be held in memory while it is processed. For unusual formats, fixed-size chunk reading may be safer.

Finally, do not assume laziness makes code automatically fast. Disk I/O, decompression, parsing, and downstream transformations still dominate performance. Lazy reading mainly keeps memory usage under control and enables streaming behavior.

Summary

  • Iterating over a file object is already a lazy pattern in Python.
  • Use generators to keep downstream processing lazy as well.
  • Read fixed-size chunks for binary data or unusual text formats.
  • Avoid read() and readlines() for very large files unless you truly need the entire contents.
  • Watch for places where later code forces everything into memory again.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.