Text and binary files

Everything in a file is stored as bytes. What differs is how a program interprets them. A text file holds characters — a note, a log, a CSV table, Python source code. A binary file holds bytes that are not meant to be read as text, such as an image, a ZIP archive or a PDF.

Python mirrors that difference. A file opened in text mode gives you str objects and translates line endings; a file opened in binary mode gives you bytes objects, exactly as stored.

Encoding

To store text as bytes, every character must be turned into one or more bytes. The rules for that are an encoding. The universal choice today is UTF-8, which can store every Unicode character: plain English letters take one byte, "é" takes two, an emoji four.

word = "café"
data = word.encode("utf-8")      # str to bytes
print(data)
print(len(word), len(data))
print(data.decode("utf-8"))      # bytes back to str
b'caf\xc3\xa9'
4 5
café

Before Python 3.15, open() without an encoding uses the locale's encoding, which on Windows is often cp1252. A file written on one computer can then be unreadable, or silently wrong, on another. Reading with the wrong encoding gives garbled text or a UnicodeDecodeError. Python 3.15 is set to make UTF-8 the default (PEP 686).

Opening a file

The built-in function open() opens a file and returns a file object: an object with methods such as read() and write() through which your program talks to the file. Its main arguments are the path, the mode and the encoding.

Mode Name File exists File does not exist
"r" read (default) reads from the start FileNotFoundError
"w" write empties it first, then writes creates it
"a" append writes at the end creates it
"x" exclusive create FileExistsError creates it

Two letters can be added to a mode. "b" selects binary mode ("rb", "wb"); without it, the file is opened in text mode. "+" opens the file for both reading and writing ("r+" reads and writes without emptying the file first).

with open("notes.txt", "x", encoding="utf-8") as f:
    f.write("first version\n")
print("created")

with open("notes.txt", "x", encoding="utf-8") as f:   # already exists
    f.write("second version\n")
created
FileExistsError: [Errno 17] File exists: 'notes.txt'

The with statement

An open file uses resources of the operating system, and data you write may wait in a buffer until the file is closed. So every file you open must be closed again, also when an error occurs halfway. The with statement does that for you: when the indented block ends — normally, through return, or because of an exception — the file is closed.

with open("greeting.txt", "w", encoding="utf-8") as f:
    f.write("Hello, file!\n")
    print("inside, closed?", f.closed)

print("after, closed?", f.closed)
inside, closed? False
after, closed? True

Without with, you would have to write f = open(…), then f.close() in the finally block of a try statement. The with statement is shorter and cannot be forgotten halfway.

Context managers

with works with any context manager: an object that defines what happens when a block is entered and when it is left, through its special methods __enter__() and __exit__(). A file object is the most common one; locks, network connections and temporary directories are others. One with statement can manage several at once:

with open("in.txt", encoding="utf-8") as src, open("out.txt", "w", encoding="utf-8") as dst:
    dst.write(src.read().upper())

Reading a file

A file object in text mode offers several ways to read:

  • f.read() returns the whole file as one string.
  • f.readline() returns the next line, including its "\n", or "" at the end of the file.
  • f.readlines() returns a list of all lines.
  • for line in f: iterates over the lines one at a time. This is the usual way: it reads only one line into memory at a time, so it works for files of any size.
with open("temps.txt", "w", encoding="utf-8") as f:
    f.write("Monday 14.5\nTuesday 17.0\nWednesday 12.5\n")

with open("temps.txt", encoding="utf-8") as f:   # mode "r" is the default
    print(repr(f.readline()))

with open("temps.txt", encoding="utf-8") as f:
    for line in f:
        day, temp = line.split()
        print(f"{day:<10} {float(temp):5.1f}")

with open("temps.txt", encoding="utf-8") as f:
    print(len(f.read()))
'Monday 14.5\n'
Monday      14.5
Tuesday     17.0
Wednesday   12.5
40

Each line keeps its newline character at the end. split() without arguments ignores it; split(",") does not, so the last field keeps the "\n". When you use the line as text, remove it first with line.rstrip("\n") or line.strip().

Reading a file that does not exist raises FileNotFoundError, which you handle like any other exception:

try:
    with open("settings.txt", encoding="utf-8") as f:
        settings = f.read()
except FileNotFoundError as e:
    print("Missing:", e.filename)
    settings = ""
Missing: settings.txt

Writing a file

f.write(text) writes a string and returns the number of characters written. Unlike print(), it adds no newline of its own: write "\n" yourself. You can also pass file=f to print(), which does add the newline and converts values to text.

with open("log.txt", "w", encoding="utf-8") as f:
    f.write("start\n")
    print("temperature", 21.5, file=f)

with open("log.txt", "a", encoding="utf-8") as f:   # add to the end
    f.write("stop\n")

with open("log.txt", encoding="utf-8") as f:
    print(f.read(), end="")
start
temperature 21.5
stop

Binary files work the same way with bytes instead of str, and without an encoding:

with open("data.bin", "wb") as f:
    f.write(bytes([0, 127, 255]))

with open("data.bin", "rb") as f:
    content = f.read()
print(content, list(content))
b'\x00\x7f\xff' [0, 127, 255]

Paths with pathlib

A path is the location of a file or folder, such as reports/2026/sales.csv. Paths are more than strings: Windows separates folders with \, Linux and macOS with /. The standard module pathlib hides that difference. Its class Path represents a path as an object with useful attributes and methods, and you join paths with the / operator.

from pathlib import Path

