Walking through directory trees is a common requirement in automation scripts, command-line tools, backup systems, code analyzers, and file organizers. For years, the standard solution in Python was os.walk(). Starting with Python 3.12, the Path class from pathlib also provides Path.walk(), bringing recursive traversal into an object-oriented path API. This guide explains how to use pathlib.Path.walk in Python, how it differs from os.walk(), and how to apply it safely in real projects.
What is pathlib.Path.walk?
Path.walk() traverses a directory tree and yields a three-item tuple for each directory: the current directory, a list of subdirectory names, and a list of file names. Its structure resembles os.walk(), but the current directory is a Path object. That makes it easier to join paths, inspect extensions, read files, or access metadata without manually converting strings.
from pathlib import Path
root = Path("project")
for directory, folders, files in root.walk():
print("Directory:", directory)
print("Folders:", folders)
print("Files:", files)
If the Path API is new to you, review the Academify introduction to file handling with pathlib. Understanding path objects and the / operator makes the examples below much easier to follow.
Building full file paths
The file names returned by the method are strings. Combine each name with the current directory to build a complete path.
from pathlib import Path
for directory, folders, files in Path("data").walk():
for name in files:
path = directory / name
print(path)
This approach is portable across Windows, macOS, and Linux because pathlib handles path separators for the current operating system. It is safer and clearer than manually concatenating strings. If your script cannot find a path, the Academify guide to FileNotFoundError in Python explains the most common causes.
Filtering files by extension
A frequent task is finding only files of a certain type. Since the complete path is a Path object, you can inspect its suffix property.
from pathlib import Path
for directory, folders, files in Path("reports").walk():
for name in files:
file_path = directory / name
if file_path.suffix.lower() == ".csv":
print(file_path)
Calling lower() also matches uppercase variants such as .CSV. After locating the files, you can process them with the techniques shown in the Academify tutorial about CSV files in Python.
Skipping directories efficiently
When the walk runs from top to bottom, you can modify the folders list in place. Removing names from this list prevents the method from entering those directories. This is useful for virtual environments, caches, dependency folders, and version-control metadata.
from pathlib import Path
ignored = {".git", ".venv", "__pycache__", "node_modules"}
for directory, folders, files in Path("my_project").walk(top_down=True):
folders[:] = [folder for folder in folders if folder not in ignored]
print(directory)
The slice assignment matters. It changes the original list used internally by the traversal. Assigning a completely new list to another variable would not prune the walk. Filtering early can save significant time when a project contains thousands of dependency or cache files.
Understanding top_down and bottom_up
The top_down argument controls traversal order. Its default value is True, so a parent directory is yielded before its children. With False, deeper directories appear before their parents.
from pathlib import Path
for directory, folders, files in Path("temporary").walk(top_down=False):
print(directory)
Bottom-up traversal is useful for deleting directory trees because their contents must be removed before the parent folder. Top-down traversal is better when you want to prune subdirectories before visiting them. Remember that modifying the folder list only controls traversal when walking from the top down.
Handling access errors
A directory tree may contain locations that the current user cannot read. The on_error argument accepts a callback that receives the operating-system error.
from pathlib import Path
def log_error(error):
print(f"Could not access: {error.filename}")
for directory, folders, files in Path("/data").walk(on_error=log_error):
print(directory)
Logging errors is important in backups and audits. Otherwise, a script may finish while silently missing part of the tree. For a deeper explanation of permission failures, read the Academify article about PermissionError in Python.
Counting files and total size
You can combine walk() with stat() to create a storage summary.
from pathlib import Path
file_count = 0
total_bytes = 0
for directory, folders, files in Path("backup").walk():
for name in files:
path = directory / name
try:
total_bytes += path.stat().st_size
file_count += 1
except OSError as error:
print("Failed:", path, error)
print("Files:", file_count)
print("Size:", total_bytes, "bytes")
The exception handler protects against race conditions. A file can be deleted, renamed, or have its permissions changed after it is listed but before stat() runs.
Path.walk versus os.walk
Both APIs follow a similar generator-based model, but they are not identical. Path.walk() yields a Path object for the current root, while os.walk() yields strings. The way symbolic links and certain directory entries are categorized can also differ. Therefore, do not replace one with the other without testing your assumptions.
The official Path.walk documentation describes its parameters and edge cases. The os.walk reference is useful for a side-by-side comparison.
Symbolic links and recursion risks
The follow_symlinks argument controls whether symbolic links that point to directories should be traversed. Its default value is False. Enabling it requires care because a link may point to an ancestor and create an infinite cycle. The walk does not automatically maintain a complete visited-directory registry for every possible loop.
from pathlib import Path
for directory, folders, files in Path("data").walk(follow_symlinks=False):
pass
For backup, cleanup, and indexing tools, keeping the default is usually safer. If your application must follow links, track resolved paths or filesystem identities to avoid revisiting the same directory.
Practical example: organize files by extension
The following script walks through an input directory and moves each file into a folder named after its extension.
from pathlib import Path
import shutil
source = Path("incoming")
destination = Path("organized")
for directory, folders, files in source.walk():
for name in files:
file_path = directory / name
extension = file_path.suffix.lower().lstrip(".") or "no_extension"
target_folder = destination / extension
target_folder.mkdir(parents=True, exist_ok=True)
shutil.move(str(file_path), target_folder / file_path.name)
Always test file-moving scripts on copies first. Name collisions, permissions, and files changing during execution can cause unexpected results. The Academify tutorial on copying and moving files with shutil covers additional precautions.
Performance considerations
Path.walk() yields results lazily, so you do not need to store an entire directory tree in memory. Process each directory as it arrives. Prune irrelevant folders as early as possible, avoid calling stat() unless metadata is necessary, and consider stopping after a match when you only need one file. On network drives, each metadata request may be expensive, so reducing filesystem calls can matter more than micro-optimizing Python code.
Best practices
Validate the root before starting, filter directories early, handle OSError, and avoid symbolic-link traversal unless it is required. Normalize extensions before comparison. For destructive scripts, add a dry-run mode that prints planned actions before changing files. Also record errors and totals so operators can verify that a backup or cleanup examined the expected number of items.
Conclusion
pathlib.Path.walk provides a modern and readable way to traverse directory trees in Python 3.12 and newer. It integrates naturally with Path operations, supports traversal-order control, allows directory pruning, and offers explicit error handling. For new projects already using pathlib, it can be an excellent alternative to os.walk(). The key is to combine its convenient API with careful handling of permissions, race conditions, links, and destructive file operations.







