Guide for setting up local PyPI servers to host and serve Python packages. This skill should be used when tasks involve creating a local PyPI repository, serving Python packages over HTTP, building distributable Python packages, or testing pip installations from a custom index URL.
This skill provides guidance for creating local PyPI servers to host and distribute Python packages.
Before planning the approach, gather critical environment information:
Check Python version: Run python3 --version to determine compatibility constraints
cgi module, breaking many older tools like pypiserverCheck available system tools: Verify what utilities exist
which ps pkill lsof killwhich curl wget ncpip list to see pre-installed packagesIdentify port availability: Check if target ports are free before starting servers
| Condition | Recommended Approach |
|---|---|
| Python 3.13+ | Use python3 -m http.server with proper directory structure |
| Python 3.12 or earlier | Either pypiserver or http.server works |
| Simple single-package hosting | http.server is sufficient |
| Full PyPI mirroring needed | Consider pypiserver or devpi |
For most local PyPI hosting tasks, use python3 -m http.server with the correct directory structure. This approach:
Pip expects a specific directory structure when using --index-url:
server_root/
└── simple/
└── <package-name>/
└── <package-name>-<version>-py3-none-any.whl
Critical Details:
simple/ directory must be at the root of the served directorysimple/, not from within itpackage_name/
├── setup.py
├── pyproject.toml (optional but recommended)
└── package_name/
├── __init__.py
└── module.py
from setuptools import setup, find_packages
setup(
name="package-name",
version="0.1.0",
packages=find_packages(),
)
# Build wheel and source distribution
python3 -m pip install build
python3 -m build
# Output appears in dist/
mkdir -p pypi-server/simple/packagename/
cp dist/*.whl pypi-server/simple/packagename/
cd pypi-server
python3 -m http.server 8080
Important: Start the server from the directory that contains simple/, not from within simple/.
Test that the structure is correct:
curl http://localhost:8080/simple/
curl http://localhost:8080/simple/packagename/
curl -I http://localhost:PORT/curl http://localhost:PORT/simple/curl http://localhost:PORT/simple/packagename/curl -I http://localhost:PORT/simple/packagename/file.whlpip install --index-url http://localhost:PORT/simple packagename==version
import packagename
# Test core functionality
Symptom: OSError: [Errno 98] Address already in use
Solutions:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
SO_REUSEADDR optionSymptom: 404 Not Found when pip tries to access /simple/
Cause: Server started from wrong directory or missing simple/ prefix
Solution: Verify the URL http://localhost:PORT/simple/ returns a directory listing
Symptom: ModuleNotFoundError: No module named 'cgi' when using pypiserver
Cause: Python 3.13 removed the deprecated cgi module
Solution: Use python3 -m http.server instead of pypiserver
Symptom: Cannot find or kill background processes (ps/pkill not available)
Solutions:
import subprocess
proc = subprocess.Popen(['python3', '-m', 'http.server', '8080'])
# Save proc.pid for later termination
/proc filesystem on Linux: ls /proc/*/cmdlineSymptom: source: not found when sourcing files
Cause: Using source in sh shell (source is a bash feature)
Solution: Use . instead of source for POSIX compatibility:
. ./venv/bin/activate
Symptom: Package not found despite correct structure
Cause: Pip normalizes package names (underscores to hyphens, lowercase)
Solution: Use lowercase names and be consistent with hyphens/underscores
For better process control and port reuse, consider a custom server script:
#!/usr/bin/env python3
import http.server
import socketserver
import os
PORT = 8080
DIRECTORY = "/path/to/pypi-server"
os.chdir(DIRECTORY)
class ReuseAddrServer(socketserver.TCPServer):
allow_reuse_address = True
handler = http.server.SimpleHTTPRequestHandler
with ReuseAddrServer(("", PORT), handler) as httpd:
print(f"Serving at port {PORT}")
httpd.serve_forever()
simple/ prefixsimple/packagename/