What you'll learn
- Read and write text files with
open()inside awithblock - Read a file all at once, or line by line without loading it into memory
- Build and inspect paths with
pathlib.Pathso your code works on every operating system - Create folders, list their contents, and check whether a file exists
Concept
Everything your program holds in variables disappears when it ends. Files are how it remembers. open(path) gives you a file object; reading from it returns text, writing to it saves text, and closing it flushes everything to disk. The with statement does the closing for you, even if an error happens in the middle, which is why every file operation in this course is written as with open(...) as f:.

The second argument to open is the mode. "r" reads and is the default; "w" writes, creating the file or wiping it if it exists; "a" appends to the end. Always pass encoding="utf-8" when the file holds text, so the same code reads the same bytes on Windows, macOS, and Linux; the next lesson explains what that means. For reading, f.read() returns the whole file as one string, f.readlines() a list of lines, and looping for line in f: streams one line at a time, which is the right choice for anything large.
| Mode | Opens for | If the file exists | If it does not |
|---|---|---|---|
"r" |
reading | reads it | FileNotFoundError |
"w" |
writing | empties it first | creates it |
"a" |
appending | keeps it, adds at end | creates it |
"x" |
writing | FileExistsError |
creates it |
Paths are the other half. A path is where a file lives, and writing them as strings breaks across operating systems because Windows uses backslashes. pathlib.Path treats a path as an object: Path("data") / "notes.txt" joins pieces with the right separator, .exists() checks, .suffix gives the extension, .stem the name without it, and .parent the folder. Path.cwd() is where the program is running, and Path.home() is the user's home folder.

Path also replaces the file-handling functions you would otherwise import from os. path.mkdir(parents=True, exist_ok=True) creates a folder and any missing parents without complaining if it exists. path.iterdir() lists a folder, path.glob("*.txt") finds files by pattern, and path.read_text() and path.write_text() handle the whole open-read-close dance for small files in one call.
Code
Writing a file, then reading it back three ways. The with block closes the file each time; read_text and write_text on a Path do the same in one call.
# write_read.py
from pathlib import Path
notes = Path("notes.txt")
with open(notes, "w", encoding="utf-8") as f:
f.write("first line\n")
f.write("second line\n")
with open(notes, encoding="utf-8") as f:
print(repr(f.read()))
with open(notes, encoding="utf-8") as f:
for number, line in enumerate(f, start=1):
print(number, line.rstrip())
print(notes.read_text(encoding="utf-8").splitlines())
'first line\nsecond line\n'
1 first line
2 second line
['first line', 'second line']Appending versus writing. "a" keeps what is there; "w" starts over. The file ends up with only the last write.
# append.py
from pathlib import Path
log = Path("app.log")
log.write_text("started\n", encoding="utf-8")
with open(log, "a", encoding="utf-8") as f:
f.write("working\n")
print(log.read_text(encoding="utf-8").splitlines())
with open(log, "w", encoding="utf-8") as f:
f.write("fresh\n")
print(log.read_text(encoding="utf-8").splitlines())
['started', 'working']
['fresh']Paths as objects. Joining with /, reading the parts, and checking existence without touching the disk until the last line.
# paths.py
from pathlib import Path
report = Path("data") / "reports" / "q3.csv"
print(report)
print(report.parent, "|", report.name, "|", report.stem, "|", report.suffix)
print(report.exists())
print(Path("notes.txt").exists())
print(report.with_suffix(".json"))
data/reports/q3.csv
data/reports | q3.csv | q3 | .csv
False
True
data/reports/q3.jsonFolders: create, fill, list by pattern, and read the matches. sorted keeps the listing order stable across systems.
# folders.py
from pathlib import Path
folder = Path("data") / "reports"
folder.mkdir(parents=True, exist_ok=True)
(folder / "q1.csv").write_text("month,total\n", encoding="utf-8")
(folder / "q2.csv").write_text("month,total\n", encoding="utf-8")
(folder / "readme.md").write_text("reports\n", encoding="utf-8")
print(sorted(p.name for p in folder.iterdir()))
print([p.stem for p in sorted(folder.glob("*.csv"))])
print(sum(1 for _ in folder.glob("*.csv")), "csv files")
['q1.csv', 'q2.csv', 'readme.md']
['q1', 'q2']
2 csv filesTry it yourself
Create journal.py that appends one line with today's entry text to journal.txt each time it runs, then prints every line numbered. Run it three times. Then create a Path for a nested folder archive/2026, make it with mkdir(parents=True, exist_ok=True), and move the journal into it using Path.rename.
Common mistakes
- Forgetting
with. A file opened with plainopenmay not be flushed or closed if an error occurs, and on Windows it stays locked. - Opening with
"w"when you meant to add. Write mode empties the file the instant it opens; use"a"to append. - Building paths with string concatenation and slashes.
folder + "/" + namebreaks on Windows;Path(folder) / namedoes not.
What's next
- Open files with
with open(path, mode, encoding="utf-8");"r"reads,"w"overwrites,"a"appends, and the block closes the file for you. - Stream large files with
for line in f; useread_textandwrite_textfor small ones. - Build paths with
Pathand/, read their parts, and create folders withmkdir(parents=True, exist_ok=True).
Your programs can remember. Next you will learn what a text file really contains, bytes, and why encoding="utf-8" was in every call.