How to Read a File Line by Line in Python

Tested on Python 3.12.13 on Red Hat Enterprise Linux 10.2 (Coughlan)
Package python3 3.12.13
Applies to Linux, macOS, Windows
Privilege Normal user with read access to the file
Scope Reading text files line by line with open(), readline(), readlines(), and file iteration; newlines, line numbers, filtering, encodings, gzip logs, and large-file memory behavior. Does not cover binary file parsing or writing files.
Short on time?Beta Get the gist of this 14-minute read in seconds, then ask follow-up questions

In this article we will read a text file line by line in Python using the built-in open() function, starting with the pattern most scripts should use: iterate directly over the file object inside a with block.

Reading one line at a time is common when processing logs, configuration files, exports, and other large text files. To read a large file line by line in Python without loading the entire file into memory, iterate over the file object inside a with block instead of calling read() or readlines(). Fixed-width binary layouts (headers, integers, floats) belong in the Python struct module guide, not in a line loop.


Quick answer: read a file line by line in Python

Open the path with encoding="utf-8", then loop over the file object. Python reads ahead in a buffer, but it does not build a list of every line for you.

python
with open("example.txt", encoding="utf-8") as file:
    for line in file:
        print(line, end="")

Suppose example.txt contains:

text
Ubuntu
RHEL
Rocky Linux
Python

The loop prints those four lines exactly as stored. The with statement closes the file when the block ends. Each line still includes its newline character, so print(line, end="") avoids a blank line between rows. To write data instead, see write to file in Python.


read() vs readline() vs readlines() vs file iteration

All four methods sit on the same text-mode file object. They differ in how much they return at once and how much memory your script holds while you work.

Method Result Memory behavior Use when
read() Whole file as one string Entire content in RAM Small, bounded files
readline() Next line (or "" at EOF) One line at a time Manual control, headers, protocols
readlines() List of lines Entire list in RAM Small file and you need a list
file.read().splitlines() List without line endings Non-streaming; whole content is read before splitting Small files when you need all lines without terminators
for line in file Iterator over lines Buffered streaming Default line-by-line processing

For normal text processing, iterate directly over the file. Reserve readlines() and splitlines() for files that comfortably fit in memory. Neither is a streaming technique because both need the full text before the list exists.


Method 1: iterate directly over the file

This is the Pythonic default. The file object is an iterator: each pass through the loop asks the interpreter for the next line until end-of-file.

python
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "servers.txt"
    path.write_text(
        "web01.prod.example\nweb02.prod.example\ndb01.prod.example\n",
        encoding="utf-8",
    )
    with path.open(encoding="utf-8") as file:
        for line in file:
            print(line, end="")

That prints three hostnames, one per line. Nothing copies the full file into a list first, so memory stays roughly flat even when the path is large. You only pay for the current line string plus Python's read buffer.

A file that omits the final newline still works. If example.txt contains first\nsecond with no \n after second:

python
with open("example.txt", encoding="utf-8") as file:
    for line in file:
        print(repr(line))

The loop prints 'first\n' and then 'second'. Python still yields the final line even when the file does not end with a newline.


Remove the newline without removing other whitespace

Text-mode reads include the line ending. With Python's default universal-newline handling in text mode, \r\n and \r input line endings are translated to \n, so removing the terminator while preserving other whitespace is straightforward.

python
line = "    indented configuration\n"
print(repr(line.removesuffix("\n")))  # preferred: drops only the line ending
print(repr(line.strip()))             # removes leading and trailing whitespace
print(repr(line.rstrip()))            # removes trailing whitespace, not just \n

For indented config or code, strip() can erase meaningful leading spaces. Prefer line.removesuffix("\n") when you only want to drop the line terminator. line.rstrip("\n") works the same way once universal newlines have normalized the ending. The Python strip() guide walks through all three trimming methods.

If the goal is clean console output and the stored newline is fine, print(line, end="") is enough.


Read a file with line numbers

Wrap the file iterator with enumerate() and start at 1 when humans count lines from one.

python
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "app.log"
    path.write_text(
        "2026-09-11 10:01:10 INFO Service started\n"
        "2026-09-11 10:02:21 ERROR Database unavailable\n"
        "invalid line here\n"
        "2026-09-11 10:03:17 INFO Retrying\n",
        encoding="utf-8",
    )
    with path.open(encoding="utf-8") as file:
        for line_number, line in enumerate(file, start=1):
            if "invalid" in line:
                print(f"Invalid config on line {line_number}: {line.removesuffix('\n')}")

