Filesystem code often looks cheap until it runs over a directory with hundreds of thousands of entries.

A loop that asks whether every path is a file, directory, or symbolic link can translate into a large number of metadata queries. On a local SSD that cost may be tolerable. On network filesystems, container-mounted volumes, or very large trees, repeated metadata lookups can become a noticeable part of runtime.

Python 3.14 adds Path.info, a cached file-type information interface on pathlib.Path. It is especially useful when paths come from Path.iterdir(), because Python may initialize the cache with information already obtained while scanning the directory.

The important word is cached.

Path.info is not a drop-in replacement for every Path.is_file() or Path.is_dir() call. It is best for classification work where a slightly older view of the filesystem is acceptable for the duration of one traversal.

This article looks at how to use it, what it can save, and where stale metadata can cause correctness problems.

The repetitive classification pattern

Consider a small directory walker:

from pathlib import Path


def summarize(directory: Path) -> dict[str, int]:
    counts = {
        "files": 0,
        "directories": 0,
        "symlinks": 0,
        "other": 0,
    }

    for path in directory.iterdir():
        if path.is_symlink():
            counts["symlinks"] += 1
        elif path.is_dir():
            counts["directories"] += 1
        elif path.is_file():
            counts["files"] += 1
        else:
            counts["other"] += 1

    return counts

The code is straightforward, and for many applications it is perfectly fine.

But notice what each branch needs to know: the type of the same filesystem entry.

If those checks require independent metadata operations, the traversal can spend more time asking the operating system about entries than performing application work.

Python 3.14 gives Path a type-information cache for this case.

Classify entries with Path.info

The same loop can use the cached information object:

from pathlib import Path


def summarize(directory: Path) -> dict[str, int]:
    counts = {
        "files": 0,
        "directories": 0,
        "symlinks": 0,
        "other": 0,
    }

    for path in directory.iterdir():
        info = path.info

        if info.is_symlink():
            counts["symlinks"] += 1
        elif info.is_dir():
            counts["directories"] += 1
        elif info.is_file():
            counts["files"] += 1
        else:
            counts["other"] += 1

    return counts

The visible difference is small. The runtime behavior is the interesting part.

When a path comes from Path.iterdir(), its info attribute may already contain file-type information learned while the parent directory was scanned. The methods on Path.info also cache their results.

That means several classification checks against the same entry can reuse information rather than repeatedly consulting the filesystem.

Simply accessing path.info does not itself issue a filesystem query.

Why iterdir() is the natural pairing

Path.info is useful on any Path, but directory scans are where its intended shape is easiest to see.

Suppose a service scans an inbox directory and dispatches work based on entry type:

from pathlib import Path


def discover(root: Path):
    for entry in root.iterdir():
        info = entry.info

        if info.is_symlink():
            yield "symlink", entry
        elif info.is_dir():
            yield "directory", entry
        elif info.is_file():
            yield "file", entry
        elif info.exists():
            yield "other", entry

The traversal already asked the operating system to enumerate root.

On platforms where the directory enumeration returns usable type information, Python can carry some of that knowledge into the Path objects produced by iterdir().

That reuse is valuable because the program is not asking a completely new question. It is refining information associated with an entry it just discovered.

The cache can become stale

The main correctness constraint is that Path.info caches results.

Imagine a path changes after the application first classifies it:

from pathlib import Path

path = Path("incoming/report.tmp")

first = path.info.exists()

# Another process may create, remove, replace, or rename the entry here.

second = path.info.exists()

second is a query to the same cached information object. It should not be interpreted as a forced refresh of the current filesystem state.

That distinction matters in directories modified by other processes.

A build system, upload worker, file synchronizer, antivirus scanner, deployment agent, or log rotator may all mutate entries while your process is looking at them.

For fresh information, Python’s documentation recommends using the direct Path methods such as:

path.is_dir()
path.is_file()
path.is_symlink()

Those are the better tools when the application needs to make a decision about the filesystem as it exists now rather than as it appeared during an earlier classification.

