The short answer: use pathlib. Path("/home/ana/report.pdf").name gives you report.pdf, and .stem gives you report without the extension. If you are working in older code, os.path.basename() does the same job. Everything below is the detail around those two lines — the edge cases, the Windows quirks, and why you should stop using string splitting for this.
The modern way: pathlib#
from pathlib import Path
p = Path("/home/ana/documents/report.pdf")
print(p.name) # report.pdf
print(p.stem) # report
print(p.suffix) # .pdf
print(p.parent) # /home/ana/documents
Four attributes, no function calls, no imports beyond pathlib. This works identically on Windows, macOS and Linux:
p = Path(r"C:\Users\ana\Documents\report.pdf")
print(p.name) # report.pdf
print(p.stem) # report
The r prefix makes it a raw string so the backslashes are not treated as escape characters.
The older way: os.path#
Plenty of existing code uses os.path, and it is still perfectly correct:
import os
path = "/home/ana/documents/report.pdf"
print(os.path.basename(path)) # report.pdf
print(os.path.dirname(path)) # /home/ana/documents
print(os.path.splitext(os.path.basename(path))[0]) # report
print(os.path.splitext(path)[1]) # .pdf
Getting the name without the extension takes two nested calls, which is the main reason pathlib reads better. os.path.splitext returns a tuple of everything before the dot and the extension, so index 0 is the stem and index 1 is the suffix.
What not to do#
path = "/home/ana/report.pdf"
name = path.split("/")[-1] # works here, fails on Windows
This is the answer people reach for first and it is a genuine bug on Windows, where paths use backslashes:
path = r"C:\Users\ana\report.pdf"
print(path.split("/")[-1])
# C:\Users\ana\report.pdf - the whole path, unchanged
There was no forward slash to split on, so the “last piece” is the entire string. The code does not crash; it silently produces the wrong answer, which is worse. Use Path or os.path.basename and the platform difference disappears.
Files with more than one extension#
archive.tar.gz is the classic case:
p = Path("backups/archive.tar.gz")
print(p.name) # archive.tar.gz
print(p.stem) # archive.tar <- only strips the last suffix
print(p.suffix) # .gz
print(p.suffixes) # ['.tar', '.gz']
.stem removes one extension at a time. To strip all of them:
def full_stem(path):
p = Path(path)
name = p.name
for suffix in p.suffixes:
if name.endswith(suffix):
name = name[: -len(suffix)]
return name
print(full_stem("backups/archive.tar.gz")) # archive
Be careful with this on files like report.2024.01.pdf, where suffixes returns three entries and stripping them all leaves just report. Whether that is right depends entirely on your data.
Getting the name from a URL#
A URL is not a file path — it can carry a query string and a fragment, and Path will happily include them in the name. Parse it properly:
from urllib.parse import urlparse, unquote
from pathlib import PurePosixPath
url = "https://example.com/files/annual%20report.pdf?v=3#page2"
name = PurePosixPath(unquote(urlparse(url).path)).name
print(name) # annual report.pdf
urlparse discards the query and fragment, unquote turns %20 back into a space, and PurePosixPath is used because URLs always use forward slashes regardless of which operating system you are running on.
Listing names in a folder#
from pathlib import Path
folder = Path("documents")
for item in folder.iterdir():
if item.is_file():
print(item.name)
# only PDFs, including subfolders
pdf_names = [p.name for p in folder.rglob("*.pdf")]
# names without extensions, sorted
stems = sorted(p.stem for p in folder.glob("*.csv"))
Quick reference#
| You want | pathlib | os.path |
|---|---|---|
| File name with extension | p.name |
os.path.basename(p) |
| File name without extension | p.stem |
os.path.splitext(os.path.basename(p))[0] |
| Extension only | p.suffix |
os.path.splitext(p)[1] |
| Containing folder | p.parent |
os.path.dirname(p) |
| Absolute path | p.resolve() |
os.path.abspath(p) |
Questions people ask#
Does the file need to exist?
No. All of these operations are pure string manipulation on the path — nothing touches the disk. Path("nonsense/nothing.txt").stem returns nothing quite happily. Only methods like .exists(), .resolve() and .iterdir() read the filesystem.
Should I use pathlib or os.path in new code?
Pathlib. It reads better, handles separators for you, and every function in the standard library that takes a path will accept a Path object directly. os.path is not deprecated, so there is no need to rewrite working code.
Why does Path.name return an empty string for my directory?
Almost always a trailing separator. Path("/var/log/") and Path("/var/log") behave differently for .name. Strip trailing slashes, or call .resolve() first.
How do I get just the file name inside an f-string?
Assign it first for readability: name = Path(path).name, then use the name in the f-string. Putting the whole expression inside the braces works but gets hard to read fast.
Where to go next#
- Python file handling — reading and writing the files you have just located.
- Build a Python file organiser — a project that uses
.suffixand.stemon every file in a folder. - Python try/except explained — for the moment a path does not exist after all.