python-packaging
Create distributable Python packages with proper project structure, setup.py/pyproject.toml, and publishing to PyPI. Use when packaging Python libraries, creating CLI tools, or distributing Python code.
ID: python-packagingPython Packaging
Comprehensive guide to creating, structuring, and distributing Python packages using modern packaging tools, pyproject.toml, and publishing to PyPI.
When to Use This Skill
- Creating Python libraries for distribution
- Building command-line tools with entry points
- Publishing packages to PyPI or private repositories
- Setting up Python project structure
- Creating installable packages with dependencies
- Building wheels and source distributions
- Versioning and releasing Python packages
- Creating namespace packages
- Implementing package metadata and classifiers
Core Concepts
1. Package Structure
- Source layout:
src/package_name/(recommended) - Flat layout:
package_name/(simpler but less flexible) - Package metadata: pyproject.toml, setup.py, or setup.cfg
- Distribution formats: wheel (.whl) and source distribution (.tar.gz)
2. Modern Packaging Standards
- PEP 517/518: Build system requirements
- PEP 621: Metadata in pyproject.toml
- PEP 660: Editable installs
- pyproject.toml: Single source of configuration
3. Build Backends
- setuptools: Traditional, widely used
- hatchling: Modern, opinionated
- flit: Lightweight, for pure Python
- poetry: Dependency management + packaging
4. Distribution
- PyPI: Python Package Index (public)
- TestPyPI: Testing before production
- Private repositories: JFrog, AWS CodeArtifact, etc.
Quick Start
Minimal Package Structure
``
my-package/
Ôö£ÔöÇÔöÇ pyproject.toml
Ôö£ÔöÇÔöÇ README.md
Ôö£ÔöÇÔöÇ LICENSE
Ôö£ÔöÇÔöÇ src/
Ôöé ÔööÔöÇÔöÇ my_package/
Ôöé Ôö£ÔöÇÔöÇ __init__.py
Ôöé ÔööÔöÇÔöÇ module.py
ÔööÔöÇÔöÇ tests/
ÔööÔöÇÔöÇ test_module.py
`
Minimal pyproject.toml
`toml
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "0.1.0"
description = "A short description"
authors = [{name = "Your Name", email = "you@example.com"}]
readme = "README.md"
requires-python = ">=3.8"
dependencies = [
"requests>=2.28.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"black>=22.0",
]
`
Package Structure Patterns
Pattern 1: Source Layout (Recommended)
`
my-package/
Ôö£ÔöÇÔöÇ pyproject.toml
Ôö£ÔöÇÔöÇ README.md
Ôö£ÔöÇÔöÇ LICENSE
Ôö£ÔöÇÔöÇ .gitignore
Ôö£ÔöÇÔöÇ src/
Ôöé ÔööÔöÇÔöÇ my_package/
Ôöé Ôö£ÔöÇÔöÇ __init__.py
Ôöé Ôö£ÔöÇÔöÇ core.py
Ôöé Ôö£ÔöÇÔöÇ utils.py
Ôöé ÔööÔöÇÔöÇ py.typed # For type hints
Ôö£ÔöÇÔöÇ tests/
Ôöé Ôö£ÔöÇÔöÇ __init__.py
Ôöé Ôö£ÔöÇÔöÇ test_core.py
Ôöé ÔööÔöÇÔöÇ test_utils.py
ÔööÔöÇÔöÇ docs/
ÔööÔöÇÔöÇ index.md
`
Advantages:
- Prevents accidentally importing from source
- Cleaner test imports
- Better isolation
pyproject.toml for source layout:
`toml
[tool.setuptools.packages.find]
where = ["src"]
`
Pattern 2: Flat Layout
`
my-package/
Ôö£ÔöÇÔöÇ pyproject.toml
Ôö£ÔöÇÔöÇ README.md
Ôö£ÔöÇÔöÇ my_package/
Ôöé Ôö£ÔöÇÔöÇ __init__.py
Ôöé ÔööÔöÇÔöÇ module.py
ÔööÔöÇÔöÇ tests/
ÔööÔöÇÔöÇ test_module.py
`
Simpler but:
- Can import package without installing
- Less professional for libraries
Pattern 3: Multi-Package Project
`
project/
Ôö£ÔöÇÔöÇ pyproject.toml
Ôö£ÔöÇÔöÇ packages/
Ôöé Ôö£ÔöÇÔöÇ package-a/
Ôöé Ôöé ÔööÔöÇÔöÇ src/
Ôöé Ôöé ÔööÔöÇÔöÇ package_a/
Ôöé ÔööÔöÇÔöÇ package-b/
Ôöé ÔööÔöÇÔöÇ src/
Ôöé ÔööÔöÇÔöÇ package_b/
ÔööÔöÇÔöÇ tests/
`
Complete pyproject.toml Examples
Pattern 4: Full-Featured pyproject.toml
`toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-awesome-package"
version = "1.0.0"
description = "An awesome Python package"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "you@example.com"},
]
maintainers = [
{name = "Maintainer Name", email = "maintainer@example.com"},
]
keywords = ["example", "package", "awesome"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"requests>=2.28.0,<3.0.0",
"click>=8.0.0",
"pydantic>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.0.0",
]
docs = [
"sphinx>=5.0.0",
"sphinx-rtd-theme>=1.0.0",
]
all = [
"my-awesome-package[dev,docs]",
]
[project.urls]
Homepage = "https://github.com/username/my-awesome-package"
Documentation = "https://my-awesome-package.readthedocs.io"
Repository = "https://github.com/username/my-awesome-package"
"Bug Tracker" = "https://github.com/username/my-awesome-package/issues"
Changelog = "https://github.com/username/my-awesome-package/blob/main/CHANGELOG.md"
[project.scripts]
my-cli = "my_package.cli:main"
awesome-tool = "my_package.tools:run"
[project.entry-points."my_package.plugins"]
plugin1 = "my_package.plugins:plugin1"
[tool.setuptools]
package-dir = {"" = "src"}
zip-safe = false
[tool.setuptools.packages.find]
where = ["src"]
include = ["my_package*"]
exclude = ["tests*"]
[tool.setuptools.package-data]
my_package = ["py.typed", "*.pyi", "data/*.json"]
Black configuration
[tool.black]
line-length = 100
target-version = ["py38", "py39", "py310", "py311"]
include = '\.pyi?$'
Ruff configuration
[tool.ruff]
line-length = 100
target-version = "py38"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP"]
MyPy configuration
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
Pytest configuration
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-v --cov=my_package --cov-report=term-missing"
Coverage configuration
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
]
`
Pattern 5: Dynamic Versioning
`toml
[build-system]
requires = ["setuptools>=61.0", "setuptools-scm>=8.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
dynamic = ["version"]
description = "Package with dynamic version"
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}
Or use setuptools-scm for git-based versioning
[tool.setuptools_scm]
write_to = "src/my_package/_version.py"
`
In __init__.py:
`python
src/my_package/__init__.py
__version__ = "1.0.0"
Or with setuptools-scm
from importlib.metadata import version
__version__ = version("my-package")
`
Command-Line Interface (CLI) Patterns
Pattern 6: CLI with Click
`python
src/my_package/cli.py
import click
@click.group()
@click.version_option()
def cli():
"""My awesome CLI tool."""
pass
@cli.command()
@click.argument("name")
@click.option("--greeting", default="Hello", help="Greeting to use")
def greet(name: str, greeting: str):
"""Greet someone."""
click.echo(f"{greeting}, {name}!")
@cli.command()
@click.option("--count", default=1, help="Number of times to repeat")
def repeat(count: int):
"""Repeat a message."""
for i in range(count):
click.echo(f"Message {i + 1}")
def main():
"""Entry point for CLI."""
cli()
if __name__ == "__main__":
main()
`
Register in pyproject.toml:
`toml
[project.scripts]
my-tool = "my_package.cli:main"
`
Usage:
`bash
pip install -e .
my-tool greet World
my-tool greet Alice --greeting="Hi"
my-tool repeat --count=3
`
Pattern 7: CLI with argparse
`python
src/my_package/cli.py
import argparse
import sys
def main():
"""Main CLI entry point."""
parser = argparse.ArgumentParser(
description="My awesome tool",
prog="my-tool"
)
parser.add_argument(
"--version",
action="version",
version="%(prog)s 1.0.0"
)
subparsers = parser.add_subparsers(dest="command", help="Commands")
# Add subcommand
process_parser = subparsers.add_parser("process", help="Process data")
process_parser.add_argument("input_file", help="Input file path")
process_parser.add_argument(
"--output", "-o",
default="output.txt",
help="Output file path"
)
args = parser.parse_args()
if args.command == "process":
process_data(args.input_file, args.output)
else:
parser.print_help()
sys.exit(1)
def process_data(input_file: str, output_file: str):
"""Process data from input to output."""
print(f"Processing {input_file} -> {output_file}")
if __name__ == "__main__":
main()
`
Building and Publishing
Pattern 8: Build Package Locally
`bash
Install build tools
pip install build twine
Build distribution
python -m build
This creates:
dist/
my-package-1.0.0.tar.gz (source distribution)
my_package-1.0.0-py3-none-any.whl (wheel)
Check the distribution
twine check dist/*
`
Pattern 9: Publishing to PyPI
`bash
Install publishing tools
pip install twine
Test on TestPyPI first
twine upload --repository testpypi dist/*
Install from TestPyPI to test
pip install --index-url https://test.pypi.org/simple/ my-package
If all good, publish to PyPI
twine upload dist/*
`
Using API tokens (recommended):
`bash
Create ~/.pypirc
[distutils]
index-servers =
pypi
testpypi
[pypi]
username = __token__
password = pypi-...your-token...
[testpypi]
username = __token__
password = pypi-...your-test-token...
`
Pattern 10: Automated Publishing with GitHub Actions
`yaml
.github/workflows/publish.yml
name: Publish to PyPI
on:
release:
types: [created]
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install dependencies
run: |
pip install build twine
- name: Build package
run: python -m build
- name: Check package
run: twine check dist/*
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: twine upload dist/*
`
Advanced Patterns
Pattern 11: Including Data Files
`toml
[tool.setuptools.package-data]
my_package = [
"data/*.json",
"templates/*.html",
"static/css/*.css",
"py.typed",
]
`
Accessing data files:
`python
src/my_package/loader.py
from importlib.resources import files
import json
def load_config():
"""Load configuration from package data."""
config_file = files("my_package").joinpath("data/config.json")
with config_file.open() as f:
return json.load(f)
Python 3.9+
from importlib.resources import files
data = files("my_package").joinpath("data/file.txt").read_text()
`
Pattern 12: Namespace Packages
For large projects split across multiple repositories:
`
Package 1: company-core
company/
ÔööÔöÇÔöÇ core/
Ôö£ÔöÇÔöÇ __init__.py
ÔööÔöÇÔöÇ models.py
Package 2: company-api
company/
ÔööÔöÇÔöÇ api/
Ôö£ÔöÇÔöÇ __init__.py
ÔööÔöÇÔöÇ routes.py
`
Do NOT include __init__.py in the namespace directory (company/):
`toml
company-core/pyproject.toml
[project]
name = "company-core"
[tool.setuptools.packages.find]
where = ["."]
include = ["company.core*"]
company-api/pyproject.toml
[project]
name = "company-api"
[tool.setuptools.packages.find]
where = ["."]
include = ["company.api*"]
`
Usage:
`python
Both packages can be imported under same namespace
from company.core import models
from company.api import routes
`
Pattern 13: C Extensions
`toml
[build-system]
requires = ["setuptools>=61.0", "wheel", "Cython>=0.29"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
ext-modules = [
{name = "my_package.fast_module", sources = ["src/fast_module.c"]},
]
`
Or with setup.py:
`python
setup.py
from setuptools import setup, Extension
setup(
ext_modules=[
Extension(
"my_package.fast_module",
sources=["src/fast_module.c"],
include_dirs=["src/include"],
)
]
)
`
Version Management
Pattern 14: Semantic Versioning
`python
src/my_package/__init__.py
__version__ = "1.2.3"
Semantic versioning: MAJOR.MINOR.PATCH
MAJOR: Breaking changes
MINOR: New features (backward compatible)
PATCH: Bug fixes
`
Version constraints in dependencies:
`toml
dependencies = [
"requests>=2.28.0,<3.0.0", # Compatible range
"click~=8.1.0", # Compatible release (~= 8.1.0 means >=8.1.0,<8.2.0)
"pydantic>=2.0", # Minimum version
"numpy==1.24.3", # Exact version (avoid if possible)
]
`
Pattern 15: Git-Based Versioning
`toml
[build-system]
requires = ["setuptools>=61.0", "setuptools-scm>=8.0"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
dynamic = ["version"]
[tool.setuptools_scm]
write_to = "src/my_package/_version.py"
version_scheme = "post-release"
local_scheme = "dirty-tag"
`
Creates versions like:
- 1.0.0
(from git tag) - 1.0.1.dev3+g1234567
(3 commits after tag)
Testing Installation
Pattern 16: Editable Install
`bash
Install in development mode
pip install -e .
With optional dependencies
pip install -e ".[dev]"
pip install -e ".[dev,docs]"
Now changes to source code are immediately reflected
`
Pattern 17: Testing in Isolated Environment
`bash
Create virtual environment
python -m venv test-env
source test-env/bin/activate # Linux/Mac
test-env\Scripts\activate # Windows
Install package
pip install dist/my_package-1.0.0-py3-none-any.whl
Test it works
python -c "import my_package; print(my_package.__version__)"
Test CLI
my-tool --help
Cleanup
deactivate
rm -rf test-env
`
Documentation
Pattern 18: README.md Template
`markdown
My Package
[](https://pypi.org/project/my-package/)
[](https://pypi.org/project/my-package/)
[](https://github.com/username/my-package/actions)
Brief description of your package.
Installation
`bash
pip install my-package
`
Quick Start
`python
from my_package import something
result = something.do_stuff()
`
Features
- Feature 1
- Feature 2
- Feature 3
Documentation
Full documentation: https://my-package.readthedocs.io
Development
`bash
git clone https://github.com/username/my-package.git
cd my-package
pip install -e ".[dev]"
pytest
`
License
MIT
`
Common Patterns
Pattern 19: Multi-Architecture Wheels
`yaml
.github/workflows/wheels.yml
name: Build wheels
on: [push, pull_request]
jobs:
build_wheels:
name: Build wheels on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- name: Build wheels
uses: pypa/cibuildwheel@v2.16.2
- uses: actions/upload-artifact@v3
with:
path: ./wheelhouse/*.whl
`
Pattern 20: Private Package Index
`bash
Install from private index
pip install my-package --index-url https://private.pypi.org/simple/
Or add to pip.conf
[global]
index-url = https://private.pypi.org/simple/
extra-index-url = https://pypi.org/simple/
Upload to private index
twine upload --repository-url https://private.pypi.org/ dist/*
`
File Templates
.gitignore for Python Packages
`gitignore
Build artifacts
build/
dist/
*.egg-info/
*.egg
.eggs/
Python
__pycache__/
*.py[cod]
*$py.class
*.so
Virtual environments
venv/
env/
ENV/
IDE
.vscode/
.idea/
*.swp
Testing
.pytest_cache/
.coverage
htmlcov/
Distribution
*.whl
*.tar.gz
`
MANIFEST.in
`
MANIFEST.in
include README.md
include LICENSE
include pyproject.toml
recursive-include src/my_package/data *.json
recursive-include src/my_package/templates *.html
recursive-exclude * __pycache__
recursive-exclude * *.py[co]
``
Checklist for Publishing
- [ ] Code is tested (pytest passing)
- [ ] Documentation is complete (README, docstrings)
- [ ] Version number updated
- [ ] CHANGELOG.md updated
- [ ] License file included
- [ ] pyproject.toml is complete
- [ ] Package builds without errors
- [ ] Installation tested in clean environment
- [ ] CLI tools work (if applicable)
- [ ] PyPI metadata is correct (classifiers, keywords)
- [ ] GitHub repository linked
- [ ] Tested on TestPyPI first
- [ ] Git tag created for release
Resources
- Python Packaging Guide: https://packaging.python.org/
- PyPI: https://pypi.org/
- TestPyPI: https://test.pypi.org/
- setuptools documentation: https://setuptools.pypa.io/
- build: https://pypa-build.readthedocs.io/
- twine: https://twine.readthedocs.io/
Best Practices Summary
1. Use src/ layout for cleaner package structure
2. Use pyproject.toml for modern packaging
3. Pin build dependencies in build-system.requires
4. Version appropriately with semantic versioning
5. Include all metadata (classifiers, URLs, etc.)
6. Test installation in clean environments
7. Use TestPyPI before publishing to PyPI
8. Document thoroughly with README and docstrings
9. Include LICENSE file
10. Automate publishing with CI/CD
Informacion
| Estado | Activo |
| Origen | upstream |
| Usos | 3 |
| Ultimo uso | 23/01 16:57 |
| Actualizado | 09/01/2026 02:14 |
Archivo origen
Z:\Repositorios\core\upstream\plugins\python-development\skills\python-packaging\SKILL.md