Files and Paths
Reading and writing text files safely with open() and with, working with paths in pathlib, and handling CSV and JSON.
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)andjson.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]