Docs
packaging
Python Packaging & Distribution
Overview
Packaging your Python code makes it installable, shareable, and professional. This module covers everything from basic project structure to publishing on PyPI.
1. Why Package Your Code?
Benefits
- ā¢Reusability - Install your code anywhere
- ā¢Distribution - Share with others via PyPI
- ā¢Version Control - Track changes with semantic versioning
- ā¢Dependency Management - Declare what your code needs
- ā¢Professional - Standard way to share Python projects
2. Project Structure
Recommended Layout
my_package/
āāā src/
ā āāā my_package/
ā āāā __init__.py
ā āāā core.py
ā āāā utils.py
ā āāā cli.py
āāā tests/
ā āāā __init__.py
ā āāā test_core.py
ā āāā test_utils.py
āāā docs/
ā āāā index.md
āāā pyproject.toml # Modern configuration
āāā README.md
āāā LICENSE
āāā CHANGELOG.md
āāā .gitignore
Alternative Flat Layout
my_package/
āāā my_package/
ā āāā __init__.py
ā āāā core.py
āāā tests/
āāā pyproject.toml
āāā README.md
3. pyproject.toml (Modern Standard)
Basic pyproject.toml
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my-package"
version = "0.1.0"
description = "A sample Python package"
readme = "README.md"
license = {text = "MIT"}
authors = [
{name = "Your Name", email = "you@example.com"}
]
requires-python = ">=3.8"
classifiers = [
"Development Status :: 3 - Alpha",
"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",
]
keywords = ["sample", "example"]
dependencies = [
"requests>=2.25.0",
"click>=8.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov",
"black",
"mypy",
"ruff",
]
docs = [
"sphinx",
"sphinx-rtd-theme",
]
[project.urls]
Homepage = "https://github.com/username/my-package"
Documentation = "https://my-package.readthedocs.io"
Repository = "https://github.com/username/my-package.git"
Issues = "https://github.com/username/my-package/issues"
[project.scripts]
my-cli = "my_package.cli:main"
[project.entry-points."my_package.plugins"]
plugin_a = "my_package.plugins:PluginA"
[tool.setuptools.packages.find]
where = ["src"]
Dynamic Version from File
[project]
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}
4. setup.py (Legacy but Still Used)
Basic setup.py
from setuptools import setup, find_packages
setup(
name="my-package",
version="0.1.0",
author="Your Name",
author_email="you@example.com",
description="A sample package",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
url="https://github.com/username/my-package",
packages=find_packages(where="src"),
package_dir={"": "src"},
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
python_requires=">=3.8",
install_requires=[
"requests>=2.25.0",
"click>=8.0.0",
],
extras_require={
"dev": ["pytest", "black", "mypy"],
},
entry_points={
"console_scripts": [
"my-cli=my_package.cli:main",
],
},
)
5. Package Metadata Files
init.py
"""My Package - A sample Python package."""
__version__ = "0.1.0"
__author__ = "Your Name"
from .core import main_function
from .utils import helper_function
__all__ = ["main_function", "helper_function"]
README.md
# My Package
A brief description of what this package does.
## Installation
```bash
pip install my-package
```
Quick Start
from my_package import main_function
result = main_function()
Features
- ā¢Feature 1
- ā¢Feature 2
License
MIT License
### LICENSE (MIT Example)
MIT License
Copyright (c) 2024 Your Name
Permission is hereby granted, free of charge, to any person obtaining a copy of this software...
### CHANGELOG.md
```markdown
# Changelog
All notable changes to this project will be documented in this file.
## [0.1.0] - 2024-01-15
### Added
- Initial release
- Core functionality
- CLI interface
6. Virtual Environments
Creating Virtual Environments
# Using venv (built-in)
python -m venv .venv
# Activate (Linux/macOS)
source .venv/bin/activate
# Activate (Windows)
.venv\Scripts\activate
# Deactivate
deactivate
Using virtualenv
pip install virtualenv
virtualenv .venv
Using conda
conda create -n myenv python=3.11
conda activate myenv
conda deactivate
7. Dependency Management
pip and requirements.txt
# Install dependencies
pip install -r requirements.txt
# Freeze current environment
pip freeze > requirements.txt
# Install with extras
pip install "my-package[dev]"
requirements.txt Format
# Core dependencies
requests>=2.25.0,<3.0.0
click>=8.0.0
# Pinned versions for reproducibility
numpy==1.24.0
# Development dependencies
-r requirements-dev.txt
# From Git
git+https://github.com/user/repo.git@v1.0.0#egg=package
# From URL
https://example.com/package.whl
pip-tools
pip install pip-tools
# requirements.in (loose constraints)
requests>=2.25.0
click>=8.0.0
# Compile to pinned requirements.txt
pip-compile requirements.in
# Sync environment
pip-sync requirements.txt
Poetry (Alternative)
pip install poetry
# Create new project
poetry new my-package
# Add dependency
poetry add requests
# Add dev dependency
poetry add --group dev pytest
# Install all dependencies
poetry install
# Build package
poetry build
# Publish to PyPI
poetry publish
8. Building Packages
Build Commands
# Install build tools
pip install build
# Build source distribution and wheel
python -m build
# Output in dist/
# dist/my_package-0.1.0.tar.gz (source)
# dist/my_package-0.1.0-py3-none-any.whl (wheel)
Wheel Types
Pure Python: my_package-0.1.0-py3-none-any.whl
Platform-specific: my_package-0.1.0-cp311-cp311-linux_x86_64.whl
Format: {name}-{version}-{python}-{abi}-{platform}.whl
9. Installing Locally
Development Install (Editable)
# Install in development mode
pip install -e .
# With extras
pip install -e ".[dev]"
# Changes to source are immediately reflected
From Local Build
# Install from wheel
pip install dist/my_package-0.1.0-py3-none-any.whl
# Install from source
pip install dist/my_package-0.1.0.tar.gz
From Git
pip install git+https://github.com/user/repo.git
pip install git+https://github.com/user/repo.git@v1.0.0
pip install git+https://github.com/user/repo.git@main
10. Publishing to PyPI
Setup PyPI Account
- ā¢Create account at https://pypi.org
- ā¢Enable 2FA
- ā¢Create API token
Configure credentials
# ~/.pypirc
[pypi]
username = __token__
password = pypi-your-token-here
[testpypi]
username = __token__
password = pypi-your-test-token-here
Upload with twine
pip install twine
# Check package before upload
twine check dist/*
# Upload to TestPyPI first
twine upload --repository testpypi dist/*
# Upload to PyPI
twine upload dist/*
Publish with Poetry
poetry publish --build
11. Command-Line Interfaces
Using Click
# my_package/cli.py
import click
@click.group()
@click.version_option()
def cli():
"""My Package CLI."""
pass
@cli.command()
@click.argument('name')
@click.option('--count', '-c', default=1, help='Number of greetings')
def greet(name, count):
"""Greet someone."""
for _ in range(count):
click.echo(f"Hello, {name}!")
@cli.command()
@click.option('--verbose', '-v', is_flag=True)
def info(verbose):
"""Show package info."""
click.echo("My Package v0.1.0")
if verbose:
click.echo("Detailed information...")
if __name__ == '__main__':
cli()
Entry Points Configuration
# pyproject.toml
[project.scripts]
my-cli = "my_package.cli:cli"
# Multiple commands
[project.scripts]
my-cli = "my_package.cli:cli"
my-tool = "my_package.tools:main"
12. Including Data Files
Package Data
# pyproject.toml
[tool.setuptools.package-data]
my_package = ["data/*.json", "templates/*.html"]
Access Package Data
import importlib.resources as pkg_resources
# Python 3.9+
with pkg_resources.files('my_package').joinpath('data/config.json').open() as f:
config = json.load(f)
# Python 3.7-3.8
with pkg_resources.open_text('my_package.data', 'config.json') as f:
config = json.load(f)
13. Versioning
Semantic Versioning
MAJOR.MINOR.PATCH
1.0.0 - Initial stable release
1.0.1 - Bug fixes (backwards compatible)
1.1.0 - New features (backwards compatible)
2.0.0 - Breaking changes
Pre-releases:
1.0.0-alpha.1
1.0.0-beta.1
1.0.0-rc.1
Development:
0.1.0 - Initial development
Version in init.py
__version__ = "0.1.0"
def get_version():
return __version__
Using setuptools-scm (Git Tags)
[build-system]
requires = ["setuptools>=45", "setuptools-scm[toml]>=6.2"]
[tool.setuptools_scm]
write_to = "src/my_package/_version.py"
14. Code Quality Tools
pyproject.toml Tool Configuration
# Black (formatter)
[tool.black]
line-length = 88
target-version = ['py38', 'py39', 'py310', 'py311']
include = '\.pyi?$'
exclude = '''
/(
\.git
| \.venv
| build
| dist
)/
'''
# Ruff (linter)
[tool.ruff]
line-length = 88
select = ["E", "F", "I", "N", "W"]
ignore = ["E501"]
target-version = "py38"
[tool.ruff.isort]
known-first-party = ["my_package"]
# mypy (type checker)
[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
exclude = ["tests"]
# pytest
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-v --cov=my_package"
# coverage
[tool.coverage.run]
source = ["src/my_package"]
branch = true
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
]
15. Best Practices
1. Use src Layout
my_package/
āāā src/
ā āāā my_package/
ā āāā __init__.py
2. Pin Dependencies in Production
# requirements.txt (production)
requests==2.28.1
click==8.1.3
# requirements.in (development)
requests>=2.25.0
click>=8.0.0
3. Include Type Hints
def greet(name: str, count: int = 1) -> list[str]:
"""Greet someone multiple times."""
return [f"Hello, {name}!" for _ in range(count)]
4. Write Good Documentation
def process_data(data: dict, strict: bool = False) -> dict:
"""
Process input data with optional strict validation.
Args:
data: Input dictionary containing raw data
strict: If True, raise errors on invalid data
Returns:
Processed data dictionary
Raises:
ValueError: If strict=True and data is invalid
Example:
>>> process_data({"key": "value"})
{"key": "processed_value"}
"""
pass
5. Use Pre-commit Hooks
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.270
hooks:
- id: ruff
args: [--fix]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0
hooks:
- id: mypy
Summary
| Task | Tool/File |
|---|---|
| Configuration | pyproject.toml |
| Build package | python -m build |
| Upload to PyPI | twine upload |
| Virtual env | venv, virtualenv, conda |
| Dependencies | pip, poetry, pip-tools |
| CLI | click, argparse |
| Version | setuptools-scm |
| Formatting | black, ruff |
| Type checking | mypy |
| Testing | pytest |
Next Steps
After mastering packaging:
- ā¢Publish your first package to TestPyPI
- ā¢Set up CI/CD with GitHub Actions
- ā¢Add documentation with Sphinx or MkDocs
- ā¢Explore conda packaging for complex dependencies