Skip to content
← Blog

Technical explainer

Vibe coding in Python: the wheel problem

4 min read

Python's supply-chain story is different from npm's, in mostly worse ways. Knowing the differences matters if you're vibe coding Python — building a quick FastAPI service, scaffolding a Streamlit app, prototyping an LLM pipeline.

Here's what actually runs when you type pip install.

Three things pip can do at install time

Python packages come in three flavors:

  1. Pure Python wheels (.whl). These are zips. pip install unzips into your site-packages. No code runs at install — code runs when you import the package later.

  2. Source distributions (sdists) (.tar.gz). These contain a setup.py or pyproject.toml. pip install runs the build step, which can execute arbitrary Python. setup.py is a script; it can read files, make network calls, do whatever it wants. This has been the supply-chain attack vector of choice on PyPI for years.

  3. Binary wheels with native extensions (*.whl containing .so / .dylib / .pyd). The install is just an unzip, but the native code runs when imported. If the binary was compiled with malicious behavior, you don't get a chance to review the source — you'd need to decompile.

Most packages publish both a wheel and an sdist. pip prefers the wheel by default, which is safer. But:

  • If no wheel exists for your platform, pip falls back to building from sdist. That's setup.py running.
  • If you set --no-binary :all: (some security-conscious shops do this), pip always builds from sdist.
  • If you use uv or poetry, the same rules apply — they're faster, not safer.

The wheel problem specifically

.whl files are zips that pip extracts and trusts. There's no signature on the wheel. There's no verification that the code inside matches the source on GitHub. If an attacker compromises a maintainer's PyPI token, they can:

  1. Build a wheel locally with malicious native code
  2. Upload it as package-1.2.3-cp312-manylinux2014_x86_64.whl
  3. Wait for every CI run and every developer install to pull it

The malicious behavior runs when import package happens. By then your CI has provisioned secrets into the runner; your laptop has its credentials loaded. The damage path is the same as npm postinstall.

This is the aiocpa attack pattern (November 2024). A maintainer's account was compromised; a malicious wheel was published that scraped crypto wallet seeds at import time. It was live for a week.

Why uv and poetry don't solve it

uv is fast because it parallelizes everything and writes its own resolver. It doesn't change the trust model — the same package, the same setup.py, the same wheel run with the same privileges.

poetry adds dependency locking with content hashes. That's helpful: if your lockfile pins package@1.2.3 with a specific hash, and an attacker republishes 1.2.3 with a different binary, poetry install will refuse. Useful — but only after you've installed the original 1.2.3 at least once and locked the hash. The first install of a freshly-compromised version still gets you.

pipx runs apps in isolated environments. That doesn't help with the install step — pipx install <evil-package> will still execute the evil package's setup.py or import logic.

What actually defends Python installs

Same shape as npm, different mechanism:

At the gate: a local proxy in front of PyPI scores each package on the equivalent signals — OSV vulnerability data, install-script analysis (setup.py patterns: subprocess calls, base64-decoded payloads, env-var enumeration, network calls), maintainer fingerprint changes between versions, dormant-revival, cooling windows, dependency-confusion against your protected internal scopes.

At the OS sandbox: every pip install, uv pip install, poetry install, or pipx install runs inside an OS-level sandbox so the install code (whether it's a Python setup.py or a native wheel) can't read your ~/.aws, ~/.ssh, or ~/.pypirc. Even if a wheel ships malicious native code that ran on import, the file system damage and exfil path are blocked at the kernel layer.

The vibe coder's Python checklist

You're going to scaffold lots of FastAPI / Streamlit / LangChain projects. A few habits that cost nothing:

  1. Use uv for speed, but don't assume it adds safety.
  2. Pin major dependencies in pyproject.toml (langchain ^0.3 not langchain *). Stops version-jump hijacks.
  3. Generate lockfiles with uv lock or poetry lock. Use them in CI.
  4. Use virtual environments per project. A malicious package in one venv can't import from another.
  5. Route installs through a gate. Same one-line setup as npm:
veln onboarding
veln wrapper on

That wraps pip, pip3, uv, poetry, and pipx. Every install — whether you typed it or your editor's agent ran it — goes through the same scoring pipeline. The wheel that would have stolen your AWS keys is refused before it downloads.

The honest bit

Python's install model is harder to defend than npm's because the surface is bigger: setup.py arbitrary code execution, native wheels, multiple package managers, and a registry that doesn't yet have widespread publish attestation. The fix for vibe coders is the same as for npm coders: a check at the registry layer that runs every install, and a sandbox that contains anything that slips past. Nothing about Python lets you skip that step.