That reports Invalid config on line 3: invalid line here. Pairing enumerate() with streaming reads is how you cite exact line numbers in error reports without loading the file twice. See Python enumerate() for more counting patterns.


Method 2: read one line at a time with readline()

readline() returns the next line including its newline, or "" when the stream hits end-of-file.

python
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "example.txt"
    path.write_text("alpha\n\nbeta\n", encoding="utf-8")
    with path.open(encoding="utf-8") as file:
        while True:
            line = file.readline()
            if line == "":
                break
            print(repr(line))

Running that loop on alpha, a blank row, and beta prints repr() values in order: 'alpha\n', then '\n', then 'beta\n'. A blank line in the file is normally "\n", not "". Only end-of-file returns the empty string, so you can tell EOF apart from an empty row.

When readline() is better than a for loop

Use readline() when your program needs to decide what to do after each individual line:

  • Read a header row, then iterate the body (common in CSV-like formats; for real CSV use the csv module).
  • Consume exactly one logical line before deciding what to read next.
  • Interleave file input with another stream where your code must handle one line at a time.

readline() is not faster than for line in file for everyday scans. Both use the same buffered reader underneath, which may read ahead internally even though your code consumes one logical line per call.


Method 3: use readlines() when you actually need a list

readlines() reads from the current position through EOF and returns a list of strings. To persist a list of lines you built in memory, the write-side mirror is writelines()—see Python writelines().

python
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "small.txt"
    path.write_text("one\n two\n three\n", encoding="utf-8")
    with path.open(encoding="utf-8") as file:
        lines = file.readlines()
    print(lines)

That prints ['one\n', ' two\n', ' three\n']. Each element keeps its trailing newline. This is fine for small files when you plan to index, sort, or pass the full list to another function. It is a poor default for multi-gigabyte logs because it builds and retains a list containing all remaining lines in memory.


Read a large file line by line without loading it into memory

The streaming pattern is the same loop you already saw:

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

Python's buffered text I/O reads chunks from the OS and splits them into lines. The interpreter does not allocate a list of the whole file for a for loop.

There are two important limits to this memory advantage:

  • Memory still depends on the current line and what your process() function stores. If you append every line to a growing list, you lose the streaming benefit.
  • A file with one enormous line (no newline for megabytes) can still force a large single string into RAM. Line-by-line reading assumes reasonable line lengths.

Benchmark: file iterator vs readlines()

I generated a 26.2 MiB fixture (500,000 lines) and compared both approaches in separate subprocesses so each run starts with a clean process and RSS measurement.

Setting Value
Python 3.12.13
OS Linux (RHEL 10.2)
CPU cores 3
File size 26.2 MiB (27,500,000 bytes)
Line count 500,000
Runs per mode 5 (median reported)

Create the fixture once, then benchmark each mode in its own process with /usr/bin/time:

bash
python3 -c "
LINE_COUNT = 500_000
LINE_TEXT = '2026-09-11 10:01:10 INFO sample log line for benchmark\n'
with open('large.log', 'w', encoding='utf-8') as f:
    for _ in range(LINE_COUNT):
        f.write(LINE_TEXT)
"

bench.py runs one mode per invocation:

python
import sys

FILE_PATH = "large.log"

def bench_iterate():
    count = 0
    with open(FILE_PATH, encoding="utf-8") as file:
        for line in file:
            count += 1
    return count

def bench_readlines():
    with open(FILE_PATH, encoding="utf-8") as file:
        lines = file.readlines()
    return len(lines)

if __name__ == "__main__":
    mode = sys.argv[1]
    if mode == "iterate":
        print(bench_iterate())
    else:
        print(bench_readlines())

Run each mode five times and read elapsed wall time plus maximum resident set size from time:

bash
/usr/bin/time -f 'elapsed=%e maxrss_kb=%M' python3 bench.py iterate
/usr/bin/time -f 'elapsed=%e maxrss_kb=%M' python3 bench.py readlines

Median results on the lab host:

Mode Median elapsed Median max RSS
for line in file 0.26 s 8.7 MiB
readlines() 0.31 s 58.4 MiB