There is no cache reset method

A subtle operational detail is that the information cache cannot be cleared in place.

If you want another Path with an empty information cache, construct a new path object:

from pathlib import Path

path = Path("incoming/report.csv")

cached_type = path.info.is_file()

fresh_path = Path(path)

The new Path represents the same pathname but starts with a fresh info cache.

For most code, however, I would not rebuild path objects merely to force refreshes everywhere. If the question is explicitly about current file type, using Path.is_file(), Path.is_dir(), or Path.is_symlink() communicates that intent more clearly.

Use Path.info for cached classification. Use the direct methods for freshness-sensitive checks.

Do not confuse classification with authorization

A file-type check is not an authorization boundary.

For example, this code is unsafe as a security design:

if upload.info.is_file():
    process_trusted_file(upload)

Even if the cached result is accurate at the instant it is observed, another actor may replace the path before process_trusted_file() opens it.

That is a time-of-check/time-of-use problem.

The same problem exists with ordinary is_file() checks. Caching can widen the conceptual gap, but removing the cache does not turn pathname checks into an atomic security primitive.

Security-sensitive code should prefer APIs that bind validation to the file descriptor or handle actually being used, with platform-appropriate protections against symlink and replacement attacks.

Treat Path.info as an efficiency feature, not a security mechanism.

Classification gets more complicated when symbolic links are involved.

The Path.info object provides:

info.is_symlink()
info.is_dir()
info.is_file()
info.exists()

Several of these operations distinguish between following and not following symbolic links.

For example, exists(), is_dir(), and is_file() accept a follow_symlinks keyword argument through the PathInfo protocol:

info.exists(follow_symlinks=False)
info.is_dir(follow_symlinks=False)
info.is_file(follow_symlinks=False)

This is useful when a traversal needs to reason about the directory entry itself rather than its target.

I prefer to make the symlink branch explicit before ordinary file or directory branches:

for entry in root.iterdir():
    info = entry.info

    if info.is_symlink():
        handle_symlink(entry)
    elif info.is_dir():
        handle_directory(entry)
    elif info.is_file():
        handle_file(entry)

That ordering makes the policy easy to review.

If the application intentionally wants to treat a symlink to a directory as a directory, encode that policy deliberately instead of relying on readers to infer it from default argument behavior.

A symbolic link can exist as a directory entry even when its target does not.

That creates a useful distinction:

info.is_symlink()
info.exists()

A broken symlink can still be a symlink while a target-following existence check reports that the target does not exist.

Code that scans deployment trees, package directories, or user-controlled uploads should decide whether broken links are ignored, reported, removed, or treated as errors.

Avoid collapsing all negative results into “missing file.”

A negative is_file() result can mean many things: directory, symlink policy mismatch, special file, inaccessible path, or missing entry.

Handle special filesystem entries deliberately

Not every path is a regular file or directory.

Unix-like systems can expose sockets, FIFOs, block devices, character devices, and other special entries.

A classifier therefore needs a final branch:

from pathlib import Path


def classify(path: Path) -> str:
    info = path.info

    if info.is_symlink():
        return "symlink"
    if info.is_dir():
        return "directory"
    if info.is_file():
        return "file"
    if info.exists():
        return "other"
    return "missing"

Whether other is acceptable depends on the application.

A source-tree indexer may simply skip special entries. A backup agent may need to preserve some of them. An upload processor may want to reject everything except regular files.

The cache changes how type information is obtained. It does not remove the need for a complete policy.

Keep the cached decision close to the traversal

I would avoid carrying Path.info assumptions far across an application.

This is a good shape:

for entry in root.iterdir():
    info = entry.info

    if info.is_file():
        index_file(entry)

The enumeration and classification happen close together.

This is harder to reason about:

entries = list(root.iterdir())

queue_for_later(entries)

Hours later, a worker might use entry.info and accidentally rely on metadata associated with an old traversal.

The longer a path object lives in a mutable filesystem, the less useful cached type information becomes as evidence about current state.

