pathlib

Path(...) gives you an object that knows its name, suffix and parent, joins with /, and reads, writes and lists files itself. Pure paths do the string arithmetic without ever touching the disk.

Files and directoriesPython 3.4+Live demo
Import
from pathlib import Path
from pathlib import PurePosixPath, PureWindowsPath
import pathlib
Public API
Path, PurePath, PurePosixPath, PureWindowsPath, PosixPath, WindowsPath, UnsupportedOperation
Path(...) is
a PosixPath on Linux and macOS, a WindowsPath on Windows
Pure paths
Path arithmetic only, no I/O — PurePosixPath and PureWindowsPath work the same on every OS
Accepted by
open(), os, shutil, json.load(open(p)) … — every path object is os.PathLike

Demo

Live evaluation
Split a path into the pieces you usually want: its folder, file name, stem and extension.
Try:
Inputs
pathstra POSIX path
Code
from pathlib import PurePosixPath
p = PurePosixPath('/home/ada/report.pdf')
(p.parent, p.name, p.stem, p.suffix)
Result
(PurePosixPath('/home/ada'), 'report.pdf', 'report', '.pdf')

PurePosixPath normalises as it parses: repeated slashes and "." segments disappear and a trailing slash is dropped, but ".." is kept because removing it would need the file system. stem and suffix only split off the LAST extension, so site.tar.gz has the stem site.tar, and a name that starts with a dot (.bashrc) has no suffix at all. In the glob tab, results come back in whatever order the OS lists them, so the snippet sorts them.

Members

Methods & attributes19
LIVE
PurePath.as_posix
as_posix() / .as_uri() / Path.from_uri(uri)
as_posix() gives the path with forward slashes; as_uri() turns an absolute path into a file: URI, and Path.from_uri() (3.13+) turns one back.
Path.chmod
chmod(mode, *, follow_symlinks=True) / .lchmod(mode)
Change permission bits, like os.chmod. lchmod() changes a symlink itself instead of its target. On Windows only the read-only flag is honoured.
LIVE
Path.exists
exists(*, follow_symlinks=True) / .is_file() / .is_dir() / .is_symlink() / .is_junction() / .is_mount() / …
Ask what is at a path: anything at all (exists), a regular file, a directory, a symlink, a mount point, a junction, a socket, FIFO or device. All return False instead of raising when the path is missing.
LIVE
Path.glob
glob(pattern, *, case_sensitive=None, recurse_symlinks=False) / .rglob(pattern, …) / .walk(top_down=True, on_error=None, follow_symlinks=False)
Find files by pattern: glob matches relative to the folder (** crosses sub-folders), rglob searches the whole tree, walk yields (folder, dirnames, filenames) like os.walk.
LIVE
PurePath.is_absolute
is_absolute() / .is_reserved()
is_absolute() tells whether a path is anchored so the current directory does not matter; is_reserved() (deprecated) flags Windows device names like NUL and CON.
LIVE
Path.iterdir
iterdir()
Iterate over the entries of a directory — files and sub-folders, one level deep, as Path objects, in arbitrary order.
LIVE
PurePath.joinpath
joinpath(*pathsegments) / path / segment
Join path segments: the / operator adds one, joinpath() adds several. An absolute segment starts the path over.
LIVE
PurePath.match
match(pattern, *, case_sensitive=None) / .full_match(pattern, *, case_sensitive=None)
Test a path against a glob-style pattern without touching the disk. match() compares from the right; full_match() (3.13+) matches the whole path and understands **.
LIVE
Path.mkdir
mkdir(mode=0o777, parents=False, exist_ok=False) / .touch(mode=0o666, exist_ok=True) / .unlink(missing_ok=False) / .rmdir()
Create and remove: mkdir makes a folder (parents=True for the whole chain), touch creates an empty file, unlink deletes a file, rmdir deletes an empty folder.
LIVE
PurePath.name
PurePath.name / .stem / .suffix / .suffixes
The last path component (name), without its extension (stem), the extension itself (suffix) and every extension (suffixes).
Path.open
open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)
Open the file at this path and return a file object — exactly like the built-in open(path, …). Use it for streaming, appending and any mode read_text / write_text do not cover.
LIVE
PurePath.parts
PurePath.parts / .parent / .parents / .anchor / .drive / .root
Take a path apart: the tuple of components, the containing folder, every ancestor, and the drive/root prefix.
LIVE
Path.read_text
read_text(encoding=None, errors=None, newline=None) / .write_text(data, …) / .read_bytes() / .write_bytes(data)
Read or write a whole file in one call — as text (read_text / write_text) or raw bytes (read_bytes / write_bytes). No open() or with block needed.
LIVE
PurePath.relative_to
relative_to(other, walk_up=False) / .is_relative_to(other)
The path from other to this path — or a ValueError when this path is not inside other (unless walk_up=True adds ".." segments). is_relative_to() asks the same question without raising.
LIVE
Path.rename
rename(target) / Path.replace(target)
Move or rename a file or folder and get the new Path back. replace() overwrites an existing target on every OS; rename() does so only on POSIX.
Path.resolve
resolve(strict=False) / .absolute() / .expanduser() / Path.cwd() / Path.home()
Turn a path into an absolute one: resolve() also removes ".." and follows symlinks, absolute() only prefixes the current directory, expanduser() replaces "~". cwd() and home() give the two starting points.
Path.stat
stat(*, follow_symlinks=True) / .lstat() / .owner() / .group() / .samefile(other)
File metadata: stat() returns an os.stat_result (size, times, mode bits, inode …), lstat() does not follow symlinks, owner() and group() give names (POSIX only), samefile() compares two paths by identity.
Path.symlink_to
symlink_to(target, target_is_directory=False) / .hardlink_to(target) / .readlink()
Make this path a symbolic link (symlink_to) or a hard link (hardlink_to) to target, and read where a symlink points (readlink).
LIVE
PurePath.with_suffix
with_suffix(suffix) / .with_stem(stem) / .with_name(name)
Return a new path with the extension, the stem or the whole file name replaced. The original path is unchanged.

