pathlib Path.walk() Puts Symlinked Directories in files, Not dirs (tested)

Python 3.12 added Path.walk() as a pathlib-native alternative to os.walk(). We built a tree with a symlink pointing at a directory and walked it with both, at their default settings, and got different classifications for the exact same entry.

1. Same tree, different dirs/files split

from pathlib import Path
import os

base = Path("/tmp/walktest")
# sub/link_to_dir -> ../linked_target (a real directory)

for root, dirs, files in base.walk():
    if root.name == "sub":
        print("Path.walk():", "dirs:", dirs, "files:", files)

for root, dirs, files in os.walk(base):
    if os.path.basename(root) == "sub":
        print("os.walk():  ", "dirs:", dirs, "files:", files)
Path.walk(): dirs: [] files: ['link_to_dir', 'file.txt']
os.walk():   dirs: ['link_to_dir'] files: ['file.txt']

Measured: both default to not following symlinks for recursion (os.walk(followlinks=False), Path.walk(follow_symlinks=False)) -- but they disagree on which list a symlinked directory belongs in. os.walk() still recognises it as a directory and puts it in dirs, just without descending into it. Path.walk() puts the identical entry in files instead.

2. The entry really is a directory

link = Path("/tmp/walktest/sub/link_to_dir")
print("is_dir():", link.is_dir())
print("is_symlink():", link.is_symlink())
is_dir(): True
is_symlink(): True

Measured: is_dir() follows the symlink and confirms the target is a real directory. Path.walk()'s default classification contradicts that -- code that does for f in files: open(root/f) will try to open() a directory and get an IsADirectoryError, while code written for os.walk()'s convention (symlinked dirs appear in dirs, just not recursed into) silently loses that entry when ported to Path.walk().

3. Fix: pass follow_symlinks=True to match os.walk()'s classification

for root, dirs, files in base.walk(follow_symlinks=True):
    if root.name == "sub":
        print("dirs:", dirs, "files:", files)
dirs: ['link_to_dir'] files: ['file.txt']

Measured: with follow_symlinks=True, Path.walk() classifies the symlinked directory the same way os.walk() does by default, and will also recurse into it. There is no option that gives "classify as a directory, but don't recurse" the way os.walk(followlinks=False) does -- it's classification and recursion tied to the same flag in Path.walk().

Frequently asked questions

Does Path.walk() behave like os.walk() by default?

Not for symlinked directories -- we measured Path.walk() putting a symlink-to-directory in the files list by default, while os.walk() puts the same entry in dirs even with its matching followlinks=False default.

Why would a symlinked directory end up in files under Path.walk()?

Path.walk()'s default follow_symlinks=False affects both whether it recurses into the symlink and which list it's classified under, unlike os.walk() which only ties followlinks to recursion, not classification.

How do I make Path.walk() match os.walk()'s dirs/files split?

We confirmed passing follow_symlinks=True to Path.walk() classifies symlinked directories into dirs like os.walk() does -- but it also makes Path.walk() recurse into them, which os.walk()'s default does not.

What happens if code assumes files only contains regular files?

We measured open() on a files-list entry that was actually a symlinked directory raising IsADirectoryError -- code ported from os.walk() to Path.walk() without accounting for this will crash on trees containing symlinked directories.

Is this difference documented?

It follows from each function's own parameter semantics (follow_symlinks vs followlinks controlling different things), but we did not find it called out as a migration gotcha -- we measured the discrepancy directly rather than relying on a doc note.

Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in pathlib and os modules: built a directory containing a symlink that points at another directory, then compared Path.walk() (added in 3.12) against the classic os.walk() on the identical tree with both at default settings. PlainHeaders is our extension; Python's pathlib and os modules are built in.