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
- Build a path with
/and print it. Note the separator on your system. - Print
name,stem,suffixandparentforPath("reports/2026/sales.final.csv"). - Use
with_suffixto turn a.csvpath into a.jsonone. - Write a file with
write_textand read it back withread_text. - Create
output/reports/dailywith onemkdircall. Run it twice. - Remove
exist_ok=Trueand run twice. Read the error. - List every
.mdfile in a folder withglob, then withrglob. Compare. - Write a script using a relative path, then run it from a different directory
and watch it fail. Fix it with
Path(__file__). - Write a program that finds every
.txtfile under a folder and reports the total size. - 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