Common patterns

Paths relative to the script
Build paths from __file__ instead of relying on the current directory.
from pathlib import Path
HERE = Path(__file__).resolve().parent
config = HERE / 'config' / 'settings.toml'
Read and write a whole file
No open() / with block needed for small files; pass the encoding.
from pathlib import Path
p = Path('notes.txt')
p.write_text('hello\n', encoding='utf-8')
text = p.read_text(encoding='utf-8')
Process every matching file in a tree
rglob searches all sub-folders; sort for a stable order.
from pathlib import Path
for csv_file in sorted(Path('data').rglob('*.csv')):
    print(csv_file.stem, csv_file.stat().st_size)
Make sure an output folder exists
parents=True creates missing parents; exist_ok=True makes it idempotent.
from pathlib import Path
out = Path('build') / 'reports'
out.mkdir(parents=True, exist_ok=True)
Change the extension
with_suffix swaps the last suffix and returns a new path.
from pathlib import Path
src = Path('photos/cat.jpeg')
dst = src.with_suffix('.webp')

Examples

1. Join with the / operator
from pathlib import PurePosixPath PurePosixPath('/srv') / 'app' / 'config.toml'
Returns
PurePosixPath('/srv/app/config.toml')
2. Name, stem and suffix
from pathlib import PurePosixPath p = PurePosixPath('/data/2026/sales.csv') (p.name, p.stem, p.suffix)
Returns
('sales.csv', 'sales', '.csv')
3. The containing folder
from pathlib import PurePosixPath PurePosixPath('/data/2026/sales.csv').parent
Returns
PurePosixPath('/data/2026')
4. Write, then read back
from pathlib import Path p = Path('hello.txt') p.write_text('Hi there', encoding='utf-8') p.read_text(encoding='utf-8')
Returns
'Hi there'
5. List a folder (sorted)
from pathlib import Path for name in ['b.txt', 'a.txt', 'c.log']: Path(name).touch() sorted(p.name for p in Path('.').iterdir())
Returns
['a.txt', 'b.txt', 'c.log']
6. Glob by extension
from pathlib import Path for name in ['a.txt', 'b.txt', 'c.log']: Path(name).touch() sorted(p.name for p in Path('.').glob('*.txt'))
Returns
['a.txt', 'b.txt']
7. Windows paths on any OS
from pathlib import PureWindowsPath PureWindowsPath('C:/Users/ada/notes.txt').parts
Returns
('C:\\', 'Users', 'ada', 'notes.txt')
8. Paths are not strings
from pathlib import PurePosixPath 'backup-' + PurePosixPath('db.sqlite')
Returns
TypeError: can only concatenate str (not "PurePosixPath") to str

