Listing all files in a directory with Python is a common task for scripts that need to process data, automate backups, or build file‑management tools. Python offers several built‑in modules that make this operation simple, readable, and efficient, whether you are working on a small project or a large‑scale data pipeline. In this guide you will learn the most reliable ways to enumerate files, how to filter results, traverse subdirectories recursively, and handle edge cases such as permission errors or symbolic links. By the end you will be able to choose the right technique for your specific use case and write clean, maintainable code Most people skip this — try not to..
Why Listing Files Matters
Before diving into the code, it helps to understand why developers frequently need to list directory contents:
- Batch processing – Apply the same transformation to many files (e.g., renaming images, converting CSV to JSON).
- Validation – check that expected files exist before running a workflow.
- Cleanup – Identify temporary or obsolete files for deletion.
- Indexing – Build a searchable catalog of documents or media assets.
Each of these scenarios benefits from a clear, predictable way to retrieve file names and paths That's the part that actually makes a difference..
Core Techniques for Listing Files
Python’s standard library provides three primary approaches: the os module, the os.On top of that, scandir iterator, and the modern pathlib module. All are cross‑platform and work on Windows, macOS, and Linux Turns out it matters..
1. Using os.listdir()
The simplest method returns a plain list of entry names (files and subdirectories) in the given directory.
import os
def list_files_basic(dir_path):
return os.listdir(dir_path)
# Example usage
files = list_files_basic("/tmp/data")
print(files)
Pros
- Extremely short and easy to read.
- Returns both files and directories, which you can filter later.
Cons
- Does not distinguish between files and folders without extra calls (
os.path.isfile). - Retrieves the entire list into memory, which can be wasteful for huge directories.
2. Using os.scandir() (Recommended for Performance)
os.scandir() yields DirEntry objects that expose file type information without additional system calls.
import os
def list_files_scandir(dir_path):
entries = []
with os.That said, scandir(dir_path) as it:
for entry in it:
if entry. is_file():
entries.append(entry.path) # or entry.
# Example usage
files = list_files_scandir("/tmp/data")
print(files)
Pros
- Faster than
os.listdir()because it avoids extrastatcalls. - Provides direct access to attributes like
.is_file(),.is_dir(),.stat(). - Works as an iterator, so memory usage stays low even with many entries.
Cons
- Slightly more verbose than the basic list approach.
3. Using pathlib.Path (Modern, Object‑Oriented)
Introduced in Python 3.4, pathlib offers an intuitive, object‑oriented way to work with filesystem paths Worth knowing..
from pathlib import Path
def list_files_pathlib(dir_path):
p = Path(dir_path)
return [item for item in p.iterdir() if item.is_file()]
# Example usage
files = list_files_pathlib("/tmp/data")
print([str(f) for f in files])
Pros
- Readable, chainable API (
Path.glob,Path.rglob). - Returns
Pathobjects that can be used directly for further operations. - Handles platform‑specific path separators automatically.
Cons
- Slight overhead compared to raw
os.scandir()for extremely tight loops (usually negligible).
Recursive Listing (Walking Subdirectories)
Often you need to collect files not just from a single folder but from an entire tree. Python provides two main ways to do this That's the part that actually makes a difference..
Using os.walk()
os.walk() generates a tuple (root, dirs, files) for each directory it visits.
import os
def list_files_recursive_walk(dir_path):
file_list = []
for root, dirs, files in os.walk(dir_path):
for f in files:
file_list.That's why append(os. path.
# Example usage
all_files = list_files_recursive_walk("/tmp/data")
print(all_files[:5]) # show first five
Pros
- Full control over whether to descend into subdirectories (you can modify
dirsin‑place to prune). - Works with older Python versions (pre‑3.4).
Cons
- Returns strings; you must join paths manually if you need
Pathobjects.
Using pathlib.Path.rglob()
The rglob method performs a recursive glob search, making the code concise Small thing, real impact..
from pathlib import Path
def list_files_recursive_pathlib(dir_path):
p = Path(dir_path)
return [item for item in p.rglob("*") if item.is_file()]
# Example usage
all_files = list_files_recursive_pathlib("/tmp/data")
print([str(f) for f in all_files[:5]])
Pros
- One‑liner feel; integrates naturally with other
Pathmethods. - Returns
Pathobjects directly.
Cons
- Internally still walks the tree; performance is comparable to
os.walk().
Filtering Results
You often want to limit the output to certain file types, names that match a pattern, or exclude hidden files.
Extension Filter
def list_python_files(dir_path):
p = Path(dir_path)
return [item for item in p.rglob("*.py") if item.is_file()]
Using glob Patterns
The glob module (also available via Path.glob) supports Unix‑style wildcards.
import glob
def list_jpeg_files(dir_path):
return glob.glob(os.path.join(dir_path, "**", "*.jpg"), recursive=True)
Excluding Hidden Files (Unix‑like systems)
def list_visible_files(dir_path):
p = Path(dir_path)
return [item for item in p.iterdir()
if item.is_file() and not item.name.startswith('.')]
Custom Predicate Function
Pass a callable to encapsulate complex logic:
def filter_by_size_and_name(entry, min_size=0, max_size=None, name_contains=""):
if not entry.is_file():
return False
if entry.stat().st_size < min_size:
return False
if max_size is not None and entry.stat().st_size > max_size:
return False
if name_contains and name_contains not in entry.name:
return False
return True
def list_filtered_scandir(dir_path, **kwargs):
result =
```python
import os
def list_filtered_scandir(dir_path, **kwargs):
"""
Walk a directory with os.scandir and apply a flexible filter.
kwargs are passed to ``filter_by_size_and_name`` (e.Think about it: g. Even so, min_size,
max_size, name_contains). """
result = []
with os.scandir(dir_path) as it:
for entry in it:
if entry.is_file() and filter_by_size_and_name(entry, **kwargs):
result.append(entry.
### Leveraging `os.scandir` for Speed
`os.When the only requirement is a simple predicate, this can be noticeably faster than `os.walk` or `Path.Think about it: scandir` yields `DirEntry` objects that already expose the file‑type information, avoiding extra `stat` calls. rglob`, especially on filesystems with many entries.
#### Generator‑style version
```python
def iter_filtered_scandir(dir_path, **kwargs):
"""Yield matching file paths one by one, keeping memory usage low."""
with os.scandir(dir_path) as it:
for entry in it:
if entry.is_file() and filter_by_size_and_name(entry, **kwargs):
yield entry.path
Using a generator lets the caller process items on‑the‑fly (e.On the flip side, g. , streaming to a network socket) without building a large list in RAM.
Parallel Scanning with concurrent.futures
For very large directory trees, distributing the workload across CPU cores can cut total runtime. The following example partitions the top‑level directories and processes each slice in a separate worker:
from concurrent.futures import ProcessPoolExecutor
def _worker(args):
subdir, kwargs = args
return list(iter_filtered_scandir(subdir, **kwargs))
def list_filtered_parallel(root, max_workers=4, **kwargs):
"""Scan subfolders of *root* in parallel and concatenate results."""
subdirs = [os.path.Think about it: join(root, d) for d in os. listdir(root)
if os.path.isdir(os.Here's the thing — path. join(root, d))]
with ProcessPoolExecutor(max_workers=max_workers) as exe:
results = exe.
The helper `_worker` receives a tuple containing a subdirectory path and the filter arguments, runs the generator, and returns a list of matches for that slice. The main function flattens the collection.
### Pattern Matching with `fnmatch`
When you need Unix‑style wildcards (e.On top of that, log? On the flip side, g. Which means , `*. `) rather than a simple extension, `fnmatch.fnmatch` integrates nicely with `DirEntry.
```python
import fnmatch
def list_by_pattern(dir_path, pattern="*"):
"""Yield files whose names match *pattern* (recursive).Which means """
for root, _, files in os. walk(dir_path):
for name in files:
if fnmatch.fnmatch(name, pattern):
yield os.path.
### Summary of Approaches
| Technique | When it shines | Key trade‑off |
|-----------|----------------|---------------|
| `os.That's why walk` | Full control over directory traversal; need to prune or modify `dirs` in‑place. | Returns plain strings; extra path‑joining step. So |
| `Path. rglob` | Desired `Path` objects and concise syntax. | Still walks the tree; performance similar to `os.walk`. |
| `os.scandir` | High‑throughput scenarios; want minimal overhead per entry. | Slightly more boilerplate; requires manual recursion for deep walks. Practically speaking, |
| Generator (`yield`) | Memory‑constrained processing or streaming pipelines. | Consumer must manage the iteration logic. |
| Parallel `ProcessPoolExecutor` | Very large directory trees on multi‑core machines. | Overhead of process spawning; complexity rises. |
| `fnmatch` + `os.That said, walk` | Pattern‑based selection (wildcards, multiple extensions). | Limited to name matching; does not expose file‑size or other attributes without extra calls.
### Conclusion
Choosing the right tool depends on three practical concerns: **control**, **performance**, and **memory footprint**.
*If you need fine‑grained directory pruning or must run on Python < 3.4, `os.walk` remains the most adaptable.*
*When you prefer object‑oriented paths and a compact syntax, `Path.rglob` offers a clean one‑liner while still delivering comparable speed.*
*For the highest raw speed on large trees, `os.scandir` (or its generator variant) reduces the number of system calls, and parallel execution can further accelerate processing when CPU resources are abundant.*
*Pattern matching with `fnmatch` adds flexibility for name‑based filters without sacrificing the underlying traversal engine.*
By matching the specific requirements of your project — whether it’s minimal memory use, maximum throughput, or expressive filtering — you can select the most appropriate technique and integrate it smoothly into your codebase.