An accounts payable team I work with wanted a Python script to list files in a directory every morning: the shared billing folder where vendor invoices land.
The obvious os.listdir() call got me halfway. It returns bare names, mixes in subfolders and a hidden sync file, and doesn’t say anything about size or date.
Quick answer: use os.listdir(path) for names, keep the files with os.path.isfile(), or loop over Path(path).iterdir():
from pathlib import Path
files = [p.name for p in Path("billing").iterdir() if p.is_file()]
In this tutorial, I’ll show you how to list files in a directory with Python using os.listdir(), pathlib, os.scandir(), glob and os.walk(). I ran every example on Python 3.12.5 on Windows 11 and kept the real output.
The sample billing folder with files and subfolders
Every script below reads the same folder. It holds five invoice-related files, a hidden .sync_state file left by a sync tool, and two month subfolders:
billing/
.sync_state
invoice_1001.pdf
invoice_1002.pdf
invoice_1003.PDF <- upper-case extension
summary.csv
vendors.xlsx
2026-09/
invoice_0915.pdf
invoice_0928.pdf
2026-10/
invoice_1014.pdf
payments.csv
Before you list anything, make sure the folder is really there. I cover that in how to check if a directory exists in Python.
Note the .PDF in capitals. Scanners and email exports do that all the time, and it breaks naive extension filters later on.
How do you list files in a directory with os.listdir()?
os.listdir() is the oldest and simplest way. It returns a plain list of names, files and folders mixed together. Here it is on the billing folder, followed by a filter that keeps files only:
import os
names = os.listdir("billing")
print(len(names), "entries:", names)
files = [n for n in names
if os.path.isfile(os.path.join("billing", n))]
print(len(files), "files:", files)
Output:
8 entries: ['.sync_state', '2026-09', '2026-10', 'invoice_1001.pdf', 'invoice_1002.pdf', 'invoice_1003.PDF', 'summary.csv', 'vendors.xlsx']
6 files: ['.sync_state', 'invoice_1001.pdf', 'invoice_1002.pdf', 'invoice_1003.PDF', 'summary.csv', 'vendors.xlsx']

How does this code work?
os.listdir() returns 8 names, including both folders and the hidden .sync_state. The names have no folder in front, so os.path.isfile() needs the joined path or it would look in the wrong place.
That’s why I always pair it with os.path.join(). The filter leaves 6 files.
Note: the docs say the order is arbitrary. On Windows (NTFS) it usually comes back alphabetical, but don’t rely on that on Linux. Sort the list yourself when order matters.
List files with pathlib Path.iterdir()
In new code, I use pathlib. Path.iterdir() yields full Path objects, so you can test each one and read its name, suffix or size without joining strings. Same folder:
from pathlib import Path
folder = Path("billing")
for path in folder.iterdir():
kind = "file" if path.is_file() else "folder"
print(f"{kind:<6} {path.name:<18} suffix={path.suffix!r}")
Output:
file .sync_state suffix=''
folder 2026-09 suffix=''
folder 2026-10 suffix=''
file invoice_1001.pdf suffix='.pdf'
file invoice_1002.pdf suffix='.pdf'
file invoice_1003.PDF suffix='.PDF'
file summary.csv suffix='.csv'
file vendors.xlsx suffix='.xlsx'
Each item already knows its folder, so is_file() works directly. The two month folders print with an empty suffix, and invoice_1003.PDF keeps its capital '.PDF'.
path.suffix and path.stem also split names for you. There are more options in getting a file extension from a file name.
Get file sizes faster with os.scandir()
os.scandir() returns DirEntry objects. They cache the file type from the directory read, so is_file() doesn’t hit the disk again. On Windows, stat() is cached too. I use it for big folders and size reports:
import os
total = 0
with os.scandir("billing") as entries:
for entry in entries:
if entry.is_file():
size = entry.stat().st_size
total += size
print(f"{entry.name:<18} {size:>6,} bytes")
print(f"{'Total':<18} {total:>6,} bytes")
Output:
.sync_state 64 bytes
invoice_1001.pdf 2,480 bytes
invoice_1002.pdf 3,125 bytes
invoice_1003.PDF 1,960 bytes
summary.csv 742 bytes
vendors.xlsx 5,812 bytes
Total 14,183 bytes

