Skip to content

Fix: Python Packaging Not Working — Build Fails, Package Not Found After Install, or PyPI Upload Errors

FixDevs ·

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] table

Or 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 metadata

Why 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 made pyproject.toml the standard. A project without a [build-system] table falls back to legacy setup.py behavior, which pip may 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 660pip install -e . with a pyproject.toml backend 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.toml must 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 directories

Src 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/*.html

Hatchling 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-isolation

pyproject.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 installs

Verify the editable install worked:

import mypackage
print(mypackage.__file__)
# Should point to your source directory, not site-packages:
# /path/to/myproject/src/mypackage/__init__.py

Legacy 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 tooling

Fix 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.gz

Wheel 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-bit

Validate 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 format

Fix 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-here

Common 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 name

Automate 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.0

Still 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 discovery

C 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 Linux

Old 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles