Python Package Development
This skill provides expert guidance for developing, configuring, and publishing Python packages following modern best practices.
Project Structure
A properly structured Python package follows this layout:
code
project_name/
├── LICENSE
├── README.md
├── pyproject.toml
├── src/
│ └── package_name/
│ ├── __init__.py
│ └── module.py
└── tests/
└── test_module.py
Key principles:
- •Use the
src/layout to prevent accidental imports from the working directory - •Package directory name should match the distribution name (with underscores for hyphens)
- •Always include
__init__.pyfor regular packages (not namespace packages) - •Include
py.typedmarker file for typed packages
pyproject.toml Configuration
Build System
Choose a build backend based on project needs:
Hatchling (recommended for most projects):
toml
[build-system] requires = ["hatchling >= 1.26"] build-backend = "hatchling.build"
Setuptools (for complex builds or C extensions):
toml
[build-system] requires = ["setuptools >= 77.0.3"] build-backend = "setuptools.build_meta"
Flit (minimal, pure Python packages):
toml
[build-system] requires = ["flit_core >= 3.12.0, <4"] build-backend = "flit_core.buildapi"
PDM (with PEP 582 local packages):
toml
[build-system] requires = ["pdm-backend >= 2.4.0"] build-backend = "pdm.backend"
Project Metadata
toml
[project]
name = "package-name"
version = "0.1.0"
description = "A short description of the package"
readme = "README.md"
requires-python = ">=3.11"
license = "MIT"
license-files = ["LICENSE"]
authors = [
{ name = "Author Name", email = "author@example.com" }
]
maintainers = [
{ name = "Maintainer Name", email = "maintainer@example.com" }
]
keywords = ["keyword1", "keyword2"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Typing :: Typed",
]
dependencies = [
"numpy>=1.24",
"pandas>=2.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"pytest-cov>=4.1",
"ruff>=0.5",
"mypy>=1.8",
]
docs = [
"mkdocs>=1.6",
"mkdocs-material>=9.5",
]
[project.urls]
Homepage = "https://github.com/user/project"
Documentation = "https://project.readthedocs.io"
Repository = "https://github.com/user/project"
Issues = "https://github.com/user/project/issues"
Changelog = "https://github.com/user/project/blob/main/CHANGELOG.md"
[project.scripts]
cli-command = "package_name.cli:main"
[project.entry-points."group.name"]
plugin-name = "package_name.plugin:PluginClass"
Build Backend Configuration
Hatchling:
toml
[tool.hatch.build.targets.wheel] packages = ["src/package_name"] [tool.hatch.build.targets.sdist] include = ["src/", "tests/", "README.md", "LICENSE"]
Setuptools:
toml
[tool.setuptools.packages.find] where = ["src"] [tool.setuptools.package-data] "*" = ["py.typed", "*.pyi"]
Development Tools
toml
[tool.pytest.ini_options]
addopts = "-ra --strict-markers"
testpaths = ["tests"]
pythonpath = ["src"]
[tool.ruff]
line-length = 88
target-version = "py311"
src = ["src"]
[tool.ruff.lint]
select = ["E", "F", "W", "I", "UP", "B", "A", "C4", "DTZ", "T10", "ISC"]
ignore = ["E501"]
[tool.ruff.lint.isort]
known-first-party = ["package_name"]
[tool.mypy]
python_version = "3.11"
strict = true
warn_unused_configs = true
mypy_path = "src"
[[tool.mypy.overrides]]
module = ["pandas.*", "numpy.*"]
ignore_missing_imports = true
[tool.coverage.run]
source = ["src"]
branch = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"@overload",
]
Building and Publishing
Build Distribution Archives
bash
# Install build tool python -m pip install --upgrade build # Build source distribution and wheel python -m build # Output in dist/: # package_name-0.1.0.tar.gz (source distribution) # package_name-0.1.0-py3-none-any.whl (wheel)
Upload to PyPI
bash
# Install twine python -m pip install --upgrade twine # Upload to TestPyPI first python -m twine upload --repository testpypi dist/* # Upload to PyPI python -m twine upload dist/*
Test Installation
bash
# From TestPyPI python -m pip install --index-url https://test.pypi.org/simple/ --no-deps package-name # From PyPI python -m pip install package-name
Version Management
Manual Versioning
Update version in pyproject.toml directly.
Dynamic Versioning with Hatchling
toml
[project] dynamic = ["version"] [tool.hatch.version] path = "src/package_name/__init__.py"
In __init__.py:
python
__version__ = "0.1.0"
Git Tag Versioning
toml
[project] dynamic = ["version"] [tool.hatch.version] source = "vcs" [build-system] requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build"
Best Practices
- •Always use src/ layout - Prevents import confusion during development
- •Pin minimum versions, not maximum -
numpy>=1.24notnumpy>=1.24,<2.0 - •Use optional dependencies - Group dev, docs, test dependencies separately
- •Include py.typed marker - For packages with type annotations
- •Write comprehensive README - Include installation, quick start, and examples
- •Choose appropriate license - MIT, Apache-2.0, or BSD-3-Clause for open source
- •Add classifiers - Help users find your package on PyPI
- •Test on TestPyPI first - Validate package before publishing to PyPI
- •Use semantic versioning - MAJOR.MINOR.PATCH format
- •Automate releases with CI/CD - GitHub Actions for testing and publishing
Common Issues
Package not found after install
- •Ensure
packagesis correctly configured for your build backend - •Check
src/layout is properly set up
Import errors in tests
- •Add
pythonpath = ["src"]to pytest configuration - •Or install package in editable mode:
pip install -e .
Missing package data
- •Configure
package-dataorincludein build backend settings - •For non-Python files, use
MANIFEST.inwith setuptools
Type hints not recognized
- •Add
py.typedmarker file to package root - •Ensure
package-dataincludes*.pyifiles