readlines() held about seven times more resident memory here because it builds the full list. Elapsed time was close on this small fixture, but for large logs the iterator is the safer default; wall-clock differences are usually smaller than the memory gap once files grow.


Read only lines that match a condition

Filter inside the loop so you never build a full list of non-matching rows.

python
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "app.log"
    path.write_text(
        "2026-09-11 10:01:10 INFO Service started\n"
        "2026-09-11 10:02:21 ERROR Database unavailable\n"
        "2026-09-11 10:03:17 INFO Retrying\n",
        encoding="utf-8",
    )
    with path.open(encoding="utf-8") as file:
        for line in file:
            if "ERROR" in line:
                print(line, end="")

That prints only the ERROR row. For stricter matching, compile a regular expression once outside the loop and call .search(line) per row.


Skip empty lines

Decide what "empty" means before you filter.

If whitespace-only lines should count as empty:

python
with open("sparse.txt", encoding="utf-8") as file:
    for line in file:
        if not line.strip():
            continue
        process(line)

If only newline-only records should be skipped, test the line directly:

python
with open("sparse.txt", encoding="utf-8") as file:
    for line in file:
        if line == "\n":
            continue
        process(line)

The first version drops lines that contain only spaces or tabs. The second keeps " \n" as real content. Pick the rule that matches your file format.


Stop reading when you find a match

Streaming lets you exit early without scanning the rest of a huge file.

python
import tempfile
from pathlib import Path

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "fatal.log"
    path.write_text(
        "INFO ok\nWARN slow\nFATAL disk full\nINFO ignored\n",
        encoding="utf-8",
    )
    with path.open(encoding="utf-8") as file:
        for line_number, line in enumerate(file, 1):
            if "FATAL" in line:
                print(f"Found on line {line_number}")
                break

That stops at the first FATAL line and never reads INFO ignored. Use break when the task is "find first match" rather than "summarize entire file".


Handle file encodings correctly

Pass encoding="utf-8" on text files unless you know the source uses something else. Python 3 defaults to a locale-specific encoding when encoding is omitted, which breaks on machines with different settings.

python
with open("data.txt", encoding="utf-8") as file:
    for line in file:
        process(line)

If bytes are not valid UTF-8, open() raises UnicodeDecodeError. If invalid bytes should be preserved visibly rather than stopping the read, errors="replace" substitutes the Unicode replacement character:

python
with open("data.txt", encoding="utf-8", errors="replace") as file:
    for line in file:
        process(line)

UTF-8 files exported from Windows editors sometimes start with a byte-order mark. Use encoding="utf-8-sig" so the BOM is stripped from the first line:

python
with open("data.txt", encoding="utf-8-sig") as file:
    for line in file:
        process(line)

Avoid errors="ignore" as a default: it silently drops bytes and can merge lines without warning.


Handle file reading errors

Catch the exceptions you can recover from instead of a broad except Exception.

python
import sys

def process(line):
    pass

try:
    with open("data.txt", encoding="utf-8") as file:
        for line in file:
            process(line)
except FileNotFoundError:
    print("Path does not exist", file=sys.stderr)
except PermissionError:
    print("Read permission denied", file=sys.stderr)
except UnicodeDecodeError as exc:
    print(f"Encoding failure at byte {exc.start}: {exc.reason}", file=sys.stderr)

FileNotFoundError means the path is missing. PermissionError means the OS blocked the read. UnicodeDecodeError means the bytes do not match the declared encoding. See FileNotFoundError in Python and try / except for fuller patterns.


Read a file line by line with pathlib

Path.open() is the same open() call with a pathlib entry point.

python
from pathlib import Path

path = Path("app.log")
with path.open(encoding="utf-8") as file:
    for line in file:
        process(line)

Path.open() provides the same streaming behavior through a Path object. Do not use path.read_text().splitlines() on very large files because read_text() loads the entire contents into one string first. If the path may not exist, handle FileNotFoundError when opening it rather than relying on a separate existence check.


Read a gzip file line by line

Compressed logs use the same iterator pattern on a text wrapper around gzip.

python
import gzip

with gzip.open("app.log.gz", "rt", encoding="utf-8") as file:
    for line in file:
        process(line)

Mode "rt" decompresses to text. gzip.open() decompresses incrementally, so iterating over it does not require loading the complete uncompressed log into memory.


Practical example: process a large log file

