glob.glob(recursive=True) Does Not Detect a Symlink Loop (tested)

A symlink that points back at one of its own ancestor directories creates a loop. We measured what glob.glob()'s recursive ** pattern does when it walks into one, and it doesn't notice.

1. Recursive glob walks the loop dozens of levels deep

import glob, time

# /tmp/globloop/sub/loop -> /tmp/globloop (points back at an ancestor)

t0 = time.time()
results = glob.glob("/tmp/globloop/**", recursive=True)
print("count:", len(results), "time:", time.time() - t0)
print("deepest:", results[-1])
count: 83 time: 0.0028s
deepest: /tmp/globloop/sub/loop/sub/loop/sub/loop/.../sub/loop

Measured: the result contains the same sub/loop segment repeated about 40 times before the walk stops. glob.glob() never raises an exception or returns early for the loop itself -- it just keeps re-entering the symlink until an internal OS-level "too many levels of symbolic links" check silently cuts it off, and returns every intermediate path it generated along the way as real matches.

2. Why: recursive glob doesn't track visited directories

import os

# os.walk's default behaviour on the same tree, for contrast
count = 0
for root, dirs, files in os.walk("/tmp/globloop"):
    count += 1
    if count > 10:
        break
print("os.walk entries visited (followlinks=False default):", count)
os.walk entries visited (followlinks=False default): 3

Measured: os.walk()'s default followlinks=False simply never enters the symlink, stopping the tree at 3 real entries. glob.glob(recursive=True) has no equivalent switch -- it always follows symlinks while expanding **, and has no cycle-detection of its own; it relies entirely on the filesystem's own loop limit to eventually stop it.

3. Fix: walk with os.walk() and handle symlinks explicitly, or filter results

results = glob.glob("/tmp/globloop/**", recursive=True)
clean = [r for r in results if r.count("/loop/") <= 1]
print(len(clean), "of", len(results), "kept after filtering repeated loop segments")
5 of 83 kept after filtering repeated loop segments

Measured: there's no built-in option to make glob.glob() stop at a symlink loop boundary -- the practical fix is to use os.walk(followlinks=False) (which never enters the loop at all) for any tree that might contain untrusted symlinks, rather than recursive glob.

Frequently asked questions

Does glob.glob(recursive=True) detect a symlink loop?

No -- we measured it walking about 40 levels into a self-referential symlink before an OS-level ELOOP limit silently stopped it, returning every intermediate repeated path as a real match.

Does glob.glob() raise an error for a symlink loop?

No exception in our test -- it returned a normal-looking list of 83 paths, just with the same directory segment repeated dozens of times, with no indication anything was wrong.

Does os.walk() have the same problem?

No -- we confirmed os.walk()'s default followlinks=False never enters the symlinked directory at all, stopping the tree traversal immediately rather than looping.

Is there a way to make glob.glob() stop at a symlink loop safely?

We found no built-in option -- recursive glob has no cycle detection or a followlinks-style parameter; os.walk(followlinks=False) is the safer tool for trees that might contain loops.

Could this cause a real performance problem on deep or wide loops?

We measured our test loop resolving in under 3ms because the OS's own symlink-depth limit caught it quickly, but a tree with multiple distinct loops or very long real paths could multiply the wasted work before that limit is hit.

Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in glob module: built a directory containing a symlink that points back at its own parent (a loop), then ran glob.glob() with the recursive '**' pattern against it. PlainHeaders is our extension; Python's glob module is built in.