os.walk

For each folder, os.walk yields its path and two lists of plain names. Join names with the folder path yourself. With the default topdown=True you can sort or prune the dirnames list in place, and the walk follows your edit.

os functionPython 3.0+ (fwalk 3.3+, Unix)Live demo
Common call
for root, dirs, files in os.walk(top):
Returns
a generator of (root, dirs, files)
Replaces
Hand-written recursion over listdir
Watch out
Prune with dirs[:] = … (in place); dirs = … does nothing
os.walk(toptop — The folder to start from. Every dirpath begins with it (as given — relative stays relative).type: str | PathLike · required, topdowntopdown — True: a folder is yielded before its sub-folders (and you can prune). False: after them — useful for deleting bottom-up.type: bool · default: True=True, onerroronerror — Called with the OSError when a folder cannot be listed. By default errors are silently ignored — even a missing top.type: callable · default: None=None, followlinksfollowlinks — Descend into symlinked folders. Off by default to avoid loops.type: bool · default: False=False) / os.fwalk(top=".", topdowntopdown — True: a folder is yielded before its sub-folders (and you can prune). False: after them — useful for deleting bottom-up.type: bool · default: True=True, onerroronerror — Called with the OSError when a folder cannot be listed. By default errors are silently ignored — even a missing top.type: callable · default: None=None, *, follow_symlinks=False, dir_fd=None)
→ generator of (str, list[str], list[str])

Demo

Live evaluation
Create some files, then walk from the current folder. dirs.sort() makes the order reproducible; root uses / on every OS.
Try:
Inputs
fileslist[str]files to create, comma-separated
Code
import os
for f in ['README.md', 'src/app.py', 'src/lib/util.py', 'tests/test_app.py']:
    os.makedirs(os.path.dirname(f) or '.', exist_ok=True)
    open(f, 'w').close()
tree = []
for root, dirs, files in os.walk('.'):
    dirs.sort()
    tree.append((root.replace(os.sep, '/'), dirs, sorted(files)))
tree
Result
[('.', ['src', 'tests'], ['README.md']), ('./src', ['lib'], ['app.py']), ('./src/lib', [], ['util.py']), ('./tests', [], ['test_app.py'])]

root starts with the top you passed ('.') and grows with os.path.join, which uses \ on Windows — hence .replace(os.sep, '/') for one result on every OS. Each folder appears once, before its sub-folders (topdown=True), and only after dirs.sort() is the visiting order reproducible: the raw order comes from the file system. In the second tab, assigning to dirs[:] changes the very list os.walk is about to descend into, so node_modules or .git is never opened.

Parameters

NameTypeRequiredDescription
topstr | PathLikeyesThe folder to start from. Every dirpath begins with it (as given — relative stays relative).
topdownboolno (True)True: a folder is yielded before its sub-folders (and you can prune). False: after them — useful for deleting bottom-up.
onerrorcallableno (None)Called with the OSError when a folder cannot be listed. By default errors are silently ignored — even a missing top.
followlinksboolno (False)Descend into symlinked folders. Off by default to avoid loops.

Return value

generator of (str, list[str], list[str]) — (dirpath, dirnames, filenames) for every folder; fwalk adds a fourth item, dirfd.

Common patterns

Every file with its full path
The classic loop.
import os
for root, dirs, files in os.walk('project'):
    for name in files:
        path = os.path.join(root, name)
        print(path)
Skip hidden and vendor folders
Prune in place, before the walk descends.
import os
SKIP = {'.git', 'node_modules', '__pycache__', '.venv'}
for root, dirs, files in os.walk('.'):
    dirs[:] = [d for d in dirs if d not in SKIP]
Delete a tree bottom-up
Files first, then the emptied folders (shutil.rmtree does the same).
import os
for root, dirs, files in os.walk(top, topdown=False):
    for name in files:
        os.remove(os.path.join(root, name))
    for name in dirs:
        os.rmdir(os.path.join(root, name))
Report unreadable folders
onerror receives the OSError instead of silently skipping.
import os
for root, dirs, files in os.walk('/var', onerror=lambda e: print('skipped:', e.filename)):
    pass