This mini-script streams a log, counts ERROR rows, prints them with line numbers, and reports the total.

python
import tempfile
from pathlib import Path

LOG = """2026-09-11 10:01:10 INFO Service started
2026-09-11 10:02:21 ERROR Database unavailable
2026-09-11 10:03:17 INFO Retrying
2026-09-11 10:03:22 ERROR Connection timed out
"""

def scan_log(path):
    error_count = 0
    with path.open(encoding="utf-8") as file:
        for line_number, line in enumerate(file, start=1):
            if "ERROR" in line:
                error_count += 1
                print(f"{line_number}: {line.rstrip()}")
    return error_count

with tempfile.TemporaryDirectory() as tmp:
    path = Path(tmp) / "app.log"
    path.write_text(LOG, encoding="utf-8")
    total = scan_log(path)
    print(f"Total ERROR records: {total}")

The scanner prints 2: 2026-09-11 10:02:21 ERROR Database unavailable, then 4: 2026-09-11 10:03:22 ERROR Connection timed out, and finishes with Total ERROR records: 2. It keeps only a counter in memory, not a list of every match. If you need to retain matching lines, memory then scales with the number and size of those matches.


Common mistakes when reading files line by line

Symptom Likely cause Fix
Memory spikes on a huge log readlines() or read() Use for line in file
Config indentation vanishes line.strip() on every row Use line.removesuffix("\n")
Huge file loaded at once read_text().splitlines() Use for line in path.open()
UnicodeDecodeError on Linux Omitted encoding= Pass encoding="utf-8" or the real codec
File handle leak open() without with Use with open(...) as file
RAM still grows while "streaming" Appending every line to a list Store only matches or aggregates
Blank line treated as EOF Confusing "" with "\n" readline() returns "" only at EOF
Double spacing in output print(line) on lines that already end with \n Use print(line, end="")
One giant line blows memory No newlines in source Split on a delimiter or use a binary chunk reader

Which method should you use?

Requirement Recommended method
Process normal text line by line for line in file
Very large file for line in file
Need line numbers enumerate(file, 1)
Need manual one-line control readline()
Need all lines in memory readlines()
Need gzip log gzip.open() + iteration
Need Path object Path.open() + iteration

Summary

Reading a text file line by line in Python usually means opening the path with encoding="utf-8" and looping for line in file inside a with block. That pattern streams through buffered I/O without building a list of every row, which is what you want for logs and exports that do not fit comfortably in RAM.

readline() still matters when you must read a header separately or stop after an exact number of lines. readlines() and read().splitlines() are fine when the file is small and you genuinely need every line in a list. On a 500,000-line lab fixture measured in separate subprocesses, iteration peaked near 8.7 MiB RSS while readlines() peaked near 58 MiB.

Watch newlines and encodings as closely as you watch the loop itself. Keep meaningful indentation with removesuffix("\n"), pick utf-8-sig when a BOM is present, and catch FileNotFoundError, PermissionError, and UnicodeDecodeError on paths you do not fully control. When the job is grep-style filtering or early exit on a match, stay in the same streaming loop and avoid collecting lines you will throw away.


References


Frequently Asked Questions

What is the best way to read a file line by line in Python?

Open the file in text mode and iterate: for line in file. Use a with block so the file closes automatically, and pass encoding="utf-8" unless you know the file uses another encoding.

Does for line in file load the whole file into memory?

No. Python reads through a buffer and yields one line at a time. Memory can still grow if your code stores every line in a list or if a single line is extremely long.

What is the difference between readline() and readlines()?

readline() returns the next line (or "" at EOF). readlines() reads everything from the current position to the end and returns a list of all remaining lines.

How do I remove \n from each line?

Use line.removesuffix("\n") when leading spaces matter. Use line.strip() only when you want to remove all leading and trailing whitespace.

How do I read a 1 GB or 10 GB file in Python?

Use for line in file with encoding="utf-8". Process or filter each line inside the loop and avoid read() or readlines() unless you accept the memory cost.

How do I get the line number while reading?

Use enumerate(file, start=1) in the loop: for line_number, line in enumerate(file, start=1).

What is the difference between readlines() and splitlines()?

readlines() reads from the file object and returns a list of lines that still include their newline characters. file.read().splitlines() reads the entire file into one string first, then splits it into lines without terminators. Neither approach streams a large file.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)