Beginner · Lesson 28 of 94

Files and paths

Watch this lesson

View full playlist ↗

Read the lesson

What you'll learn

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:.

Files and paths: with open('notes.txt', 'w') as f: → f.write(...) → block ends → file is closed and saved, always
Files and paths · Figure 1 · Open for full size

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.

Files and paths: Path('data/reports/q3.csv') → .parent → data/reports → .name → q3.csv → .stem → q3 → .suffix → .csv
Files and paths · Figure 2 · Open for full size

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())
Output
'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())
Output
['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"))
Output
data/reports/q3.csv
data/reports | q3.csv | q3 | .csv
False
True
data/reports/q3.json

Folders: 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")
Output
['q1.csv', 'q2.csv', 'readme.md']
['q1', 'q2']
2 csv files

Try 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

What's next

Your programs can remember. Next you will learn what a text file really contains, bytes, and why encoding="utf-8" was in every call.

Course overview