Pitfalls

1. Building paths with string concatenation
Gluing strings with "/" doubles or drops separators. The / operator inserts exactly one and normalises the rest.
string +
base = 'data/'
base + '/' + 'raw.csv'
'data//raw.csv'
the / operator
from pathlib import PurePosixPath
PurePosixPath('data/') / 'raw.csv'
PurePosixPath('data/raw.csv')
2. An absolute segment throws away everything before it
Joining a segment that starts with "/" restarts the path from the root — a classic bug when the second part comes from user input.
leading slash
from pathlib import PurePosixPath
PurePosixPath('/srv/uploads') / '/etc/passwd'
PurePosixPath('/etc/passwd')
check it stays inside
from pathlib import PurePosixPath
base = PurePosixPath('/srv/uploads')
(base / '/etc/passwd').is_relative_to(base)
False
3. Expecting ".." to be resolved
Pure paths never collapse "..": a/../b could point anywhere if a is a symlink. resolve() does it against the real file system; for pure paths, compare parts yourself.
kept as-is
from pathlib import PurePosixPath
PurePosixPath('reports/../secrets.txt')
PurePosixPath('reports/../secrets.txt')
resolve() on a real Path
from pathlib import Path
Path('reports').mkdir()
(Path('reports/../secrets.txt').resolve() == Path('secrets.txt').resolve())
True

When to use

Use it
  • Any new code that builds, inspects or walks file paths
  • Reading or writing small files in one call (read_text / write_text)
  • Finding files by pattern (glob / rglob) and walking trees (walk)
  • Manipulating Windows or POSIX paths on any OS (PureWindowsPath / PurePosixPath)
Reach for something else
  • Copying and moving whole trees → shutil (copytree, move, rmtree); Path.copy / move only arrive in 3.14
  • Temporary files and folders → tempfile
  • URLs → urllib.parse (as_uri / from_uri only handle file: URIs)

Notes

CPython impl
Lib/pathlib/_local.py (PurePath, Path) on top of _abc.py (PurePathBase, PathBase) in 3.13 — stem, suffix, exists, is_file and friends are inherited from the _abc bases
os.path.join()
PurePath.joinpath() or the / operator
os.path.dirname()
PurePath.parent
os.path.basename()
PurePath.name
os.path.splitext()
PurePath.stem, PurePath.suffix
os.path.isabs()
PurePath.is_absolute()
os.path.relpath()
PurePath.relative_to() — lexical, raises ValueError instead of adding ".." (unless walk_up=True)
os.path.abspath()
Path.absolute() (does not remove "..") — Path.resolve() for the realpath() behaviour
os.path.expanduser()
Path.expanduser() — raises RuntimeError when the home directory cannot be found
os.path.exists() / isfile() / isdir() / islink()
Path.exists() / is_file() / is_dir() / is_symlink()
os.getcwd()
Path.cwd()
os.stat() / os.lstat()
Path.stat() / Path.lstat()
os.listdir()
Path.iterdir()
os.walk()
Path.walk() (3.12+)
os.mkdir(), os.makedirs()
Path.mkdir() (parents=True for makedirs)
os.remove(), os.unlink() / os.rmdir()
Path.unlink() / Path.rmdir()
os.rename() / os.replace()
Path.rename() / Path.replace()
os.chmod() / os.symlink() / os.link() / os.readlink()
Path.chmod() / symlink_to() / hardlink_to() / readlink()
glob.glob()
Path.glob() — dotfiles included, ** always recursive

FAQ

pathlib for new code: a Path carries its own name, suffix, parent and I/O methods, joins with /, and is accepted everywhere a path string is (open, os, shutil). os.path remains fine in old code — the Notes table lists the pathlib equivalent of each os.path function.