pathlib.Pathtrey.io/news โ a weekly Python tip ๐
>>> path = "/home/trey/Documents/some_file.txt"
pathlib: added in Python 3.4
os
os.path
glob
shutil
shutil modulecopyfile(src, dst), copyfileobj(fsrc, fdst)
copy(src, dst), copy2(src, dst)
copymode(src, dst), copystat(src, dst)
copytree(src, dst), move(src, dst)
rmtree(path)
chown(path, ...)
glob moduleglob(pattern)
iglob(pattern)
escape(pathname)
translate(pathname)
os.path modulenormpath(name), abspath(path)
basename(path), dirname(path)
isfile(path), isdir(path)
splitext(path), getsize(path)
relpath(path, parent), join(parent, name)
ossysos.getcwd(), os.chdir()
os.chmod()
os.symlink(), os.link(), os.readlink()
os.stat(), os.lstat()
os.scandir(), os.walk()
os.rename(), os.replace(), os.remove()
os.mkdir(), os.makedirs()
os.abort(),
os.access(),
os.chown(),
os.chroot(),
os.close(),
os.closerange(),
os.confstr(),
os.copy_file_range(),
os.cpu_count(),
os.ctermid(),
os.device_encoding(),
os.dup(),
os.dup2(),
os.eventfd(),
os.eventfd_read(),
os.eventfd_write(),
os.execl(),
os.execle(),
os.execlp(),
os.execlpe(),
os.execv(),
os.execve(),
os.execvp(),
os.execvpe(),
os.fchdir(),
os.fchmod(),
os.fchown()
os.fdatasync(),
os.fdopen(),
os.fork(),
os.forkpty(),
os.fpathconf(),
os.fsdecode(),
os.fsencode(),
os.fspath(),
os.fstat(),
os.fstatvfs(),
os.fsync(),
os.ftruncate(),
os.fwalk(),
os.get_blocking(),
os.get_exec_path(),
os.get_inheritable(),
os.get_terminal_size(),
os.getcwdb(),
os.getegid(),
os.getenv(),
os.getenvb(),
os.geteuid(),
os.getgid(),
os.getgrouplist(),
os.getgroups(),
os.getloadavg(),
os.getlogin(),
os.getpgid(),
os.getpgrp()
os.getpid(),
os.getppid(),
os.getpriority(),
os.getrandom(),
os.getresgid(),
os.getresuid(),
os.getsid(),
os.getuid(),
os.getxattr(),
os.grantpt(),
os.initgroups(),
os.isatty(),
os.kill(),
os.killpg(),
os.lchown(),
os.listdir(),
os.listxattr(),
os.lockf(),
os.login_tty(),
os.lseek(),
os.major(),
os.makedev(),
os.memfd_create(),
os.minor(),
os.mkfifo(),
os.mknod(),
os.nice(),
os.open(),
os.openpty(),
os.pathconf(),
os.pidfd_open()
os.pipe(),
os.pipe2(),
os.popen(),
os.posix_fadvise(),
os.posix_fallocate(),
os.posix_openpt(),
os.posix_spawn(),
os.posix_spawnp(),
os.pread(),
os.preadv(),
os.ptsname(),
os.putenv(),
os.pwrite(),
os.pwritev(),
os.read(),
os.readinto(),
os.readv(),
os.register_at_fork(),
os.remove(),
os.removedirs(),
os.removexattr(),
os.renames(),
os.rmdir()
os.sched_get_priority_max(),
os.sched_get_priority_min(),
os.sched_getaffinity(),
os.sched_getparam(),
os.sched_getscheduler(),
os.sched_rr_get_interval(),
os.sched_setaffinity(),
os.sched_setparam(),
os.sched_setscheduler(),
os.sched_yield(),
os.sendfile(),
os.set_blocking(),
os.set_inheritable()
os.setegid(),
os.seteuid(),
os.setgid(),
os.setgroups(),
os.setns(),
os.setpgid(),
os.setpgrp(),
os.setpriority(),
os.setregid(),
os.setresgid(),
os.setresuid(),
os.setreuid(),
os.setsid(),
os.setuid(),
os.setxattr(),
os.spawnl(),
os.spawnle(),
os.spawnlp(),
os.spawnlpe(),
os.spawnv(),
os.spawnve(),
os.spawnvp(),
os.spawnvpe(),
os.splice(),
os.statvfs(),
os.strerror(),
os.sync(),
os.sysconf(),
os.system(),
os.tcgetpgrp(),
os.tcsetpgrp()
os.timerfd_create(),
os.timerfd_gettime(),
os.timerfd_gettime_ns(),
os.timerfd_settime(),
os.timerfd_settime_ns(),
os.times(),
os.truncate(),
os.ttyname(),
os.umask(),
os.uname(),
os.unlink(),
os.unlockpt(),
os.unsetenv(),
os.unshare(),
os.urandom(),
os.utime(),
os.wait(),
os.wait3(),
os.wait4(),
os.waitid(),
os.waitpid(),
os.waitstatus_to_exitcode(),
os.write(),
os.writev()
os.getcwd(), os.chdir()
os.chmod()
os.symlink(), os.link(), os.readlink()
os.stat(), os.lstat()
os.scandir(), os.walk()
os.rename(), os.replace(), os.remove()
os.mkdir(), os.makedirs()
shutil.copy(src, dst), shutil.copy2(src, dst),
shutil.copytree(src, dst), shutil.move(src, dst),
shutil.rmtree(path),
os.getcwd(), os.chdir(),
os.chmod(),
os.symlink(), os.link(), os.readlink(),
os.stat(),
os.scandir(), os.walk(),
os.rename(), os.replace(), os.remove()
os.mkdir(), os.makedirs(),
os.path.abspath(path),
os.path.basename(path), os.path.dirname(path),
os.path.isfile(path), os.path.isdir(path),
os.path.splitext(path), os.path.getsize(path),
os.path.relpath(path, parent), os.path.join(parent, name),
glob.glob(pattern)
os.path versus string operations
import os.path
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
import os.path
BASE_DIR = os.path.abspath(__file__).rsplit("/", maxsplit=2)[0]
TEMPLATES_DIR = BASE_DIR + "/templates"
import os
import os.path
BASE_DIR = os.path.abspath(__file__).rsplit(os.sep, maxsplit=2)[0]
TEMPLATES_DIR = BASE_DIR + os.sep + "templates"
C:\Documents\ever\seen/a path/like this.txt
os.path modulenormpath(name), abspath(path)
basename(path), dirname(path)
isfile(path), isdir(path)
splitext(path), getsize(path)
relpath(path, parent), join(parent, name)
passes strings around when a better type exists
target = "2025-09-25"
if target[:4] == "2025":
print("That's this year")
from datetime import datetime
user_input = "2025-09-25"
target = datetime.strptime(user_input, "%Y-%m-%d").date()
if target.year == 2025:
print("That's this year")
import os.path
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATES_DIR = BASE_DIR / "templates"
from typing import Optional
# Are these meant to represent filenames or file contents?
question: Optional[str] = None
answer: Optional[str] = None
def find_editorconfig_file() -> str:
... # Does this return a file path or file contents?
from pathlib import Path
from typing import Optional
question: Optional[Path] = None
answer: Optional[Path] = None
def find_editorconfig_file() -> Path:
... #
Path objects
import os.path
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
TEMPLATES_DIR = os.path.join(BASE_DIR, "templates")
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATES_DIR = BASE_DIR / "templates"
from os.path import isdir, join, isfile
from glob import iglob
# ...
if isdir(path):
for path in iglob(join(path, "*.py"), recursive=True):
if isfile(path):
process_file(path)
else:
process_file(path)
from pathlib import Path
# ...
if path.is_dir():
for path in path.rglob("*.py"):
if path.is_file():
process_file(path)
else:
results.append(process_file(path))
with open("config.txt", mode="rt") as file:
content = file.read()
content = Path("config.txt").read_text()
| Task | The old way | The pathlib way |
|---|---|---|
| Make dir | os.mkdir(path) |
path.mkdir(parents=True) |
| with parents | os.makedirs(path) |
|
| Copy file |
shutil.copyfile(src, dst)shutil.copy(src, dst)shutil.copy2(src, dst)shutil.copytree(src, dst)
|
path.copy(dst) (3.14+)path.copy_into(dst) (3.14+)
|
pathlib work?Path objects
>>> from pathlib import Path
>>> path = Path(".editorconfig")
>>> path
PosixPath('.editorconfig')
p.chmod(),
p.copy(),
p.copy_into(),
p.exists(),
p.expanduser(),
p.glob(),
p.is_dir(),
p.is_file(),
p.iterdir(),
p.mkdir(),
p.move(),
p.move_into(),
p.read_text(),
p.rename(),
p.replace(),
p.resolve(),
p.rglob(),
p.rmdir(),
p.stat(),
p.unlink(),
p.walk(),
p.write_text(),
p.joinpath(),
p.relative_to(),
p.with_name(),
p.with_stem(),
p.with_suffix(),
Path.cwd(),
Path.home(),
p.name,
p.parent,
p.parents,
p.parts,
p.stem,
p.suffix
p.absolute(),
p.as_uri(),
p.is_fifo(),
p.group(),
p.hardlink_to(),
p.is_block_device(),
p.is_char_device(),
p.is_junction(),
p.is_mount(),
p.is_socket(),
p.is_symlink(),
p.lchmod(),
p.lstat(),
p.open(),
p.owner(),
p.readlink(),
p.read_bytes(),
p.samefile(),
p.symlink_to(),
p.touch(),
p.write_bytes(),
Path.from_uri(),
p.as_posix(),
p.is_absolute(),
p.is_relative_to(),
p.is_reserved(),
p.full_match(),
p.match(),
p.with_segments(),
p.anchor,
p.drive,
p.root,
p.suffixes
from pathlib import Path
path = Path("example.txt")
with open(str(path)) as f:
content = f.read()
from pathlib import Path
path = Path("example.txt")
with open(path) as f:
content = f.read()
pathlib works everywhere you need itos.chdir()
shutil.chown()
sqlite3.connect()
logging.FileHandler()
zipfile.ZipFile()
subprocess.run()
os.path.abspath()
os.path.isfile()
os.path.join()
os.remove()
os.mkdir()
shutil.copy()
shutil.move()
shutil.copy(src, dst), shutil.copy2(src, dst),
shutil.copytree(src, dst), shutil.move(src, dst),
shutil.rmtree(path),
os.chmod(),
os.symlink(), os.link(), os.readlink(),
os.stat(),
os.scandir(), os.walk(),
os.rename(), os.replace(), os.remove()
os.mkdir(), os.makedirs(),
os.path.abspath(path),
os.path.basename(path), os.path.dirname(path),
os.path.isfile(path), os.path.isdir(path),
os.path.splitext(path), os.path.getsize(path),
os.path.relpath(path, parent), os.path.join(parent, name)
Path objectsdjango (in settings for example)
pandas.read_csv(path)
PIL.Image.open(path)
pytest.main([path])
click.File()(path)
pathlibPath objects
>>> from pathlib import Path
>>> notes_path = Path("Documents/notes.txt")
>>> notes_path
WindowsPath('documents/notes.txt')
>>> notes_path2 = Path(r"documents\notes.txt")
>>> notes_path2
WindowsPath('documents/notes.txt')
>>> notes_path2 == notes_path
True
>>> from pathlib import Path
>>> home = Path.home()
>>> path1 = home.joinpath(".config.toml")
>>> path2 = home / ".config.toml"
>>> path3 = Path(home, ".config.toml")
>>> path3
PosixPath('/home/trey/.config.toml')
>>> path1 == path2 == path3
True
| pathlib | Traditional approach | Returns |
|---|---|---|
Path(name) |
os.path.normpath(name) |
Path |
path.resolve() |
os.path.abspath(path) |
Path |
path.name |
os.path.basename(path) |
str |
path.parent |
os.path.dirname(path) |
Path |
path.suffix |
os.path.splitext(path)[1] |
str |
path.stem |
os.path.splitext(path)[0] |
str |
path.is_file() |
os.path.isfile(path) |
bool |
parent / name |
os.path.join(parent, name) |
Path |
pym.dev/pathlib-modulepathlibpathlib.Path
import os
import pathlib
class BetterPath(pathlib.Path):
def chdir(self):
os.chdir(self)
Python 3.12+ officially supports pathlib inheritance
>>> BetterPath.home().chdir()
>>> git_path = GitPath("src/main.py", repo_root=".")
>>> git_path.parent # repo_root is preserved when creating new paths
GitPath('/home/user/project/src', repo_root='/home/user/project')
class GitPath(pathlib.Path):
def __init__(self, *segments, repo_root):
super().__init__(*segments)
self.repo_root = pathlib.Path(repo_root).resolve()
def with_segments(self, *args):
"""Ensure any derived paths remember the repo root."""
return type(self)(*args, repo_root=self.repo_root)
def relative_to_repo(self):
return self.relative_to(self.repo_root or self.root)
def __repr__(self):
return f"{type(self).__name__}({str(self)!r}, repo_root={self.repo_root!r})"
pathlib.Path objects are Path-like objectsos.PathLike protocolopen() accept path-like objects__fspath__() method worksos.fspath() paths to strings
import os
class MyPath:
def __init__(self, path):
self.path = os.fspath(path)
def __fspath__(self):
return self.path
my_path = MyPath("example.txt")
with open(my_path) as file:
content = file.read()
open method
path = Path("example.txt")
with path.open() as file:
contents = file.read()
path = Path("example.txt")
with open(path) as file:
contents = file.read()
The open method is a relic from an earlier time
>>> print(f"Reading: {path}")
Reading: example.txt
path = Path("example.txt")
with open(str(path)) as f:
content = f.read()
path = Path("example.txt")
with open(path) as f:
content = f.read()
directory = "/home/trey/project"
filename = ".editorconfig"
config = Path(directory).joinpath(filename)
config = Path(directory) / filename
config = Path(directory, filename)
Works whether directory is a string or Path object
pathlib. You won't regret it.pathlib is slow"Yes, it can be slower for some operations
400,000 files searched
0.91 seconds with os.walk()
0.85 seconds with pathlib.Path().walk()
2.22 seconds when converting back to pathlib.Path()
Don't optimize parts of your code that aren't bottlenecks
just another way to represent paths
the ideal way to represent file paths
# Start by converting at function boundaries
def process_config(config_path):
path = Path(config_path) # Works with strings OR Path objects
return path.read_text()
# Legacy code still works
process_config("/path/to/config.txt") # string path
process_config(Path("config.txt")) # Path object
# Mix and match as you migrate
import os
for filename in os.listdir(directory):
file_path = Path(directory) / filename # pathlib joining
if file_path.suffix == '.py': # pathlib properties
with open(file_path) as f: # builtin functions accept Path
content = f.read()
The Path class accepts strings and other Path objects
pathlib.Path with argparse
# For immediate file opening:
parser.add_argument("input", type=argparse.FileType("r"))
# args.input is already an open file object
# For flexible path handling:
parser.add_argument("path", type=Path)
# args.path is a Path object
if args.path.is_dir():
... # Directory given
elif args.path.is_file():
... # Existing file given
else:
... # Path doesn't represent a file or a directory!
# Add common_prefix functionality
class BetterPath(pathlib.Path):
def common_prefix(self, other):
return self.with_segments(os.path.commonprefix((self, other)))
# Enhanced rmdir with recursive option
class BetterPath(pathlib.Path):
def rmdir(self, *, recursive=False, ignore_errors=False):
if recursive:
shutil.rmtree(self, ignore_errors=ignore_errors)
else:
try:
super().rmdir()
except Exception:
if not ignore_errors:
raise
from plumbum import local
# This is a plumbum Path, not a pathlib Path
my_file = local.path("example.txt")
# But it works with open() because it has __fspath__
with open(my_file) as f:
content = f.read()
# It also works with os.path functions
import os.path
print(os.path.exists(my_file)) # Works!