Python
os.walk
directory traversal
recursive search
file system

Using os.walk to recursively traverse directories in Python

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

os.walk is one of Python’s most practical tools for recursive directory traversal. It is useful for indexing, cleanup, backup, and static analysis tasks, but large trees and mixed permissions can expose edge cases quickly. A robust implementation starts simple and then adds pruning, filtering, and error handling deliberately.

Understand os.walk Output

os.walk(root) yields three values per directory:

  • current_root: current directory path
  • dirs: list of subdirectories under current directory
  • files: list of file names under current directory

Baseline traversal:

python
1import os
2
3for current_root, dirs, files in os.walk("/tmp/project"):
4    for name in files:
5        full_path = os.path.join(current_root, name)
6        print(full_path)

This is enough for many scripts and is a good reference behavior for tests.

Prune Unwanted Subtrees

If you need to skip directories such as .git or venv, mutate dirs in place. That is the supported way to control recursion.

python
1import os
2
3skip_dirs = {".git", "venv", "__pycache__"}
4
5for current_root, dirs, files in os.walk("/tmp/project"):
6    dirs[:] = [d for d in dirs if d not in skip_dirs]
7    for name in files:
8        print(os.path.join(current_root, name))

Using in-place assignment matters. Reassigning dirs without slice replacement does not change walk behavior.

This pruning technique works when os.walk is running top-down, which is the default. That default is useful because it lets you decide which subdirectories to descend into before recursion continues.

Filter by File Type and Size

Real tasks usually need filtering.

python
1import os
2
3for root, dirs, files in os.walk("/tmp/project"):
4    for name in files:
5        if not name.endswith(".py"):
6            continue
7
8        path = os.path.join(root, name)
9        if os.path.getsize(path) > 1024 * 1024:
10            continue
11
12        print(path)

This pattern keeps memory usage low because processing is streaming instead of collecting all paths first.

Handle Permission Errors Safely

Traversal can fail on restricted directories. Use onerror to capture and continue.

python
1import os
2
3def handle_error(err: OSError) -> None:
4    print(f"walk error: {err}")
5
6for root, dirs, files in os.walk("/var/log", onerror=handle_error):
7    for name in files:
8        print(os.path.join(root, name))

For production jobs, send these errors to structured logging instead of plain prints.

Deterministic Order for Repeatable Output

os.walk does not guarantee order across platforms or filesystems. Sort when deterministic output is required.

python
1import os
2
3for root, dirs, files in os.walk("/tmp/project"):
4    dirs.sort()
5    files.sort()
6    for name in files:
7        print(os.path.join(root, name))

Deterministic ordering is important for tests, checksums, and diff-based pipelines.

If you ever need bottom-up traversal, pass topdown=False. That mode is helpful for delete operations where child files and directories need to be processed before the parent directory itself.

Avoid Common Path Handling Mistakes

Always build paths with os.path.join or pathlib instead of manual string concatenation. Manual concatenation creates portability issues and subtle bugs on different operating systems.

pathlib alternative:

python
1from pathlib import Path
2
3for root, dirs, files in os.walk("/tmp/project"):
4    root_path = Path(root)
5    for name in files:
6        path = root_path / name
7        print(path)

This version can be easier to read when your code already uses Path objects.

Build a Reusable Traversal Function

Encapsulating traversal logic improves testability and reuse.

python
1import os
2from typing import Iterator
3
4
5def iter_files(base_dir: str, suffix: str) -> Iterator[str]:
6    for root, dirs, files in os.walk(base_dir):
7        dirs[:] = [d for d in dirs if d not in {".git", "venv"}]
8        for name in files:
9            if name.endswith(suffix):
10                yield os.path.join(root, name)
11
12
13if __name__ == "__main__":
14    for file_path in iter_files("/tmp/project", ".py"):
15        print(file_path)

This function is runnable and easy to unit test with temporary directories.

Performance Considerations on Large Trees

For very large trees:

  • avoid building giant in-memory lists
  • process each file as it is discovered
  • prune early to reduce recursion width
  • parallelize downstream processing only after path discovery if needed

os.walk itself is usually fast enough for control flow. Bottlenecks are often file parsing or network calls in your processing step.

Common Pitfalls

A common pitfall is trying to prune directories by assigning a new dirs variable instead of mutating dirs[:]. Another is assuming traversal order is stable across runs, which breaks deterministic outputs. Teams also ignore permission errors and then misinterpret missing files as traversal bugs. Manual path concatenation introduces cross-platform issues. Finally, collecting every path into memory before processing causes avoidable memory pressure on large directory trees.

Summary

  • Use os.walk for streaming recursive traversal with low overhead.
  • Prune recursion by mutating dirs[:] in place.
  • Add file filters, error handlers, and sorting based on use case.
  • Use safe path construction with os.path.join or pathlib.
  • Wrap traversal in reusable functions for cleaner tests and maintainable scripts.

Course illustration
Course illustration

All Rights Reserved.