Pass pathnames across durable boundaries. Re-evaluate freshness-sensitive state when the work actually executes.

A queueing system should revalidate before destructive work

Suppose a scanner discovers candidate files and a worker later deletes them after processing.

The scanner can use Path.info efficiently:

from pathlib import Path


def discover_files(root: Path):
    for path in root.iterdir():
        if path.info.is_file():
            yield path

But the destructive worker should not assume that cached classification is still authoritative:

from pathlib import Path


def delete_if_regular(path: Path) -> bool:
    fresh = Path(path)

    if not fresh.is_file():
        return False

    fresh.unlink()
    return True

Even this pattern does not make the check and deletion atomic against concurrent filesystem mutation. It merely avoids intentionally relying on a stale classification cache.

For security-sensitive or race-sensitive deletion, use stronger platform-specific techniques.

The performance win is workload-dependent

Do not assume Path.info will make every filesystem loop dramatically faster.

The payoff depends on several variables:

  • whether directory enumeration already supplies file-type information,
  • which filesystem is involved,
  • whether entries are local or remote,
  • how many type questions you ask per entry,
  • whether symlinks require target metadata,
  • operating-system and Python implementation details,
  • filesystem cache warmth,
  • and how much real work happens after classification.

A loop that spends 99% of its time parsing multi-megabyte files will barely notice a metadata optimization.

A scanner that touches millions of entries and does almost nothing else may notice it immediately.

Measure the workload you actually operate.

Benchmark directories, not toy path objects

A useful benchmark should resemble the production access pattern.

For example:

from pathlib import Path
from time import perf_counter


def classify_direct(root: Path) -> int:
    total = 0
    for path in root.iterdir():
        if path.is_symlink():
            total += 1
        elif path.is_dir():
            total += 1
        elif path.is_file():
            total += 1
    return total


def classify_cached(root: Path) -> int:
    total = 0
    for path in root.iterdir():
        info = path.info
        if info.is_symlink():
            total += 1
        elif info.is_dir():
            total += 1
        elif info.is_file():
            total += 1
    return total


def timed(fn, root: Path):
    start = perf_counter()
    result = fn(root)
    return result, perf_counter() - start

Run both functions repeatedly against representative directories.

Include network mounts if production uses network mounts. Include symlinks if production has symlinks. Include the same directory sizes and depth patterns.

Also validate that both implementations produce the same classification for a stable test tree.

A faster benchmark is not useful if it silently changes application semantics.

Avoid benchmarking only warm-cache runs

Operating systems aggressively cache filesystem metadata.

That can make repeated local benchmarks look unrealistically cheap.

Cold-cache benchmarking is difficult to make portable and reproducible, but you can still avoid misleading conclusions:

  • run enough iterations to understand variance,
  • compare local and remote filesystems separately,
  • record filesystem and platform information,
  • include realistic directory sizes,
  • and focus on end-to-end application timing as well as microbenchmarks.

The goal is not to prove that one API wins every time. The goal is to learn whether metadata-query reduction matters in your environment.

Compatibility needs a Python 3.14 boundary

Path.info was added in Python 3.14.

If a library supports earlier Python releases, do not call it unconditionally.

One option is a version-specific implementation:

import sys
from pathlib import Path


def is_regular_file(path: Path) -> bool:
    if sys.version_info >= (3, 14):
        return path.info.is_file()
    return path.is_file()

But I would be careful with tiny compatibility wrappers like this.

The two branches do not have identical freshness characteristics. A helper named only is_regular_file() hides that difference.

For libraries, it can be clearer to use Path.info only inside performance-sensitive directory scans on Python 3.14+, while leaving ordinary correctness-sensitive checks on the established Path methods.

pathlib.types.PathInfo is useful for interfaces

Python 3.14 also exposes the PathInfo protocol in pathlib.types.

That matters when code wants to type an object that provides this classification behavior without requiring one specific implementation.

Conceptually, an interface can accept a path-information provider:

from pathlib.types import PathInfo


