RizTech Academy logo
RizTech Academy
Files and DataLesson 5 of 520 min

File paths done properly with pathlib

Every file operation so far has used a bare filename, which works while everything sits in one folder. Real programs read from one place and write to another, run from directories you did not expect, and have to work on machines that are not yours.

Why not just build strings

path = "data" + "/" + "sales.csv"

Three problems.

Separators differ. Windows uses \, everything else uses /. Windows mostly tolerates /, but paths you print or compare come back with backslashes, and string comparisons then fail.

Backslashes are escapes. "data\new.csv" contains a newline, because \n is a newline. You would need "data\\new.csv" or a raw string, and this catches people constantly.

Joining gets fiddly. Does the first part already end with a separator? Both data//sales.csv and datasales.csv are easy mistakes.

pathlib

from pathlib import Path

path = Path("data") / "sales.csv"
print(path)
data/sales.csv

The / operator joins paths. It looks strange for about a day and then reads better than anything else. Python uses the right separator for the platform, and the same code works everywhere.

You can pass a Path anywhere a filename is expected:

with open(path, encoding="utf-8") as file:
    ...

Or skip open entirely, which is often neater:

text = path.read_text(encoding="utf-8")
path.write_text("hello", encoding="utf-8")

Those open, read or write, and close in one call. For a whole small file, they are the shortest correct thing you can write.

Taking a path apart

path = Path("/home/priya/projects/report.final.csv")

print(path.name)        # report.final.csv
print(path.stem)        # report.final
print(path.suffix)      # .csv
print(path.parent)      # /home/priya/projects
print(path.parts)       # ('/', 'home', 'priya', 'projects', 'report.final.csv')

suffix is the last extension only; suffixes gives all of them. stem is the name without the final extension.

Changing parts without string surgery:

print(path.with_suffix(".json"))     # /home/priya/projects/report.final.json
print(path.with_name("other.csv"))   # /home/priya/projects/other.csv

with_suffix is the tidy way to write "the same file, different format".

Asking about a path

path = Path("data/sales.csv")

print(path.exists())
print(path.is_file())
print(path.is_dir())

Useful, with one caveat: between checking and using, things can change. Another program may delete the file in between. For that reason,

if path.exists():
    contents = path.read_text(encoding="utf-8")

is slightly weaker than

try:
    contents = path.read_text(encoding="utf-8")
except FileNotFoundError:
    contents = ""

This is the "try it, do not check first" argument from module 6, applied to files. exists() is fine for a quick decision; try/except is right when it matters.

Size and modification time:

print(path.stat().st_size)        # bytes

Creating folders

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

parents=True creates missing intermediate folders. exist_ok=True means an already existing folder is not an error.

Write both. Without parents, a missing intermediate raises FileNotFoundError. Without exist_ok, running your program twice raises FileExistsError the second time. Together they express "make sure this folder exists", which is what you meant.

A useful habit before writing:

output_file.parent.mkdir(parents=True, exist_ok=True)
output_file.write_text(data, encoding="utf-8")

That guarantees the destination folder exists. Writing into a folder that does not exist is a common cause of FileNotFoundError on a write, which surprises people who assume writing always creates things.

Finding files

folder = Path("data")

for csv_file in folder.glob("*.csv"):
    print(csv_file.name)

for csv_file in folder.rglob("*.csv"):     # every subfolder too
    print(csv_file)

glob matches within the folder; rglob recurses. Both return Path objects you can use directly.

Processing every CSV in a folder becomes three lines:

for path in sorted(Path("data").glob("*.csv")):
    rows = path.read_text(encoding="utf-8").splitlines()
    print(f"{path.name}: {len(rows) - 1} rows")

sorted() matters — glob order is not guaranteed, and unsorted output looks random to a user.

Relative paths and where you are

This causes real confusion. A relative path is resolved against the current working directory — where the program was launched from, not where the file lives.

Path("data/sales.csv")

Run from the project root, that works. Run the same script from inside src, and it does not. Nothing about your code changed.

For files that belong with your code, anchor to the file itself:

HERE = Path(__file__).resolve().parent
DATA = HERE / "data" / "sales.csv"

__file__ is the path of the current source file, and .resolve() makes it absolute. DATA now points at the same file regardless of where the program was started.

Anchor to __file__ for files shipped with your code — templates, bundled data. Use paths relative to the working directory for files the user supplies, since those are relative to where they are standing.

Other useful anchors:

print(Path.cwd())        # where the program was started
print(Path.home())       # the user's home folder

Deleting

path.unlink()                    # delete a file
path.unlink(missing_ok=True)     # no error if it is already gone
folder.rmdir()                   # delete an EMPTY folder

There is no recursive delete in pathlib, which is a deliberate safety feature. shutil.rmtree() does it, and deserves real care — it is irreversible, there is no recycle bin, and a wrong variable deletes a great deal very quickly. Print what you are about to delete before you delete it.

Practice

  1. Build a path with / and print it. Note the separator on your system.
  2. Print name, stem, suffix and parent for Path("reports/2026/sales.final.csv").
  3. Use with_suffix to turn a .csv path into a .json one.
  4. Write a file with write_text and read it back with read_text.
  5. Create output/reports/daily with one mkdir call. Run it twice.
  6. Remove exist_ok=True and run twice. Read the error.
  7. List every .md file in a folder with glob, then with rglob. Compare.
  8. Write a script using a relative path, then run it from a different directory and watch it fail. Fix it with Path(__file__).
  9. Write a program that finds every .txt file under a folder and reports the total size.
  10. Write a function taking an input path and writing alongside it with a different extension, creating the folder if needed.

That is module seven. Your programs can now read and write real files, handle JSON and CSV properly, and find their way around a filesystem that is not yours.

Next module: modules, packages and virtual environments — how to structure a project and install other people's code without breaking your own.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship