How to List Files in a Directory with Python

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']
Command Prompt output of os.listdir returning 8 entries and an isfile filter keeping 6 files in the billing folder
os.listdir() returns all 8 entries; the isfile() filter drops the two month folders and leaves 6 files.

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
Command Prompt output of os.scandir printing the size of each file in the billing folder and the total in bytes
os.scandir() lists each file with its size; the hidden .sync_state counts too, which is why the total includes 64 extra 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
Command Prompt output of os.walk printing every file in billing and its 2026-09 and 2026-10 subfolders, 10 files in total
os.walk() goes into both month folders; the script finds 10 files across three folders.

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']