def classify(info: PathInfo) -> str:
    if info.is_symlink():
        return "symlink"
    if info.is_dir():
        return "directory"
    if info.is_file():
        return "file"
    if info.exists():
        return "other"
    return "missing"

Protocols are especially useful for adapters and tests, where you may want to model filesystem type information without touching the real filesystem.

Do not over-abstract simple scripts, but the protocol is a helpful addition for reusable libraries.

Tests should cover cache behavior explicitly

A test suite for code using Path.info should cover more than normal files.

At minimum, test:

  • regular files,
  • directories,
  • symbolic links,
  • broken symbolic links where supported,
  • missing paths,
  • special entries if the application supports them,
  • and mutations between discovery and use.

A mutation test is particularly important because it documents the intended freshness boundary.

For example, a test can discover an entry, obtain cached information, mutate the filesystem, and then verify that the application uses a fresh direct check before performing an operation that depends on current state.

The goal is not to assert implementation details of every cache entry. The goal is to make your application’s assumptions explicit.

Separate discovery from action

A useful design pattern is to distinguish two phases.

The discovery phase is allowed to use cached classification:

from pathlib import Path


def discover(root: Path):
    for path in root.iterdir():
        info = path.info

        if info.is_file():
            yield path

The action phase re-establishes whatever conditions matter to correctness:

from pathlib import Path


def consume(path: Path):
    path = Path(path)

    if not path.is_file():
        return

    with path.open("rb") as source:
        process(source)

This is a useful boundary even without Path.info.

Discovery answers, “What looked interesting when I scanned?”

Action answers, “What can I safely do now?”

The cached API makes that distinction worth stating explicitly.

Do not optimize away error handling

File trees change while applications traverse them.

Even after a positive type check, an operation can fail:

try:
    with path.open("rb") as source:
        process(source)
except FileNotFoundError:
    pass

The entry may have been removed between classification and open. Permissions may change. A mount may disappear. A symlink target may move.

No metadata cache can eliminate those races.

Design filesystem code around the possibility that the operation itself fails.

In many cases, attempting the operation and handling expected exceptions is more robust than building long chains of preflight checks.

Where Path.info fits well

I would reach for Path.info in workloads such as:

  • directory indexers,
  • static-site asset scanners,
  • backup discovery passes,
  • source-tree analysis,
  • file-browser listings,
  • packaging tools,
  • local cache inventories,
  • and batch import discovery.

These jobs often enumerate many entries and immediately classify each one.

The cache aligns naturally with that lifecycle.

Where I would prefer direct Path methods

I would prefer Path.is_file(), Path.is_dir(), and related direct methods when:

  • the path object may have lived for a long time,
  • another process frequently mutates the directory,
  • the result gates a destructive action,
  • freshness is more important than metadata-query reduction,
  • the path did not come from a nearby directory scan,
  • or the code is easier to understand without caching semantics.

Performance features are most valuable when their scope is obvious.

A direct check is often the right choice in ordinary application code.

A practical review checklist

When reviewing code that introduces Path.info, I ask a few questions.

Is the code classifying many paths from the same traversal?

Can the filesystem change between discovery and action?

Does a stale answer merely cause extra work, or can it cause incorrect or destructive behavior?

What is the symlink policy?

Are special entries handled?

Does the application still handle operation-time errors?

Has the performance benefit been measured on the relevant filesystem?

Is Python 3.14 an acceptable runtime requirement?

Those questions usually reveal whether the cache is being used as an optimization or accidentally promoted into a correctness guarantee.

Final thoughts

Path.info is a small Python 3.14 addition with a very specific strength: it lets filesystem-heavy code reuse cached type information while classifying paths.

Its best use is close to directory enumeration, especially when one entry is tested in several ways before the program decides what to do with it.

The tradeoff is freshness.

Once an application understands that boundary, the design becomes straightforward: use Path.info for efficient discovery, use direct Path methods or operation-time validation when current state matters, and never treat pathname metadata checks as atomic security guarantees.

That makes Path.info a useful optimization without allowing the optimization to quietly redefine filesystem correctness.