report = Path("reports") / "2026" / "sales.csv"
print(report)
print(report.name)
print(report.stem)
print(report.suffix)
print(report.parent)
print(report.with_suffix(".json"))
reports/2026/sales.csv
sales.csv
sales
.csv
reports/2026
reports/2026/sales.json

On Windows, the same code prints the paths with backslashes. Most functions that take a file name, including open(), accept a Path as well as a string.

Relative and absolute paths

An absolute path starts at the root of the file system (/home/ada/notes.txt, or C:\Users\Ada\notes.txt on Windows). A relative path such as notes.txt is relative to the current working directory: the folder the program was started from, which is not necessarily the folder where the script is saved. Path.cwd() tells you which folder that is, and resolve() turns a relative path into an absolute one.

from pathlib import Path

notes = Path("notes.txt")
print(notes.is_absolute())

full = notes.resolve()
print(full.is_absolute())
print(full.parent == Path.cwd())
False
True
True

Reading and writing with Path

For small files, a Path can read or write the whole file in one call, opening and closing it for you. It also answers questions about the file system.

from pathlib import Path

todo = Path("todo.txt")
print(todo.exists())

todo.write_text("buy coffee\ncall Ada\n", encoding="utf-8")
print(todo.exists(), todo.is_file())
print(todo.read_text(encoding="utf-8").splitlines())
False
True True
['buy coffee', 'call Ada']

Creating and listing folders

mkdir() creates a folder; with parents=True it also creates missing parent folders, and with exist_ok=True it does not complain if the folder already exists. iterdir() lists everything in a folder, and glob() lists the entries that match a pattern such as "*.csv". Neither returns them in a guaranteed order, so sort them when order matters.

from pathlib import Path

folder = Path("reports") / "2026"
folder.mkdir(parents=True, exist_ok=True)

for name in ["sales.csv", "costs.csv", "summary.txt"]:
    (folder / name).write_text("", encoding="utf-8")

print(sorted(p.name for p in folder.iterdir()))
print(sorted(p.name for p in folder.glob("*.csv")))
print(folder.is_dir())
['costs.csv', 'sales.csv', 'summary.txt']
['costs.csv', 'sales.csv']
True

CSV files

CSV (comma-separated values) is a plain text format for tables: one row per line, with the fields separated by commas. A field that contains a comma or a quote is put between double quotes. Spreadsheets can open and save it. Because of that quoting, do not split lines on commas yourself: use the standard csv module.

Open CSV files with newline="". The csv module handles line endings itself; without this argument, text mode translates them as well, which gives extra blank rows on Windows and breaks fields that contain a line break. Real-world exports add semicolons, decimal commas and a byte order mark; see CSV dialects and Encodings and the BOM.

import csv

rows = [
    ["product", "price", "note"],
    ["tea", "2.50", "green, organic"],
    ["coffee", "3.20", ""],
]
with open("prices.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerows(rows)

with open("prices.csv", encoding="utf-8") as f:
    print(f.read(), end="")

with open("prices.csv", newline="", encoding="utf-8") as f:
    for row in csv.reader(f):
        print(row)
product,price,note
tea,2.50,"green, organic"
coffee,3.20,
['product', 'price', 'note']
['tea', '2.50', 'green, organic']
['coffee', '3.20', '']

DictReader and DictWriter

csv.DictReader uses the first row as column names and gives each following row as a dictionary, so you refer to fields by name instead of by position. csv.DictWriter does the reverse. Every value read from a CSV file is a string; convert numbers yourself.

import csv

with open("stock.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["product", "price", "quantity"])
    writer.writeheader()
    writer.writerow({"product": "tea", "price": 2.50, "quantity": 12})
    writer.writerow({"product": "cake", "price": 4.75, "quantity": 3})

with open("stock.csv", newline="", encoding="utf-8") as f:
    total = 0
    for row in csv.DictReader(f):
        print(row)
        total += float(row["price"]) * int(row["quantity"])
print(f"stock value: {total:.2f}")
{'product': 'tea', 'price': '2.5', 'quantity': '12'}
{'product': 'cake', 'price': '4.75', 'quantity': '3'}
stock value: 44.25

JSON files

JSON (JavaScript Object Notation) is a text format for structured data, used by most web APIs and many configuration files. It maps closely to Python: objects become dictionaries, arrays become lists, and true, false and null become True, False and None. The standard json module converts in both directions:

  • json.dumps(obj) turns an object into a JSON string; json.loads(text) turns a JSON string into an object.
  • json.dump(obj, f) and json.load(f) do the same, writing to or reading from an open file. (The s stands for "string".)
import json

station = {"name": "Oslo", "active": True, "readings": [14.5, 17.0], "error": None}

text = json.dumps(station)
print(text)

back = json.loads(text)
print(back == station, back["readings"][1])
{"name": "Oslo", "active": true, "readings": [14.5, 17.0], "error": null}
True 17.0

With indent, the output is spread over several lines and easier for people to read — useful for files:

import json

settings = {"city": "Zürich", "units": "metric", "alerts": [5, 30]}

with open("settings.json", "w", encoding="utf-8") as f:
    json.dump(settings, f, indent=2, ensure_ascii=False)

with open("settings.json", encoding="utf-8") as f:
    print(f.read())

with open("settings.json", encoding="utf-8") as f:
    loaded = json.load(f)
print(loaded["city"], loaded["alerts"])
{
  "city": "Zürich",
  "units": "metric",
  "alerts": [
    5,
    30
  ]
}
Zürich [5, 30]