Open Source Security

Managing Python Package Namespace Conflicts

Python's flat namespace creates real security problems. Here is how namespace packages, shadowing, and install order interact, and how to avoid the surprises.

Nayan Dey
Senior Security Engineer
6 min read

Python packaging has a flat namespace by default, and that flat namespace is the foundation of more than one class of supply chain attack. Typosquatting works because names look similar. Dependency confusion works because public and private indexes share the same namespace. Namespace package shadowing works because Python's import resolution is order-dependent and most developers have never thought about the order.

This is not a theoretical issue. It is a class of mistakes I see in real codebases, in real SBOMs, causing real incidents.

Two Kinds of Namespace Packages, Both Risky

Python has historically had two namespace package mechanisms.

PEP 420 implicit namespace packages (Python 3.3+) let multiple distributions contribute to the same dotted-name hierarchy without any explicit declaration. If acme-auth and acme-billing both ship under the acme namespace, import acme.auth and import acme.billing both work even though they come from different wheels. There is no __init__.py at the acme level.

pkg_resources-style namespace packages (older, still seen) use an __init__.py with pkg_resources.declare_namespace(__name__) or similar. Setuptools also supports pkgutil-style namespace packages via __init__.py containing __path__ = __import__('pkgutil').extend_path(__path__, __name__).

The security implication of both: the acme top-level name is claimable by any package that registers under it. If your internal acme-auth package lives on a private index and a public PyPI package publishes acme-auth first, dependency confusion can install the public one. The namespace does not enforce anything about ownership.

This bit Microsoft's core-parts team, among many others, in the 2021 dependency confusion incidents that Alex Birsan documented. The attack worked against dozens of companies using namespace packages for internal organization.

Prevent Namespace Hijacking on PyPI

If your organization uses a namespace like acme-* for internal packages, claim it on PyPI. Register placeholder packages under the relevant names, owned by your organization. Even if you do not publish anything, you own the name and an attacker cannot squat it.

This is not free — PyPI's naming policy allows the PyPI maintainers to reclaim names in certain cases — but claiming prevents the drive-by squat.

Better yet, configure your package manager to never resolve your namespace from public sources. Poetry's priority = "explicit", pip's --index-url with strict scoping, Pipenv's per-package index declaration. The namespace package approach should be paired with namespace isolation in the resolver.

How Does Install Order Change Behavior?

Python resolves imports by walking sys.path. For namespace packages, every directory on sys.path that contains a matching directory contributes. For regular packages, the first match wins.

If your environment has two packages that both claim the name acme.auth — one from your private index, one from PyPI — you can end up with one of two outcomes depending on installation order and installer behavior. This is not supposed to happen (PyPI refuses conflicting package names for the same project), but it can happen across different package names that ship the same module name. I have seen a package named acme-internal-auth install a Python module named acme.auth, colliding with a PyPI package acme-auth-utils that also installed acme.auth (with different contents).

The import that wins is whichever .dist-info was written last, because pip overwrites without warning. You can end up with a Frankenstein install where metadata for one package coexists with code from another. Debugging this is miserable.

The mitigation is to enforce non-colliding module names. pip check catches some of this but not all. SBOM-level analysis that notes "these two packages install modules with overlapping names" is more reliable.

Typosquatting and Visual Confusion

requests is the most commonly typosquatted package on PyPI. Historical incidents include reqeusts, requsts, requestts, and Unicode-lookalike variants using Cyrillic letters. PyPI has improved its detection — the normalized_name check catches many straight typos — but adversarial names still land.

2022's ctx incident: an attacker took over a dormant package name, published a malicious version, and harvested AWS credentials from everyone who auto-upgraded. Package name squatting plus abandoned-name takeover is a combined risk.

Defenses:

Pin exact versions. requests==2.31.0 does not upgrade silently.

Require hashes. --require-hashes makes name squatting insufficient — the attacker also has to match the hash, which requires compromising the actual publisher.

Review new dependencies. When a PR adds pybeautifulsoup (a real typosquat that existed for a time) or colourama (another historical example), a human should notice.

Use SBOM-based monitoring. A tool that knows your full dependency set can alert when a package is flagged by PyPI for name squatting, even if you do not upgrade.

The src/ Layout Prevents Accidental Local Shadowing

A subtle namespace issue that happens during development: if your project is laid out with a top-level directory matching an installed package name, running python script.py from the project root puts . on sys.path and the local directory shadows the installed package.

A project with mypackage/__init__.py at the top level will have import mypackage resolve to the local directory, not to an installed version with the same name. Usually this is desired. Occasionally it causes ghostly behavior: tests pass locally because they pick up the local code, fail in CI because CI runs against an installed version.

The src/ layout fixes this. Put your package under src/mypackage/, do not add src/ to sys.path, and rely on the installed version being the only one Python finds. This is a convention, not a security feature, but it eliminates a class of "works on my machine" bugs that are sometimes security-relevant (the local version has a fix the installed version does not).

Entry Point Namespace Collisions

A separate namespace: entry points. When two installed packages register entry points in the same group with the same name, the resolution is implementation-defined. importlib.metadata.entry_points returns a sequence, but which entry an application uses depends on how the application iterates.

I have seen two installed pytest plugins register the same plugin name, resulting in pytest using whichever was installed later, and the team not noticing for months that the "older" plugin had been silently disabled. In a security context, imagine two packages that both register a flask.commands entry named deploy.

Audit entry points in security-sensitive groups. importlib.metadata makes this easy in CI.

Private Package Naming Strategy

If you are starting fresh with internal Python packages, use a scoped namespace like @acme/ mentally — Python does not support that syntax, but you can use a prefix like acme_ consistently. Treat acme_ as "always from the private index" in your resolver config. Register acme- (with the hyphen, the wheel normalization) on PyPI as a placeholder to prevent squatting.

Avoid names that collide with well-known public packages. acme_requests (named after the requests library) is asking for confusion. acme_http_client is clearer.

How Safeguard Helps

Safeguard's SBOM analysis flags namespace package configurations where internal and public packages could collide, and highlights dependency resolver configurations that are vulnerable to dependency confusion. Reachability analysis traces which imports in your code actually resolve to which installed package, surfacing cases where install order matters. Griffin AI can generate claim-the-namespace PRs for your private prefixes when you do not yet own them on PyPI, and draft resolver configuration changes to make private indexes explicit. Policy gates block deploys when a new dependency shares a module-name prefix with your private namespace, so a typosquat or confusion attempt fails at the gate rather than in production.

Never miss an update

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