LESSON 109 · DEEPER PYTHON30 min

Find Files with Python glob and fnmatch

Select matching file names from a known list using fnmatchcase and deterministic sorting.

Before you start: Lists · Loops · Functions

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Find Files with Python glob and fnmatch

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    This filter returns main.py and src/a.py. Should a build tool treat the nested file as top level? Run it and compare that output with the intended file scope. The broken result includes a nested name because fnmatchcase sees the entire string and its *.py pattern matches the suffix even after a slash. The matching call does not know that the caller meant only names from one directory. A pattern is a matching rule, not a directory boundary. Notice that this bug has no exception and no failing exit status. The filter returns a plausible list containing main.py, so a test that only

  2. 00:40

    checks it found one Python file could pass. A stronger test states the intended scope and asserts that nested paths are absent. The source list may have come from a recursive crawl or external manifest; fnmatchcase does not know that history. A package builder using the result could unintentionally include a nested module when it wanted only top-level files. The correction is to validate path depth as a separate requirement from suffix matching. The candidate list already contains src/a.py; this program is not searching a directory. fnmatchcase checks that whole string, and its star can consume the slash.

  3. 01:23

    The extra result follows from the input contract, not a new disk file. The diagnosis prints separate checks for a nested path, an uppercase extension, and a plain Python filename. That separates three decisions: whether the input is a path or a base name, whether case matters, and whether matching results should be sorted. fnmatch may normalize case by platform; fnmatchcase applies the literal rule everywhere. This example applies a string matcher to names supplied by a caller; it never checks that those files exist. That distinction matters for a build manifest, where the list may contain stale or deliberately crafted paths.

  4. 02:05

    A wildcard question mark or bracket class still matches characters of a string, not an authorized directory tree. If the program later discovers files with Path.glob, it must decide whether the search is immediate or recursive and retain the resulting paths until root containment is checked. Dropping the parent directories first would conceal a nested match. Backslash is a literal character in a POSIX filename but a separator in Windows paths, so this cross-platform selector rejects both slash styles under its own top-level-only rule. A test should state that rule explicitly rather than relying on the host operating system's path parser.

  5. 02:47

    The first check rejects tool.PY because the pattern says lower-case py. The second accepts src/a.py despite its slash. Case policy and path scope therefore need separate tests. Also reject a Windows-style backslash when candidate strings may come from different systems. The fixed select function rejects names containing a separator before applying fnmatchcase with *.py. It sorts the selected base names, giving a.py and main.py in the terminal. Actual disk discovery is a different operation: pathlib.Path.glob walks a directory tree, and its results should be constrained to an approved root.

  6. 03:30

    Silently replacing each input with Path(name).name would be tempting, but it turns src/a.py into a.py and erases evidence that the caller supplied a nested path. Rejecting the original string is clearer for this top-level-only contract. Filtering a list of names is distinct from discovering files: this function never opens a path or follows a link. If a later operation does, join the accepted name to a trusted base, resolve the actual candidate, and verify it remains inside the approved directory. A stable sort then makes the output suitable for deterministic manifests.

  7. 04:11

    The path rule runs before the pattern rule, so only base names reach fnmatchcase. Sorting makes tests and display stable. For actual disk discovery with Path.glob, choose the root directory first and check is_file if the caller truly needs files rather than matching paths. The last run checks an uppercase .PY extension and a list with no matching names. It prints b.py for the chosen lower-case pattern and then an empty list. These two outputs clarify the lesson's case-sensitive contract and its empty- result behavior. Never use a glob pattern alone to authorize file deletion. The final mixed list is a contract test rather than proof that any listed file

  8. 04:52

    exists. Add a name with a trailing space, a bare extension, and a Unicode filename if those can arrive from the manifest; decide whether each is accepted, normalized, or rejected. A stable sort makes output reproducible across input orders, which helps compare generated manifests. Keep path selection separate from any later operation that opens or deletes files. For deletion, join an accepted basename to a trusted root, resolve the actual destination, and verify containment after following links. A symlink that looks harmless by name could point outside the intended tree.

  9. 05:34

    Pattern matching alone cannot grant filesystem authority. With an empty input list the selector should return an empty list, and it should never invent matches from a directory it did not inspect. The mixed input tests three properties: nested forward-slash path, uppercase suffix, and backslash path. Only b.py survives. An empty candidate list must return an empty result, not an exception. Keep both cases when changing the selection rule later.