How does this code work?
The with block closes the directory handle when the loop ends. entry.stat().st_size gives bytes, and the six files add up to 14,183 bytes.
The 64-byte .sync_state is part of that total. For a single file’s size, see how to get the file size in Python.
Filter files by extension or pattern with glob
When you only want some files, glob takes shell-style wildcards: * for any text and ? for one character. Same billing folder, four patterns:
import glob
from pathlib import Path
print("glob *.pdf: ", glob.glob("billing/*.pdf"))
print("pattern 100?: ", glob.glob("billing/invoice_100?.*"))
print("Path.glob: ", sorted(p.name for p in Path("billing").glob("*.pdf")))
print("glob * count: ", len(glob.glob("billing/*")))
Output:
glob *.pdf: ['billing\\invoice_1001.pdf', 'billing\\invoice_1002.pdf', 'billing\\invoice_1003.PDF']
pattern 100?: ['billing\\invoice_1001.pdf', 'billing\\invoice_1002.pdf', 'billing\\invoice_1003.PDF']
Path.glob: ['invoice_1001.pdf', 'invoice_1002.pdf', 'invoice_1003.PDF']
glob * count: 7
Two things surprised the AP team here. First, on Windows *.pdf also matched invoice_1003.PDF, because pattern matching ignores case there. On Linux it wouldn’t.
Second, glob("billing/*") found 7 entries, not 8. Wildcards skip names that start with a dot, so .sync_state drops out. Python 3.11 added include_hidden=True when you want them.
Why does endswith(‘.pdf’) miss some files?
A list comprehension with endswith() is the other common filter, and it’s case-sensitive on every system. Same folder:
import os
names = os.listdir("billing")
exact = [n for n in names if n.endswith(".pdf")]
fixed = [n for n in names if n.lower().endswith(".pdf")]
print("endswith('.pdf'): ", exact)
print("lower().endswith('.pdf'):", fixed)
Output:
endswith('.pdf'): ['invoice_1001.pdf', 'invoice_1002.pdf']
lower().endswith('.pdf'): ['invoice_1001.pdf', 'invoice_1002.pdf', 'invoice_1003.PDF']
The exact match finds 2 PDFs and quietly skips invoice_1003.PDF. Lowering the name first finds all 3. If list comprehensions are new to you, start with list comprehension in Python.
Pro Tip: with pathlib, compare p.suffix.lower() == ".pdf". It’s the same fix and reads a little cleaner.
How do you list files in subdirectories with os.walk()?
Everything so far stayed at the top level. The month folders hold three more invoices and a CSV. os.walk() visits every folder and yields (root, dirs, files) for each one:
import os
count = 0
for root, dirs, files in os.walk("billing"):
dirs.sort() # visit month folders in order
for name in sorted(files):
print(os.path.join(root, name))
count += 1
print(count, "files in billing and its subfolders")
Output:
billing\.sync_state
billing\invoice_1001.pdf
billing\invoice_1002.pdf
billing\invoice_1003.PDF
billing\summary.csv
billing\vendors.xlsx
billing\2026-09\invoice_0915.pdf
billing\2026-09\invoice_0928.pdf
billing\2026-10\invoice_1014.pdf
billing\2026-10\payments.csv
10 files in billing and its subfolders

How does this code work?
The first pass gives billing itself, then each subfolder. Sorting dirs in place controls the visit order, and removing a name from dirs would skip that folder entirely.
The script found 10 files. os.walk() is basically recursion in Python done for you. Python 3.12 also added Path.walk(), which works the same way but yields Path objects.
Find matching files in every subfolder with rglob()
When you only want one file type from the whole tree, Path.rglob() is shorter than os.walk(). glob can do the same with ** and recursive=True. Same billing tree:
import glob
from pathlib import Path
pdfs = sorted(Path("billing").rglob("*.pdf"))
for p in pdfs:
print(p.relative_to("billing"))
deep = glob.glob("billing/**/*.pdf", recursive=True)
print(len(pdfs), "via rglob,", len(deep), "via glob(recursive=True)")
Output:
2026-09\invoice_0915.pdf
2026-09\invoice_0928.pdf
2026-10\invoice_1014.pdf
invoice_1001.pdf
invoice_1002.pdf
invoice_1003.PDF
6 via rglob, 6 via glob(recursive=True)
Both return 6 PDFs: the three top-level invoices plus the three in the month folders. relative_to("billing") trims the folder name, so 2026-09\invoice_0915.pdf prints with the Windows backslash.
Without recursive=True, ** acts like a single *. I’ve lost time to that one.
Sort files by name, date modified and size
The morning report needed the newest invoices on top. sorted() with a key function does it, using st_mtime for the modified time and st_size for bytes. Same top-level files:
from datetime import datetime
from pathlib import Path
files = [p for p in Path("billing").iterdir() if p.is_file()]
newest = sorted(files, key=lambda p: p.stat().st_mtime,
reverse=True)
print("Newest first:")
for p in newest[:3]:
when = datetime.fromtimestamp(p.stat().st_mtime)
print(f" {when:%Y-%m-%d %H:%M} {p.name}")
largest = max(files, key=lambda p: p.stat().st_size)
print("Largest:", largest.name, largest.stat().st_size, "bytes")
print("By name:", sorted(p.name.lower() for p in files)[:3])
Output:
Newest first:
2026-10-12 08:31 .sync_state
2026-10-12 08:30 summary.csv
2026-10-09 11:05 invoice_1003.PDF
Largest: vendors.xlsx 5812 bytes
By name: ['.sync_state', 'invoice_1001.pdf', 'invoice_1002.pdf']