BadPie: Bake it ‘Til You Fake It
Blog post on how to use alternative package indexes with pip and introducing a proof-of-concept Python package index/mirror proxy tool
In this post, I share some research into Python package management utilities — with a focus on pip, the most widely used. Specifically, I wanted to explore the different ways you can tell pip to use an alternative package index or mirror.
It turns out there are many different ways:
CLI switches
pip install --index-url https://example.org/simple requests
pip install -i https://example.org/simple requests
Persistent configs
(test-env) python3 -m pip config debug
env_var:
env:
global:
/Library/Application Support/pip/pip.conf, exists: False
site:
/Users/dtm/project/badpie/test-venv-project/test-env/pip.conf, exists: False
user:
/Users/dtm/.pip/pip.conf, exists: False
/Users/dtm/.config/pip/pip.conf, exists: False
You can set these using pip itself i.e.
pip config set global.index-url https://example.org/simple
Environment variables
export PIP_INDEX_URL=https://example.org/simple
export PIP_EXTRA_INDEX_URL=https://example.org/simple
requirements.txt (direct) - with --index-url
inside the file
--index-url https://example.org/simple
requests
requirements.txt (indirect) - using -r other.txt
, where the referenced file hides the index
-r other.txt
requests
where other.txt
--index-url https://example.org/simple
But how does pip work?
Well you can run pip with a -vvv flag to check out what's happening under the hood:
pip3 install --index-url https://pypi.org/simple -vvv requests
Simplified, pip does three things:
- Hits the index.
- Grabs the package metadata.
- Pulls a wheel package (
.whl
) - a ZIP of the code.

The default PyPI index also provides SHA256 hashes for packages and their metadata, helping verify integrity:
curl -s https://pypi.org/simple/requests/|grep 2.32.5|grep .whl
<a href="https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl#sha256=2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6" data-requires-python=">=3.9" data-dist-info-metadata="sha256=65b5a08da81f4915513e760965ff14b7518f65b7bb3efe0dab364bbcc4d40cb0" data-core-metadata="sha256=65b5a08da81f4915513e760965ff14b7518f65b7bb3efe0dab364bbcc4d40cb0">requests-2.32.5-py3-none-any.whl</a><br />
BadPie

Enter BadPie — my proof-of-concept transparent proxy PyPI mirror that demonstrates how code can be modified inline and pass hash checks
- BadPie proxies your request to real PyPI, grabs the requested package.
- It injects code—for example, by appending a print("hello world") to __init__.py.
- It recalculates (or strips) the SHA-256 hash
- It caches the modified wheel
- You get the package, pip sees a valid hash and installs it.
Check the config in the BadPie script itself:
You probably just need to define MODIFICATION_CODE - what code you want to inject and PACKAGES_TO_MODIFY - the list of packages to modify all others are served without modification.
PYPI_URL = "https://pypi.org/simple"
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CACHE_DIR = os.path.join(BASE_DIR, 'cache')
MODIFIED_DIR = os.path.join(BASE_DIR, 'modified')
os.makedirs(CACHE_DIR, exist_ok=True)
os.makedirs(MODIFIED_DIR, exist_ok=True)
MODIFICATION_CODE = '''
print("hello world")
'''
# Packages to modify
PACKAGES_TO_MODIFY = ["requests"]
Start BadPie:
python3 app_hash_modify.py
* Running on http://127.0.0.1:5000
* Debug mode: off
Point pip to your BadPie proxy (we’ll pretend it’s running at https://example.org/simple
), then install a package through that index:
(test-env) pip install -i https://example.org/simple requests
Looking in indexes: https://example.org/simple
Collecting requests
Downloading https://example.org/simple/requests/requests-2.32.5-py3-none-any.whl.metadata (4.9 kB)
Collecting charset_normalizer<4,>=2 (from requests)
Downloading https://example.org/simple/charset-normalizer/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl.metadata (36 kB)
Collecting idna<4,>=2.5 (from requests)
Downloading https://example.org/simple/idna/idna-3.10-py3-none-any.whl.metadata (10 kB)
Collecting urllib3<3,>=1.21.1 (from requests)
Downloading https://example.org/simple/urllib3/urllib3-2.5.0-py3-none-any.whl.metadata (6.5 kB)
Collecting certifi>=2017.4.17 (from requests)
Downloading https://example.org/simple/certifi/certifi-2025.8.3-py3-none-any.whl.metadata (2.4 kB)
Downloading https://example.org/simple/requests/requests-2.32.5-py3-none-any.whl (207 kB)
Downloading https://example.org/simple/certifi/certifi-2025.8.3-py3-none-any.whl (161 kB)
Downloading https://example.org/simple/charset-normalizer/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl (205 kB)
Downloading https://example.org/simple/idna/idna-3.10-py3-none-any.whl (70 kB)
Downloading https://example.org/simple/urllib3/urllib3-2.5.0-py3-none-any.whl (129 kB)
Installing collected packages: urllib3, idna, charset_normalizer, certifi, requests
Successfully installed certifi-2025.8.3 charset_normalizer-3.4.3 idna-3.10 requests-2.32.5 urllib3-2.5.0
(test-env) python3
Python 3.12.6 (v3.12.6:a4a2d2b0d85, Sep 6 2024, 16:08:03) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
hello world
How to defend
To reduce the risk:
- Treat
requirements.txt
files as code — they need the same review discipline as source. - Monitor your network and pip (and uv) processes for unusual metadata requests to non-PyPI domains i.e. NOT pypi.org and files.pythonhosted.org.
- Watch for unexpected pip config or environment variable changes.
The same applies to the uv package manager, with a few extra places to check in addition to those desribed for pip:
UV_INDEX_URL
UV_DEFAULT_INDEX
pyproject.toml
uv.toml
(test-env) uv pip install -i https://example.org/simple requests
Using Python 3.12.6 environment at: test-env
Resolved 5 packages in 10.19s
Installed 5 packages in 9ms
+ certifi==2025.8.3
+ charset-normalizer==3.4.3
+ idna==3.10
+ requests==2.32.5
+ urllib3==2.5.0
(test-env) python3
Python 3.12.6 (v3.12.6:a4a2d2b0d85, Sep 6 2024, 16:08:03) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
BadPie was here!
👉 You can explore BadPie yourself on GitHub.