Understand the concept

A build tool must choose Python source files from a list of names, while a separate disk search can use glob to find real paths. Both use shell-style patterns, but their inputs differ: fnmatch compares names already in memory; glob walks the filesystem. The browser task keeps its candidate list in memory so it is portable and cannot see a visitor's private files.

fnmatch may normalize case according to the host operating system. A pattern such as *.py can therefore behave differently across platforms when a filename ends in .PY. Assuming that glob returns a fixed order is another trap; filesystem traversal order is not a stable presentation order.

Compare fnmatch.fnmatch with fnmatch.fnmatchcase on mixed-case sample names. The latter applies the pattern exactly as written, making the lesson's rule portable. Apply sorted() to the selected names so tests and UI lists have an explicit order. Use fnmatch.fnmatchcase(name, '*.py') for a case-sensitive in-memory filter, and sorted(...) for deterministic results. When searching actual directories, use pathlib.Path.glob or glob.glob but validate the search root and do not assume a pattern by itself enforces access controls.

The final run includes a nested name like src/main.py. A plain fnmatchcase over full strings matches the *.py suffix even across a slash; if the requirement is top-level files only, reject names containing a path separator first. The task filters supplied names and explicitly rejects any candidate containing a path separator. Shell-style glob patterns are not regular expressions. Asterisks and question marks have different syntax from re, and a pattern is not a security boundary. Define whether matching is case-sensitive and whether paths or base names are allowed before using results to delete or publish files.

  • A build tool must choose Python source files from a list of names, while a separate disk search can use glob to find real paths.
  • fnmatch may normalize case according to the host operating system.
  • Use fnmatch.fnmatchcase(name, '*.py') for a case-sensitive in-memory filter, and sorted(...) for deterministic results.
  • The final run includes a nested name like src/main.py.

See it step by step

Read the code, predict the output, then compare it with the result.

01. Match a name case-sensitively

PYTHON
from fnmatch import fnmatchcase
print(fnmatchcase('main.py', '*.py'))
print(fnmatchcase('main.PY', '*.py'))
EXPECTED OUTPUT
True
False

The exact-case rule yields True then False independently of the host platform.

02. Sort matching names

PYTHON
from fnmatch import fnmatchcase
names = ['z.py', 'README.md', 'a.py']
print(sorted(n for n in names if fnmatchcase(n, '*.py')))
EXPECTED OUTPUT
['a.py', 'z.py']

Sorting makes the result stable even if input order changes.

03. Exclude nested candidates

PYTHON
from fnmatch import fnmatchcase
names = ['src/main.py', 'main.py']
print([n for n in names if '/' not in n and fnmatchcase(n, '*.py')])
EXPECTED OUTPUT
['main.py']

The extra path rule is an application choice, not a guarantee supplied by the pattern.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Observe the first result

A build tool must choose Python source files from a list of names, while a separate disk search can use glob to find real paths. Both use shell-style patterns, but their inputs differ: fnmatch compares names already in memory; glob walks the filesystem. The browser task keeps its candidate list in memory so it is portable and cannot see a visitor's private files.

The exact-case rule yields True then False independently of the host platform. Compare fnmatch.fnmatch with fnmatch.fnmatchcase on mixed-case sample names. The latter applies the pattern exactly as written, making the lesson's rule portable. Apply sorted() to the selected names so tests and UI lists have an explicit order.

