Addaly is in open beta. Things will change, and AI answers can be wrong — check anything that matters.

Python, From Zero, For AI

From your first line of code to your first API call.

Lesson 45 of 898 min

Files larger than memory

The line that kills the process

python
data = open("events.jsonl").read()          # a 6 GB string
rows = list(csv.DictReader(f))              # 40 million dictionaries

Both load the whole file into memory. On a laptop with 8 GB of RAM the first raises MemoryError if you are lucky, and if you are not, the operating system starts swapping and the machine becomes unusable for ten minutes.

A Python object costs far more than the bytes it represents. A dictionary with five short string keys is roughly 300–400 bytes; the same row in the file might be 60. Loading a 1 GB CSV into a list of dictionaries can need 6–8 GB.

Counting the rows of a two-gigabyte CSVlist(csv.DictReader(f))12000f.readlines()3400f.read()2050for line in f6peak memory in megabytesThe file is the same size in all four. What differs is how much of it exists as Python objects atonce: a dict with five short keys is 300 to 400 bytes for maybe 60 bytes of data. Looping over thefile object holds one line, whatever the file size, which is why it is the only one that still worksat 40 GB.
Counting the rows of a two-gigabyte CSVlist(csv.DictReader(f))12000f.readlines()3400f.read()2050for line in f6peak memory in megabytesThe file is the same size in all four. What differsis how much of it exists as Python objects at once:a dict with five short keys is 300 to 400 bytes formaybe 60 bytes of data. Looping over the file objectholds one line, whatever the file size, which is whyit is the only one that still works at 40 GB.

Files are already iterators

python
with open("events.jsonl", encoding="utf-8") as f:
    for line in f:
        record = json.loads(line)
        process(record)

This holds one line at a time. The file object reads a buffer from disk, hands you lines, and discards them as you go. It works identically for a 2 KB file and a 200 GB one, and it is the default way to read text in Python for exactly that reason.

csv.DictReader and csv.reader stream the same way, as long as you do not wrap them in list().

Write your own streaming step with yield

python
def parse_events(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                yield json.loads(line)

def large_orders(events, threshold):
    for e in events:
        if e.get("amount", 0) > threshold:
            yield e

for order in large_orders(parse_events(path), 10_000):
    print(order["id"])

A function containing yield is a generator: calling it produces an object that does nothing until you loop over it, and then produces one value at a time. Nothing above holds more than one record in memory, and the three stages read as a pipeline. The generators lesson in the last module takes the mechanism apart; this is the use you will reach for first.

Two properties to know now. A generator is single-use — loop over it twice and the second loop sees nothing, with no error. And the work happens while you consume it, so an exception from line 4 million surfaces in the middle of your loop rather than at the call.

Counting without loading

python
total = sum(1 for _ in open(path, encoding="utf-8"))

from collections import Counter
counts = Counter(json.loads(l)["type"] for l in open(path, encoding="utf-8"))

Both use constant memory regardless of file size. sum, max, min, any, all and Counter all accept generators, which is what makes summary statistics on a huge file a one-liner.

Peeking at the first few

python
from itertools import islice

with open(path, encoding="utf-8") as f:
    for line in islice(f, 5):
        print(line[:200])

islice takes the first n items of any iterator without reading the rest. This is how you inspect the shape of a 40 GB file in a fraction of a second, and it is the right first move on any file you have not seen before. On the command line, head -5 file.jsonl does the same thing.

Binary files, in chunks

python
with open(src, "rb") as fin, open(dst, "wb") as fout:
    while chunk := fin.read(1024 * 1024):
        fout.write(transform(chunk))

A megabyte at a time. The := walrus operator assigns and tests in one expression, which is the tidy way to write a read-until-empty loop.

For hashing a large file, the same pattern feeds the hash incrementally, so you never hold more than a chunk.

Writing as you go

python
with open(out, "w", encoding="utf-8") as f:
    for record in transform(parse_events(path)):
        f.write(json.dumps(record, ensure_ascii=False) + "\n")

Building the whole output in a list and writing at the end doubles your memory requirement for no benefit. Write each record as it is produced. If the job dies at 80 per cent you also keep 80 per cent of the output, which the all-at-once version does not.

When streaming is the wrong answer

If you need to sort the whole file, join it against another file, or answer questions in a different order than the data is in, streaming stops helping — those operations need the data somewhere it can be indexed.

At that point the cheapest tool is already installed:

python
import sqlite3

sqlite3 ships with Python, writes a single file, handles databases far larger than memory, and gives you indexes, sorting and joins in SQL. Loading a 20 GB CSV into SQLite once and querying it is often an order of magnitude faster and simpler than any amount of clever Python. It runs on a phone, needs no server, and is the most widely deployed database in the world.

The other option is pandas with chunksize, which reads a large file in blocks — covered in the next module, where the memory arithmetic gets its own treatment.

The one thing to keep

Loop over the file object rather than reading it, chain generators for each processing step, and move to SQLite the moment you need to sort or join rather than filter.

Before you move on

A script that streams a 30 GB log with a generator pipeline is changed to `records = list(parse_events(path))` so the data can be looped over twice. It now crashes with MemoryError. What is the minimal correct fix?

Pick the one you would defend. Nobody sees your answer.

No ads. No data sale. No public scores on people. Ever.

© 2026 Addaly

Files larger than memory · Python, From Zero, For AI · Addaly