Menu
Try in Playground

Python File Handling: Read, Write, and Append Files Safely

How to read and write files in Python — the with statement, text vs binary, and the safer modern path-based API.

Files Are Just Strings With a Path

Most programs eventually need to read or write a file — configs, user data, logs, CSV exports, small caches. Python keeps this simple with a single built-in function, open(), plus a with block to make sure things close cleanly.

Let's work through the common cases.

Reading a Whole File

The simplest read:

with open("notes.txt") as f:
    contents = f.read()

print(contents)

open("notes.txt") opens the file for reading (the default). The with block means Python will close the file when you exit the block — even if an exception is raised. Once closed, you can't use f any more; the text lives in contents.

contents is a single string containing the entire file. Fine for small files; bad for a 2 GB log.

Reading Line by Line

For larger files, iterate instead of reading everything into memory:

with open("big.log") as f:
    for line in f:
        if "ERROR" in line:
            print(line.strip())

Each iteration yields one line including the trailing newline — which is why .strip() shows up so often. If you want a list of lines, f.readlines() does that, but usually the direct iteration is what you want.

Writing a File

To write, pass the mode as a second argument:

with open("output.txt", "w") as f:
    f.write("first line\n")
    f.write("second line\n")

Two things to note:

  • "w" overwrites. If output.txt already exists, its content is gone the moment open runs.
  • You have to add newlines yourself. f.write("hello") writes exactly those five characters — no trailing newline.

For appending instead of overwriting:

with open("output.txt", "a") as f:
    f.write("another line\n")

You can also feed in multiple lines at once with writelines:

lines = ["a\n", "b\n", "c\n"]
with open("letters.txt", "w") as f:
    f.writelines(lines)

Same rule: you supply the newlines.

print has a file argument, which is an easy way to write without the manual newlines:

with open("log.txt", "w") as f:
    print("started", file=f)
    print("finished", file=f)

Each print writes its arguments plus a trailing newline. For casual output, this is often the nicest form.

Text vs Binary Mode

By default, open is in text mode. Python decodes bytes to strings (using UTF-8 by default on modern platforms) and handles newline translation.

For images, PDFs, compiled files, or anything that isn't text, open in binary mode by adding "b" to the mode:

with open("image.png", "rb") as f:
    data = f.read()

print(len(data))  # number of bytes

with open("image_copy.png", "wb") as f:
    f.write(data)

In binary mode, you get bytes objects instead of str, and Python doesn't touch line endings.

Specifying Encoding

For text files, be explicit about encoding. UTF-8 is almost always right in 2026:

with open("notes.txt", encoding="utf-8") as f:
    contents = f.read()

If you're dealing with legacy files from a Windows-only shop, you might see cp1252 or latin-1 as well. Guessing wrong gives you garbled characters or a UnicodeDecodeError.

pathlib: The Modern Way to Handle Paths

The standard library's pathlib module gives you a more ergonomic API than raw strings:

from pathlib import Path

path = Path("notes.txt")

# Simple read and write.
contents = path.read_text(encoding="utf-8")
path.write_text("new contents\n", encoding="utf-8")

# Build paths without worrying about / vs \.
data_dir = Path("data")
log_path = data_dir / "today.log"

print(log_path)
print(log_path.exists())
print(log_path.suffix)       # ".log"
print(log_path.stem)         # "today"
print(log_path.parent)       # "data"

Path.read_text() and write_text() are shortcuts for the with open(...) as f pattern when you just want all-at-once reads and writes. For line-by-line iteration, you still use open or path.open().

A Small End-to-End Example

Reading a list of items from a file, filtering them, and writing the result to another file:

from pathlib import Path

source = Path("items.txt")
destination = Path("filtered.txt")

items = source.read_text().splitlines()
kept = [item for item in items if item and not item.startswith("#")]

destination.write_text("\n".join(kept) + "\n")

print(f"Kept {len(kept)} items out of {len(items)}")

splitlines() is the line-oriented companion to join. It splits on newline characters without keeping them — handy when you're going to rejoin the pieces anyway.

When Things Go Wrong

Opening a file that doesn't exist raises FileNotFoundError. Trying to read a file you don't have permission for raises PermissionError. These are both subclasses of OSError, which in turn is a subclass of Exception — we'll talk about handling exceptions properly on the next page.

For now, a quick preview of what error handling looks like:

from pathlib import Path

path = Path("maybe.txt")

try:
    contents = path.read_text()
except FileNotFoundError:
    print("File doesn't exist — starting fresh.")
    contents = ""

print(repr(contents))

Key Habits

  • Always use with open(...) instead of raw open + manual close.
  • Always pass encoding= for text files; UTF-8 by default.
  • Iterate large files line-by-line; don't .read() them whole.
  • For paths and common read/write patterns, use pathlib — it'll save you a lot of string gymnastics.

Up next: JSON. Most real-world text files you read aren't plain lines — they're structured, and JSON is the most common shape. The same with open(...) habits carry over directly.

Frequently Asked Questions

How do I read a file in Python?

Use open() with the with statement, which closes the file automatically when you're done. with open('file.txt') as f: contents = f.read() reads the whole file into a string. For line-by-line, loop over the file object directly: for line in f:.

What's the difference between open modes 'r', 'w', and 'a'?

'r' is read (the default). 'w' is write — it creates the file or truncates it if it exists. 'a' is append — it adds to the end of the file without erasing what's there. Add 'b' for binary mode or '+' for read+write.

Why should I use with open instead of plain open?

The with statement guarantees the file is closed even if an error happens partway through. Without with, you have to call .close() yourself and remember to do it in error paths. with is safer and less code.

Learn to code with Coddy

GET STARTED