PYTHON
from fnmatch import fnmatchcase
print(fnmatchcase('main.py', '*.py'))
print(fnmatchcase('main.PY', '*.py'))
EXPECTED OUTPUT
True
False
Follow the reasoning
  1. Predict the first sample's printed value and type.
  2. Run it, then compare with the expected output.
02 / 03

Change the rule deliberately

fnmatch may normalize case according to the host operating system. A pattern such as *.py can therefore behave differently across platforms when a filename ends in .PY. Assuming that glob returns a fixed order is another trap; filesystem traversal order is not a stable presentation order.

Sorting makes the result stable even if input order changes. Use fnmatch.fnmatchcase(name, '*.py') for a case-sensitive in-memory filter, and sorted(...) for deterministic results. When searching actual directories, use pathlib.Path.glob or glob.glob but validate the search root and do not assume a pattern by itself enforces access controls. Use fnmatchcase, reject path separators, and wrap the generator in sorted().

PYTHON
from fnmatch import fnmatchcase
names = ['z.py', 'README.md', 'a.py']
print(sorted(n for n in names if fnmatchcase(n, '*.py')))
EXPECTED OUTPUT
['a.py', 'z.py']
Follow the reasoning
  1. Identify the line that changes the rule.
  2. Run the corrected sample with the same input.
03 / 03

Verify the boundary

Compare fnmatch.fnmatch with fnmatch.fnmatchcase on mixed-case sample names. The latter applies the pattern exactly as written, making the lesson's rule portable. Apply sorted() to the selected names so tests and UI lists have an explicit order. Use fnmatch.fnmatchcase(name, '*.py') for a case-sensitive in-memory filter, and sorted(...) for deterministic results. When searching actual directories, use pathlib.Path.glob or glob.glob but validate the search root and do not assume a pattern by itself enforces access controls.

The extra path rule is an application choice, not a guarantee supplied by the pattern. The final run includes a nested name like src/main.py. A plain fnmatchcase over full strings matches the *.py suffix even across a slash; if the requirement is top-level files only, reject names containing a path separator first. The task filters supplied names and explicitly rejects any candidate containing a path separator. Shell-style glob patterns are not regular expressions. Asterisks and question marks have different syntax from re, and a pattern is not a security boundary. Define whether matching is case-sensitive and whether paths or base names are allowed before using results to delete or publish files.

PYTHON
from fnmatch import fnmatchcase
names = ['src/main.py', 'main.py']
print([n for n in names if '/' not in n and fnmatchcase(n, '*.py')])
EXPECTED OUTPUT
['main.py']
Follow the reasoning
  1. Try the documented edge case.
  2. Explain why the output satisfies the function contract.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnReturn a sorted list of case-sensitive, top-level *.py names from the supplied list. Ignore names containing / or backslash. The prepared output is ['a.py', 'main.py'].
Need a hint?

Use fnmatchcase, reject path separators, and wrap the generator in sorted().

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.Which function matches with exact case on all platforms?

    02.Does glob promise a sorted result?

    03.Is *.py a regular expression?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    The visible failure

    COMMON MISTAKE
    fnmatch.fnmatch(name, '*.py')
    FIX
    fnmatch.fnmatchcase(name, '*.py')

    What happens: fnmatch may normalize case according to the host operating system.
    Use fnmatch.fnmatchcase(name, '*.py') for a case-sensitive in-memory filter, and sorted(...) for deterministic results. When searching actual directories, use pathlib.Path.glob or glob.glob but validate the search root and do not assume a pattern by itself enforces access controls.

    Missing the edge condition

    COMMON MISTAKE
    return [name for name in names if name.endswith('.py')]
    FIX
    return sorted(name for name in names if '/' not in name and fnmatch.fnmatchcase(name, '*.py'))

    What happens: The final run includes a nested name like src/main.py.
    Shell-style glob patterns are not regular expressions. Asterisks and question marks have different syntax from re, and a pattern is not a security boundary. Define whether matching is case-sensitive and whether paths or base names are allowed before using results to delete or publish files.

    Browse this lesson’s problem fixes

    Sources and further reading