A Surprising Python Gotcha - Clashing Package Names
Gotchas / “Footguns” in Python - Rare, but Not Unprecedented
When I first got introduced to Python, I have to admit that my initial reaction was not favorable; some of the syntax - mixing plain english with things like dictionary comprehension and odd indenting rules just seemed off-putting. But, very quickly, Python grew on me. Primarily because I favor simplicity and ease-of-use over complexity - which is something that Python excels at. Now I would rank Python as one of my favorite programming languages!
That isn’t to say that Python doesn’t have “gotchas” that break the ethos of keep-it-simple. What is likely the most notorious is the infamous “mutable default arguments” - how Python evaluates function argument defaults only once, at the point of definition!
from itertools import count
from typing import List
class Soup:
def __init__(self, main_ingredients: List[str], seasonings: List[str] = []) -> None:
self.main_ingredients = main_ingredients
self.seasonings = seasonings
def add_seasonings(self, new_seasonings: List[str]):
self.seasonings.append(*new_seasonings)
def make(self):
return self.main_ingredients + self.seasonings
regular_soup = Soup(["broth", "vegetables"])
regular_soup.add_seasonings(["salt"])
print(regular_soup.make())
# > ['broth', 'vegetables', 'salt']
for _ in range(4):
another_regular_soup = Soup(["broth", "vegetables"])
another_regular_soup.add_seasonings(["salt"])
no_salt_soup = Soup(["broth", "vegetables"])
print(no_salt_soup.make())
# > ['broth', 'vegetables', 'salt', 'salt', 'salt', 'salt', 'salt']
# UH OH
I Didn’t Think Python’s Module System Could Get Worse, and then…
The impetus behind this post is something I just recently learned - THE HARD WAY - a somewhat shocking footgun in how Python handles package installation and namespacing.
To be fair to Python, the issue I’m about to describe kind of straddles the Python language itself and 3rd party package managers.
However, I don’t think that means Python can deflect blame; I see Python abdicating its role in package management / module resolution as a huge part of the problem and how we end up with messy disparate systems.
What Happened
I was working on a project that already used a package called django-ninja, and used uv to add a new dependency to the project, completely unrelated to django-ninja, for document parsing:
uv add "kreuzberg[all]"
Suddenly, everything broke - the Django backend basically imploded with a ton of errors - all related to places where I was using django-ninja.
There was an immediate clue as to what went wrong though - since I was running Django in “dev” mode with its file watcher, it caught an interesting file change when I ran uv add - something like this:
[run:backend:dev] /Users/joshuatz/jtzdev/my_project/.venv/lib/python3.13/site-packages/ninja/__init__.py changed, reloading.
Huh!? That seems odd… why did a seemingly unrelated package just touch files in django-ninja’s module folder?
I wonder…
❯ uv tree --package kreuzberg
Resolved 277 packages in 8ms
kreuzberg v4.9.7
├── easyocr v1.7.2 (extra: all)
│ ├── ninja v1.13.0
Uh-oh. And, wait a second, why did django-ninja install into site-packages/ninja instead of site-packages/django_ninja?
❯ pkg="django-ninja"; tmp="$(mktemp -d)" && pip download -q --only-binary :all: --no-deps -d "$tmp" "$pkg" && unzip -l "$tmp"/*.whl
Length Date Time Name
--------- ---------- ----- ----
1162 03-18-2026 20:06 ninja/__init__.py
1525 03-18-2026 20:06 ninja/conf.py
...
❯ pkg="ninja"; tmp="$(mktemp -d)" && pip download -q --only-binary :all: --no-deps -d "$tmp" "$pkg" && unzip -l "$tmp"/*.whl
Length Date Time Name
--------- ---------- ----- ----
1533 08-11-2025 14:46 ninja/__init__.py
1576 08-11-2025 14:46 ninja/ninja_syntax.pyi
...
WOW! So, two major surprises here:
- Although Python registries (i.e. pypi) enforce globally unique package names, they do NOT enforce unique namespaces / top-level directory names.
- If there is a naming collision,
pip(and other tools, such asuv) will SILENTLY overwrite the directory contents with the new package, with zero warnings or installation errors!!!
Just this alone blew my mind - I wouldn’t be accepting of this behavior from any package manager, and having it coming from the Python ecosystem is especially jarring considering Python’s insistence on keeping things simple.
But… it gets worse. 😬
How to Workaround Package Namespace Site-Packages Clobbering
When I said it gets worse… if you read through the issues linked above, what you will find is that - at the time of writing this post (May 2026) - this still does not have a working built-in solution. It seems like the Python community only offers the following solutions at the moment:
- Pester the package author / maintainer(s) to refactor their package to use a different namespace
- Fork the package yourself, patch it, and then use your patched fork
Uh, big yikes y’all. Not a good look.
While I agree that package maintainers should try to avoid namespace collisions themselves, I disagree that the go-to solution should be to either pester the package author to fix their entire package or fork it yourself to fix it. This is just… bad.
This is a fixed problem in other build systems - e.g. most major JavaScript build systems allow for module aliasing (e.g. tsconfig’s compilerOptions.paths) - it is bizarre that this has not been fixed in Python, and even more so that it doesn’t even warn you when it happens.
Anyways, I did want to share a (hacky) way that I found that is a bit of a compromise. I didn’t want to maintain a fork of django-ninja and have to deal with hosting, distribution, etc., but I also didn’t think it likely that this would get fixed in the upstream package anytime soon. My workaround ended up being to write a small script to:
- Fetch the distributed code (tarball) from pypi / pypa
- Automatically rename the root directory and replace import strings, to use a new “alias” / namespace that won’t conflict with others
- Save the patched package as a locally-vendored directory, and install it into the project as an editable package
Let me be clear: I hate that I had to do this, and do not recommend this as a long-term solution. But in case it is useful to others, here is this approach as a reusable script: joshuatz/py-package-aliaser.
Now I can just run my py-package-aliaser script to install django-ninja under /django_ninja instead of /ninja, and then:
# Now, instead of this:
from ninja import ModelSchema
# I can use the new alias
from django_ninja import ModelSchema
No more overwritten site-packages!