šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Python pathlib Deep Dive

Beyond the basics — glob patterns, path resolution, and the specific pathlib methods that replace nearly every os.path and os function you've been calling manually.

⚔ Total XP: 0|šŸ’» python XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core logic.

Quick Quiz //

What is the difference between Path.glob("*.py") and Path.rglob("*.py")?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

Modern Python Best Practices introduced pathlib as the modern alternative to os.path. This lesson goes deeper — the full method surface that lets pathlib replace os.walk, shutil operations, and manual string parsing entirely, not just path joining.

1glob and rglob: Finding Files Without os.walk()

Before pathlib, finding every .py file in a directory tree meant either glob.glob("src/**/*.py", recursive=True) from the separate glob module, or manually walking the tree with os.walk() and filtering filenames yourself — both work, but require importing a second module or writing manual filtering logic. Path.glob(pattern) and Path.rglob(pattern) (recursive glob, shorthand for glob("**/" + pattern)) fold that capability directly onto the Path object itself, returning an iterator of matching Path objects — themselves fully-featured, ready for further pathlib operations, not plain strings needing to be re-wrapped.

Glob patterns support the same wildcard syntax most developers already know from shell usage: * matches any characters within one path segment, ** (used with rglob, or explicitly in a glob("**/pattern") call) matches across any number of directory levels, and ? matches a single character. test_*.py matches test_utils.py and test_core.py but not utils_test.py, letting you express fairly precise file-selection logic in one line instead of a filtering loop.

Because glob/rglob return generators (lazy, per the Generators lesson), wrapping the result in list(...) is only necessary when you need the full list — for something like 'process every matching file one at a time', iterating the generator directly avoids materializing every path in memory at once, exactly the same efficiency benefit covered for generators generally.

āœ•
—
+
from pathlib import Path

project = Path("src")
python_files = list(project.glob("*.py"))          # top-level only
all_python_files = list(project.rglob("*.py"))       # recursive, all subdirectories
test_files = list(project.rglob("test_*.py"))        # recursive with a name pattern
localhost:3000
Console Output
list(project.rglob("test_*.py"))
Every matching test file, from every subdirectory

2Decomposing a Path Without String Parsing

Extracting a filename's extension, its name without the extension, or its containing directory used to mean either os.path.splitext() (returning a tuple) or manual string slicing on \ or / — both fragile relative to platform differences and easy to get subtly wrong (what about a file with no extension? multiple dots?). Path objects expose this decomposition as properties directly: .stem (the filename without its final suffix), .suffix (the final extension, including the dot), .suffixes (a list, for multi-extension names like archive.tar.gz), .parent (the containing directory, itself a Path), .name (the full filename with extension), and .parts (a tuple of every path component).

Because .parent returns another Path object rather than a string, it chains naturally — p.parent.parent walks up two directory levels, and p.parent / "other_file.txt" builds a sibling path — without ever needing to re-parse or re-wrap a string in between operations. This chaining is a recurring theme across pathlib's design: nearly every method and property that conceptually 'returns a path' returns an actual Path object, not a string, keeping the full method surface available at every step.

This eliminates a whole category of platform-specific string-parsing bugs (assuming / as a separator on a codebase that also needs to run on Windows, for instance) by construction — Path's decomposition properties work correctly regardless of the underlying OS's path separator convention, since the object itself, not manual string logic, understands the platform's actual path syntax.

āœ•
—
+
p = Path("/home/user/reports/2026/summary.csv")
print(p.stem)     # 'summary' -- filename without extension
print(p.suffix)   # '.csv'
print(p.parent)   # PosixPath('/home/user/reports/2026')
print(p.parts)    # ('/', 'home', 'user', 'reports', '2026', 'summary.csv')
localhost:3000
Path Decomposition
p.stem, p.suffix, p.parent
'summary', '.csv', PosixPath('/home/user/reports/2026')

3File Operations: Replacing os, shutil, and Manual open() Calls

pathlib extends well beyond path manipulation into the actual file operations that used to require os, shutil, or manual open()/close() pairs. Path.mkdir(parents=True, exist_ok=True) replaces os.makedirs(path, exist_ok=True); Path.write_text()/.read_text() and .write_bytes()/.read_bytes() replace an explicit open()/write()/close() sequence (or a with open(...) as f: block) for the common case of writing or reading an entire file's contents at once; .rename(), .unlink() (delete a file), and .exists()/.is_file()/.is_dir() cover what os.rename, os.remove, and os.path.exists/isfile/isdir used to require importing os for.

parents=True on mkdir() is the Path equivalent of os.makedirs() versus os.mkdir() — it creates every missing intermediate directory in the path, not just the final one, so Path("output/2026/reports").mkdir(parents=True) succeeds even if neither output/ nor output/2026/ exist yet. exist_ok=True makes the call idempotent — safe to run repeatedly (a common need in scripts that might be re-run) without raising FileExistsError if the directory is already there from a previous run.

