A correct Python scanner must normalize package names before matching them to advisories. Under the packaging rule, case is ignored and any run of dots, hyphens or underscores counts as a single hyphen. A tool that compares raw strings can report a clean result for a package that does have an advisory.
What the rule says
PEP 503 defines the normalized name as the lowercase name with every run of the characters dot, hyphen and underscore replaced by one hyphen. The PEP is marked Final, and the maintained version now lives on the PyPA specifications site.
In practice that means these all point to the same project:
| Written in a file | Normalized |
|---|---|
| Django | django |
| Foo_Bar | foo-bar |
| foo.bar | foo-bar |
| FOO__BAR | foo-bar |
Your requirements file, your lockfile, a vendor SBOM and an advisory record can each use a different spelling. Nothing is wrong with any of them. The packaging spec treats them as equal.
Why this matters for scan accuracy
Advisory databases and manifests are written by different people at different times. If the advisory says foo-bar and your file says Foo_Bar, a matcher that skips normalization finds nothing. The scan finishes, the exit code is zero, and the report looks clean.
This is a false negative, the quiet kind. No warning appears, because as far as the tool knows there was simply no match. The same issue can flip the other way in a deduplication step, where two spellings of one package get counted twice.
We have not measured how often any specific tool gets this wrong, and you should not assume yours does or does not. Test it.
A ten minute test
- Pick a Python package version you know has a published advisory. Use a real advisory from a public database and confirm the affected range yourself.
- Create three small requirements files that pin that exact version. Write the name in its canonical form, in mixed case, and with underscores or dots swapped for hyphens.
- Scan each file with the same tool and the same database.
- Compare. All three should report the same finding.
- Repeat with a lockfile format such as a poetry or uv lock, since those are written by tools with their own spelling habits.
If one file produces a different answer, you have found a matching gap worth reporting to the tool maker. For a wider method, see how to test a scanner for missed vulnerabilities with a seeded test.
Other places names drift
Normalization is not unique to Python. npm names are case sensitive in some contexts, Maven uses a group and artifact pair, and Linux distributions split source and binary names. The common lesson is to ask a vendor how matching works per ecosystem, and to treat a surprising zero as a prompt to look closer.
Safeguard's SBOM and dependency scanning builds an inventory of components with their ecosystem identifiers, and the free scan at https://gold.safeguard.sh/scan lets you try a manifest of your own.
Try it yourself
Open the dependency inventory demo and look at how each component carries an ecosystem and a name. Notice that matching starts from that identifier.
Click through it at your own pace, or open it full screen.
Quick check
Are Foo_Bar and foo-bar the same PyPI package?
Yes. PEP 503 normalizes names by lowercasing and collapsing runs of dots, hyphens and underscores into one hyphen, so both map to foo-bar.
Can a scanner miss a vulnerable Python package because of spelling?
It can if it matches raw strings. The test above shows whether your tool normalizes names before matching.
Sources
Last reviewed: 19 October 2026.