Docs

README

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

  1. •Create account at https://pypi.org
  2. •Enable 2FA
  3. •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

TaskTool/File
Configurationpyproject.toml
Build packagepython -m build
Upload to PyPItwine upload
Virtual envvenv, virtualenv, conda
Dependenciespip, poetry, pip-tools
CLIclick, argparse
Versionsetuptools-scm
Formattingblack, ruff
Type checkingmypy
Testingpytest

Next Steps

After mastering packaging:

  1. •Publish your first package to TestPyPI
  2. •Set up CI/CD with GitHub Actions
  3. •Add documentation with Sphinx or MkDocs
  4. •Explore conda packaging for complex dependencies
README - Python Tutorial | DeepML