The practical result: for the large majority of everyday file and directory manipulation in application code, pathlib alone — without importing os, os.path, glob, or shutil separately — covers what's needed, consolidating file-system interaction behind one consistent, chainable, object-oriented API instead of four separate modules with four different calling conventions.

āœ•
—
+
output_dir = Path("output") / "2026" / "reports"
output_dir.mkdir(parents=True, exist_ok=True)  # creates ALL missing parent dirs

report = output_dir / "summary.txt"
report.write_text("Report contents", encoding="utf-8")
report.rename(output_dir / "summary_final.txt")
localhost:3000
Consolidated File Ops
mkdir, write_text, rename, unlink
One consistent API instead of os + shutil + open()

4Step-by-Step Breakdown

pathlib isn't just os.path.join with nicer syntax — it's a near-complete replacement for os, os.path, glob, and shutil's file-related functions. Let's use the whole thing.

Path.glob() and rglob() replace manual os.walk() loops for finding files matching a pattern.

Checkpoint: What is the difference between Path.glob("*.py") and Path.rglob("*.py")?

  • →rglob searches recursively through all subdirectories; glob only searches the top level
  • →rglob returns results sorted alphabetically; glob does not

Path objects decompose themselves — .stem, .suffix, .parent, and .parts give you every piece without manual string splitting.

mkdir(parents=True, exist_ok=True) and the full read/write/rename/unlink method set cover nearly everything os and shutil used to be needed for.

Checkpoint: What does mkdir(parents=True, exist_ok=True) guard against?

  • →Missing intermediate parent directories (parents=True) AND an error if the directory already exists (exist_ok=True)
  • →Insufficient filesystem permissions to create the directory

pathlib covers the filesystem; collections covers the specialized data structures the standard library gives you beyond list/dict/set.

Reproduce Real glob vs rglob. Finish matches_pattern(): rglob searches recursively, glob only searches the top level.

Level Up šŸš€

Advanced cheat sheets, SEO tricks, and interview prep for this topic.

Browser Support

ChromeSupported

Fully supported (via server-side Python execution).

FirefoxSupported

Fully supported (via server-side Python execution).

SafariSupported

Fully supported (via server-side Python execution).

EdgeSupported

Fully supported (via server-side Python execution).

Best Practices

Use rglob() with a specific pattern instead of os.walk() plus manual filename filtering

It expresses the same recursive search intent in one line, returns ready-to-use Path objects, and avoids manually reconstructing full paths from os.walk()'s (dirpath, dirnames, filenames) tuples.

Chain Path operations (parent, /, .with_suffix()) instead of round-tripping through strings

Converting a Path to a string mid-chain and back loses type safety and pathlib's cross-platform correctness guarantees for no benefit — stay in Path objects until you genuinely need a string, e.g. for a legacy API.

Frequent Bugs

THE BUG

Using string concatenation or os.path.join with manually-typed separators to build a path, producing code that only works correctly on one operating system.

THE FIX

Use the / operator on Path objects (base_dir / "subdir" / "file.txt") which correctly handles the platform-appropriate separator automatically.

Real-World Examples

Finding and Archiving Old Log Files

A maintenance script needs to find every .log file older than 30 days across a nested logs directory and move them into an archive folder, preserving relative structure.

from pathlib import Path
import time

logs_dir = Path("var/logs")
archive_dir = Path("var/archive")
cutoff = time.time() - 30 * 86400

for log_file in logs_dir.rglob("*.log"):
    if log_file.stat().st_mtime < cutoff:
        destination = archive_dir / log_file.relative_to(logs_dir)
        destination.parent.mkdir(parents=True, exist_ok=True)
        log_file.rename(destination)

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Calling mkdir() without parents=True on a path whose intermediate directories don't exist yet, raising FileNotFoundError.

# Wrong: fails if 'output' or 'output/2026' don't already exist Path("output/2026/reports").mkdir() # Correct: creates every missing intermediate directory Path("output/2026/reports").mkdir(parents=True, exist_ok=True)

The Solution //

Use mkdir(parents=True, exist_ok=True) by default for directory creation unless you specifically need to detect a missing intermediate directory as an error.

Lesson Glossary

[01]glob pattern

A wildcard-based pattern (*, **, ?) used to match filenames or paths, supported by Path.glob() and Path.rglob().

Code Preview
// glob pattern context

[02]Path.stem

A Path property returning the filename without its final extension.

Code Preview
// Path.stem context

[03]Path.parent

A Path property returning the containing directory as another Path object, enabling chained navigation.

Code Preview
// Path.parent context

[04]idempotent

An operation that produces the same result and does not error when repeated, such as mkdir(exist_ok=True).

Code Preview
// idempotent context

Continue Learning