Examples

1. One triple per folder
import os os.makedirs('a/b/c') [(root.replace(os.sep, '/'), dirs, files) for root, dirs, files in os.walk('a')]
Returns
[('a', ['b'], []), ('a/b', ['c'], []), ('a/b/c', [], [])]
2. Full paths of all files
import os os.makedirs('a/b') open('a/b/f.txt', 'w').close() paths = [] for root, dirs, files in os.walk('a'): for name in files: paths.append(os.path.join(root, name).replace(os.sep, '/')) paths
Returns
['a/b/f.txt']
3. Count files in a tree
import os os.makedirs('a/b') for i in range(3): open(f'a/b/{i}.txt', 'w').close() sum(len(files) for _, _, files in os.walk('a'))
Returns
3
4. Bottom-up: deepest first
import os os.makedirs('a/b/c') [root.replace(os.sep, '/') for root, dirs, files in os.walk('a', topdown=False)]
Returns
['a/b/c', 'a/b', 'a']
5. A missing top yields nothing
import os list(os.walk('does-not-exist'))
Returns
[]
6. …unless you pass onerror
import os errors = [] list(os.walk('does-not-exist', onerror=errors.append)) type(errors[0]).__name__
Returns
'FileNotFoundError'
7. It is a lazy generator
import os type(os.walk('.')).__name__
Returns
'generator'

Pitfalls

1. Rebinding dirs instead of editing it
dirs = [...] makes a new local list; os.walk still descends into the old one. Mutate it: dirs[:] = …, dirs.remove(…), dirs.clear().
dirs = []
import os
os.makedirs('t/x')
os.makedirs('t/y')
seen = []
for root, dirs, files in os.walk('t'):
    dirs = []
    seen.append(root.replace(os.sep, '/'))
sorted(seen)
['t', 't/x', 't/y']
dirs.clear()
import os
os.makedirs('t/x')
os.makedirs('t/y')
seen = []
for root, dirs, files in os.walk('t'):
    dirs.clear()
    seen.append(root.replace(os.sep, '/'))
seen
['t']
2. Forgetting to join with root
files holds bare names. Opening or sizing them without root looks in the current folder.
bare name
import os
os.makedirs('a/b')
open('a/b/x.txt', 'w').close()
[f for root, dirs, files in os.walk('a') for f in files if os.path.isfile(f)]
[]
os.path.join(root, f)
import os
os.makedirs('a/b')
open('a/b/x.txt', 'w').close()
[f for root, dirs, files in os.walk('a') for f in files if os.path.isfile(os.path.join(root, f))]
['x.txt']
3. Expecting an error for a missing folder
os.walk swallows listing errors by default, so a typo in top quietly yields nothing.
silent
import os
list(os.walk('srcc'))
[]
onerror=raise
import os
def fail(e):
    raise e
try:
    list(os.walk('srcc', onerror=fail))
except FileNotFoundError as e:
    result = type(e).__name__
result
'FileNotFoundError'

When to use

Use it
  • Visiting every folder of a tree, with control over where to descend
  • Bottom-up processing (topdown=False), e.g. cleaning up
Reach for something else
  • Only matching files by pattern → glob.glob("**/*.py", recursive=True) or Path.rglob
  • Deleting a tree → shutil.rmtree; copying one → shutil.copytree
  • One folder only → os.scandir

Notes

CPython impl
Pure Python in Lib/os.py on top of os.scandir, with an explicit stack instead of recursion (3.12 and 3.13), so very deep trees do not hit the recursion limit
Order
Folders and files come in scandir order, which the file system decides. Sort dirs in place for a deterministic walk
fwalk
Unix only (3.3+): like walk but also yields a directory file descriptor and is safe against symlink races; use the dirfd with dir_fd= arguments
Path.walk
pathlib.Path.walk (3.12+) is the same algorithm returning Path objects

FAQ

[os.path.join(root, f) for root, dirs, files in os.walk(top) for f in files] — or with pathlib, [p for p in Path(top).rglob('*') if p.is_file()].