Safeguard
Supply Chain•

PyPI name normalization: why Foo_Bar and foo-bar are the same package

PyPI treats dots, dashes and underscores as one character and ignores case. See the exact rule and what it means for typosquat checks and requirements.

Safeguard Security Team
3 min read

In Python packaging, names are compared after normalization: runs of dots, dashes and underscores become a single dash, and everything is lowercased. So Foo_Bar, foo-bar and foo.bar all mean the same project. That helps users, but it shapes how lookalike package checks should work.

The rule

PEP 503 defines normalization in one short function:

import re

def normalize(name):
    return re.sub(r"[-_.]+", "-", name).lower()

The regular expression matches any run of one or more dots, dashes or underscores. Each run is replaced with one dash, and the result is lowercased. The PEP states it plainly: the name should be lowercased with all runs of those characters replaced by a single dash.

Examples:

  • Foo_Bar becomes foo-bar.
  • foo.-_bar becomes foo-bar.
  • Foo__Bar becomes foo-bar.

What this means for installs

When you write a name in a requirements file, the installer and the index compare the normalized form. A requirement for Flask_Cors and one for flask-cors resolve to the same project. This is why you will see both spellings in the wild without confusion.

For review, it means a text diff can mislead you in two directions.

  1. A pull request that changes a spelling of a name, from one separator to another, is usually cosmetic. It resolves to the same project.
  2. A pull request that adds a name looking like an existing dependency is not cosmetic. If the normalized names differ, it is a different project.

Using it in a lookalike check

If you build a reviewer script or a policy check, normalize first, then compare.

  1. Normalize every name in the manifest and in your approved list.
  2. Treat equal normalized names as the same package. Do not flag them as typosquats of each other.
  3. For different normalized names, compute a small edit distance against your approved list and against well known projects, and flag close ones for human review.
  4. Look beyond edit distance. Swapped words, added suffixes such as a "-utils" or "-lib" tail, and dropped letters are common patterns for deceptive names.
  5. Check publisher, upload date and download history before trusting a new name, not only the spelling.

Edit distance alone throws false alarms, and it misses names that are far in spelling but close in meaning. Treat it as a filter that feeds a person, not a verdict.

Normalization is not a security control

The rule exists to make names consistent. It does not prevent someone registering a different name that looks similar to a person. It also does nothing about a compromised legitimate project. Pair name checks with hash pinning, a lockfile and a review of who publishes the package.

Safeguard scans dependencies and can compare a manifest against known risky packages; a free scan at https://gold.safeguard.sh/scan shows what a manifest pulls in.

Quick check

Are requests and Requests the same PyPI project?

Yes. Names are lowercased during normalization, so case alone never creates a different project.

Is a name that differs by one letter normalized to the same name?

No. Normalization only collapses separators and case. A one letter difference is a different project and deserves review.

Sources

Last reviewed: 19 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.