Fix: Python Packaging Not Working — Build Fails, Package Not Found After Install, or PyPI Upload Errors
Quick Answer
How to fix Python packaging issues — pyproject.toml setup, build backends (setuptools/hatchling/flit), wheel vs sdist, editable installs, package discovery, and twine upload to PyPI.
The Problem
Your package builds but can’t be imported after installing:
pip install dist/mypackage-0.1.0-py3-none-any.whl
python -c "import mypackage"
# ModuleNotFoundError: No module named 'mypackage'Or pip install -e . fails:
pip install -e .
# ERROR: File "setup.py" or "setup.cfg" not found
# OR:
# ERROR: pyproject.toml does not contain a [build-system] tableOr the build fails with a backend error:
python -m build
# * Creating venv isolated environment...
# * Installing packages in isolated environment... (setuptools>=61)
# ERROR: Could not build wheels for mypackage, which is required to install...Or twine upload fails with authentication or metadata errors:
twine upload dist/*
# HTTPError: 400 Bad Request — File already exists.
# OR:
# InvalidDistribution: Invalid distribution metadataWhy This Happens
Python’s packaging ecosystem has evolved significantly, and mixing old and new conventions causes failures:
- Missing or incorrect
pyproject.toml— PEP 517/518 madepyproject.tomlthe standard. A project without a[build-system]table falls back to legacysetup.pybehavior, whichpipmay refuse to use. - Package not found after install — the
[tool.setuptools.packages.find]or equivalent discovery config is wrong, so the package directory isn’t included in the distribution. - Wrong package structure — flat layout (
src/mypackage/) vs src layout (src/mypackage/) require different discovery settings. Using the wrong setting includes the wrong files. - Editable install requires PEP 660 —
pip install -e .with apyproject.tomlbackend requires the backend to support PEP 660. Older versions of setuptools don’t. Update setuptools. - Version conflicts in the wheel filename — the version in
pyproject.tomlmust match what PyPI expects. Re-uploading the same version fails; you must bump the version.
Fix 1: Set Up pyproject.toml Correctly
Modern Python projects use pyproject.toml exclusively — no setup.py or setup.cfg needed:
# pyproject.toml — minimal working example (setuptools backend)
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "mypackage"
version = "0.1.0"
description = "A short description of mypackage"
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.9"
authors = [
{ name = "Your Name", email = "[email protected]" }
]
keywords = ["keyword1", "keyword2"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
# Runtime dependencies
dependencies = [
"requests>=2.28.0",
"pydantic>=2.0.0",
]
# Optional dependencies (extras)
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov",
"black",
"mypy",
]
docs = [
"sphinx",
"sphinx-rtd-theme",
]
# URLs shown on PyPI
[project.urls]
Homepage = "https://github.com/yourname/mypackage"
Repository = "https://github.com/yourname/mypackage"
Documentation = "https://mypackage.readthedocs.io"
"Bug Tracker" = "https://github.com/yourname/mypackage/issues"
# Console scripts (CLI entrypoints)
[project.scripts]
mypackage-cli = "mypackage.cli:main"Choose the right build backend:
# Option 1: setuptools (most flexible, most packages use this)
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.backends.legacy:build"
# Option 2: hatchling (modern, zero-config for simple packages)
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# Option 3: flit (for pure-Python packages with minimal config)
[build-system]
requires = ["flit_core>=3.2"]
build-backend = "flit_core.buildapi"
# Option 4: poetry-core (if you use Poetry)
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"Fix 2: Fix Package Discovery
The most common reason a package installs but can’t be imported is incorrect discovery config.
Flat layout — your package is directly in the project root:
myproject/
├── mypackage/
│ ├── __init__.py
│ └── module.py
├── tests/
├── pyproject.toml
└── README.md# pyproject.toml — flat layout (default for setuptools)
[tool.setuptools.packages.find]
where = ["."] # Look in the root directory
exclude = ["tests*"] # Exclude test directoriesSrc layout — recommended for larger projects:
myproject/
├── src/
│ └── mypackage/
│ ├── __init__.py
│ └── module.py
├── tests/
├── pyproject.toml
└── README.md# pyproject.toml — src layout
[tool.setuptools.packages.find]
where = ["src"] # Look inside src/Verify what’s included in your package:
# Build and inspect the wheel contents
python -m build --wheel
python -m zipfile -l dist/mypackage-0.1.0-py3-none-any.whl
# Should show:
# mypackage/__init__.py
# mypackage/module.py
# mypackage-0.1.0.dist-info/METADATA
# etc.Include non-Python files:
# pyproject.toml — include data files
[tool.setuptools.package-data]
mypackage = ["*.json", "data/*.csv", "templates/*.html"]
# Or use MANIFEST.in for sdist
# MANIFEST.in
# include mypackage/data/*.csv
# include mypackage/templates/*.htmlHatchling package discovery:
# pyproject.toml with hatchling
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage"]
# Or for multiple packages
[tool.hatch.build.targets.wheel]
packages = ["src/mypackage", "src/mypackage_utils"]Fix 3: Fix Editable Installs
pip install -e . (editable/development install) lets you modify source code without reinstalling:
# Ensure you have a recent pip and setuptools
pip install --upgrade pip setuptools
# Install in editable mode
pip install -e .
# Install with optional extras
pip install -e ".[dev,docs]"
# If still failing, try with build isolation disabled
pip install -e . --no-build-isolationpyproject.toml must declare editable support:
# setuptools — editable installs work by default with setuptools>=64
[build-system]
requires = ["setuptools>=64"] # Not 61 — 64+ for reliable editable installs
build-backend = "setuptools.backends.legacy:build"
# For setuptools editable mode with src layout
[tool.setuptools]
package-dir = {"" = "src"} # Needed for src layout editable installsVerify the editable install worked:
import mypackage
print(mypackage.__file__)
# Should point to your source directory, not site-packages:
# /path/to/myproject/src/mypackage/__init__.pyLegacy fallback if nothing else works:
# setup.py — minimal fallback for legacy editable installs
from setuptools import setup
setup()
# pyproject.toml still holds all metadata
# This file just exists as a hook for legacy toolingFix 4: Build and Inspect the Distribution
Use the build package to create distributions:
# Install build
pip install build
# Build both wheel and sdist
python -m build
# Build only wheel (faster)
python -m build --wheel
# Build only sdist
python -m build --sdist
# Output in dist/
ls dist/
# mypackage-0.1.0-py3-none-any.whl
# mypackage-0.1.0.tar.gzWheel naming convention:
{distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
mypackage-0.1.0-py3-none-any.whl
# py3 = Python 3 (pure Python)
# none = no ABI requirements
# any = platform independent
mypackage-0.1.0-cp312-cp312-linux_x86_64.whl
# cp312 = CPython 3.12
# cp312 = CPython 3.12 ABI
# linux_x86_64 = Linux, 64-bitValidate the distribution before uploading:
# Install twine for validation
pip install twine
# Check metadata validity
twine check dist/*
# Common errors from twine check:
# - Missing long description content type (add readme to pyproject.toml)
# - Invalid classifier strings (check https://pypi.org/classifiers/)
# - Invalid project URL formatFix 5: Upload to PyPI with Twine
# Install twine
pip install twine
# Upload to TestPyPI first (always test here first)
twine upload --repository testpypi dist/*
# Then verify installation works:
pip install --index-url https://test.pypi.org/simple/ mypackage
# Upload to real PyPI
twine upload dist/*Authentication setup:
# Option 1: API token (recommended — never use your password)
# Create a token at https://pypi.org/manage/account/token/
twine upload dist/* -u __token__ -p pypi-your-token-here
# Option 2: Store in .pypirc
# ~/.pypirc
[distutils]
index-servers =
pypi
testpypi
[pypi]
username = __token__
password = pypi-your-token-here
[testpypi]
repository = https://test.pypi.org/legacy/
username = __token__
password = pypi-your-test-token-hereCommon upload errors:
# "File already exists" — you can't overwrite a release on PyPI
# Bump the version in pyproject.toml, rebuild, then upload
# version = "0.1.1" # Not 0.1.0
# "Invalid distribution metadata" — run twine check first
twine check dist/*
# "403 Forbidden" — wrong token or no permission to upload
# Check that the token scope includes the specific project
# "400 Bad Request: The name X is too similar to an existing project"
# Choose a more unique nameAutomate with GitHub Actions:
# .github/workflows/publish.yml
name: Publish to PyPI
on:
release:
types: [published]
jobs:
build-and-publish:
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/mypackage
permissions:
id-token: write # For trusted publishing (no token needed!)
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build distribution
run: |
pip install build
python -m build
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
# Uses OIDC trusted publishing — no API token required
# Configure at: https://pypi.org/manage/project/mypackage/settings/publishing/Fix 6: Version Management
Managing versions across files leads to drift. Use a single source of truth:
# Option 1: Static version in pyproject.toml (simplest)
[project]
version = "0.1.0"
# Option 2: Dynamic version from __version__ in your package
[project]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "mypackage.__version__"}# mypackage/__init__.py
__version__ = "0.1.0"Use hatch-vcs or setuptools-scm for Git tag-based versioning:
# pyproject.toml — hatch-vcs (version from Git tags)
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
dynamic = ["version"]
[tool.hatch.version]
source = "vcs" # Uses git tags like v0.1.0
# Tag your release, then build
# git tag v0.1.0
# git push origin v0.1.0
# python -m build -> version is 0.1.0# pyproject.toml — setuptools-scm
[build-system]
requires = ["setuptools>=61", "setuptools-scm"]
build-backend = "setuptools.backends.legacy:build"
[project]
dynamic = ["version"]
[tool.setuptools_scm]
# Generates version from git tags
# v1.0.0 -> 1.0.0
# v1.0.0-3-gdeadbeef -> 1.0.1.dev3+gdeadbeef (between tags)Bump version consistently with bump-my-version:
pip install bump-my-version
# Configure in pyproject.toml
# [tool.bumpversion]
# current_version = "0.1.0"
# [[tool.bumpversion.files]]
# filename = "pyproject.toml"
# search = 'version = "{current_version}"'
# replace = 'version = "{new_version}"'
# Bump patch/minor/major
bump-my-version bump patch # 0.1.0 -> 0.1.1
bump-my-version bump minor # 0.1.0 -> 0.2.0
bump-my-version bump major # 0.1.0 -> 1.0.0Still Not Working?
ModuleNotFoundError after pip install -e . — the editable install succeeded but the wrong Python or pip is in use. Check which python and which pip — they should point to the same environment. If you’re using virtual environments (you should be), activate it first: source .venv/bin/activate (or .venv\Scripts\activate on Windows), then install.
Namespace packages not found — if your package uses implicit namespace packages (no __init__.py at the top level), setuptools may not find them automatically. Either add __init__.py or configure discovery explicitly:
[tool.setuptools.packages.find]
namespaces = true # Enable namespace package discoveryC extension compilation fails during build — if your package includes C extensions (.pyd, .so), the build requires a C compiler. On Linux: sudo apt install python3-dev build-essential. On macOS: xcode-select --install. On Windows: install Visual Studio Build Tools. For distributing compiled packages, build platform-specific wheels on each target platform using cibuildwheel:
# .github/workflows/build-wheels.yml
- name: Build wheels
uses: pypa/[email protected]
env:
CIBW_SKIP: "pp* *-musllinux*" # Skip PyPy and musl LinuxOld dist/ artifacts causing confusion — always clean dist/ before building to avoid uploading stale files:
rm -rf dist/ build/ *.egg-info
python -m build
twine upload dist/*For related Python issues, see Fix: Python Import Error and Fix: pip Install Fails.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Celery Beat Not Working — Scheduled Tasks Not Running or Beat Not Starting
How to fix Celery Beat issues — beat scheduler not starting, tasks not executing on schedule, timezone configuration, database scheduler, and running beat with workers.
Fix: Celery Task Not Executing — Worker Not Processing Tasks
How to fix Celery tasks not executing — worker configuration, broker connection issues, task routing, serialization errors, and debugging stuck or lost tasks.
Fix: Kafka Consumer Not Receiving Messages, Connection Refused, and Rebalancing Errors
How to fix Apache Kafka issues — consumer not receiving messages, auto.offset.reset, Docker advertised.listeners, max.poll.interval.ms rebalancing, MessageSizeTooLargeException, and KafkaJS errors.
Fix: Docker Secrets Not Working — BuildKit --secret Not Mounting, Compose Secrets Undefined, or Secret Leaking into Image
How to fix Docker secrets — BuildKit secret mounts in Dockerfile, docker-compose secrets config, runtime vs build-time secrets, environment variable alternatives, and verifying secrets don't leak into image layers.