Batu Lab NotesPractical developer guides

Walk a project tree without following directory symlinks

By Batu ยท English technical notes

Also published in our Blogger archive.

Direct answer

os.walk(root, followlinks=False) avoids descending through directory symlinks. The example also removes symlink entries from the mutable directory list before descent, making its exclusion policy explicit. A regular src directory is traversed; vendor-link, which points to a separate temporary tree, is skipped.

The assertion checks the relative paths actually observed. Seeing only src/main.py confirms that the synthetic outside file was not included in this walk. Relative paths keep the result independent of the temporary directory name.

This example requires permission to create a symlink. Its expected output describes the successful symlink setup; if the platform rejects that operation, it prints symlink=unavailable instead. That is an unsupported fixture, not a successful traversal test. The rule also says nothing about symlinks to individual files or filesystem changes during a scan. Apply a separate file-opening policy before reading discovered content; a traversal option does not create a security sandbox.

Complete example

import os
from pathlib import Path
from tempfile import TemporaryDirectory

with TemporaryDirectory() as directory:
    root = Path(directory) / "project"
    outside = Path(directory) / "outside"
    (root / "src").mkdir(parents=True)
    outside.mkdir()
    (root / "src" / "main.py").write_text("pass\n", encoding="utf-8")
    (outside / "secret.py").write_text("secret = 1\n", encoding="utf-8")
    try:
        (root / "vendor-link").symlink_to(outside, target_is_directory=True)
    except OSError:
        print("symlink=unavailable")
    else:
        seen = []
        for current, directories, names in os.walk(root, followlinks=False):
            for name in directories[:]:
                if (Path(current) / name).is_symlink():
                    directories.remove(name)
            seen.extend((Path(current) / name).relative_to(root).as_posix() for name in names)
        assert seen == ["src/main.py"]
        print("files=src/main.py skipped=vendor-link")

Expected stdout (for a platform supporting the demonstrated operation):

files=src/main.py skipped=vendor-link

Sources

- os.walk

- pathlib.Path.symlink_to

Prepared with AI assistance. The example uses synthetic data; its stated limits apply.