Categories Python

Creating a Python Application for Distribution

Python application packaging is a crucial skill for any developer looking to share their code with the world. Whether you’re building a tool for internal use or publishing an open-source project, proper packaging ensures your application can be easily distributed, installed, and maintained.

In this guide, we’ll walk through the process of creating and packaging a Python application using a text-based calculator as our example. We’ll cover everything from initial project setup to final distribution.

You’ll learn:

  • How to structure a Python project for distribution
  • Setting up a development environment
  • Creating and testing a functional application
  • Building and distributing packages
  • Best practices for maintenance and updates

Project Setup and Structure

Basic Project Structure Overview

Let’s start by creating a well-organized project structure. Our calculator project will follow the standard Python package layout:

calculator/
├── calculator/
│   ├── __init__.py
│   ├── core.py
│   └── cli.py
├── tests/
│   ├── __init__.py
│   └── test_calculator.py
├── pyproject.toml
├── README.md
├── LICENSE
└── .gitignore

Each file and directory serves a specific purpose:

  • calculator/: Main package directory containing source code
  • tests/: Directory for test files
  • pyproject.toml: Modern Python project configuration
  • Supporting files for documentation and version control

Setting up pyproject.toml

The pyproject.toml file is the heart of modern Python packaging. This file defines your project’s metadata, dependencies, build requirements, and more in a standardized format. Here are two common approaches:

Option 1: Using Poetry

Poetry is an alternative dependency management and packaging utility for Python. Read our Python Poetry for Modern Python Package Management guide for more information.

[build-system]   
# Specifies tools needed to build the package
requires = ["poetry-core>=1.0.0"]
# Identifies the backend that actually builds the package
build-backend = "poetry.core.masonry.api"

[tool.poetry]
# Basic project metadata
name = "calculator"        # Package name on PyPI
version = "1.0.0"               # Following semantic versioning
description = "A text-based calculator application"
authors = ["Your Name <your.email@example.com>"]
readme = "README.md"            # Will be included in package info
# Classifiers help users find your package on PyPI
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "Topic :: Software Development :: Libraries :: Python Modules",
]

# Additional URLs displayed on PyPI
[tool.poetry.urls]
"Homepage" = "https://github.com/yourusername/text-calculator"
"Bug Tracker" = "https://github.com/yourusername/text-calculator/issues"
"Documentation" = "https://text-calculator.readthedocs.io"

# Package dependencies - Poetry uses ^notation (compatible releases)
[tool.poetry.dependencies]
python = "^3.8.1"
# Add other dependencies your package needs, for example:
# requests = "^2.28.0"

# Development-only dependencies, not installed by users
[tool.poetry.dev-dependencies]
pytest = "^7.3.1"
black = "^23.3.0"
flake8 = "^6.0.0"
mypy = "^1.3.0"

# Command-line entry points - makes `calculator` command available
[tool.poetry.scripts]
calculator = "calculator.cli:main"

Option 2: Using Setuptools (PEP 621 Standard)

[build-system]
# Setuptools is the most common build system
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
# Core metadata (standardized by PEP 621)
name = "text-calculator"
version = "1.0.0"
authors = [
  {name = "Your Name", email = "your.email@example.com"},
]
description = "A text-based calculator application"
readme = "README.md"
requires-python = ">=3.7"
license = {text = "MIT"}
classifiers = [
    "Programming Language :: Python :: 3",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "Topic :: Software Development :: Libraries :: Python Modules",
]
# Runtime dependencies
dependencies = [
    # Conditional dependencies for compatibility
    "importlib-metadata; python_version < '3.8'",
    # Regular dependencies
    # "requests>=2.28.0",
]

# Optional dependency groups that users can install with extras
[project.optional-dependencies]
dev = [
    "pytest>=7.3.1",
    "black>=23.3.0",
    "flake8>=6.0.0",
    "mypy>=1.3.0",
]
docs = [
    "sphinx>=6.1.3",
    "sphinx-rtd-theme>=1.2.0",
]

# Project links for PyPI
[project.urls]
"Homepage" = "https://github.com/yourusername/text-calculator"
"Bug Tracker" = "https://github.com/yourusername/text-calculator/issues"
"Documentation" = "https://text-calculator.readthedocs.io"
"Source Code" = "https://github.com/yourusername/text-calculator"

# Command-line entry points
[project.scripts]
calculator = "calculator.cli:main"

# Setuptools-specific configurations
[tool.setuptools]
packages = ["calculator"]  # List of packages to include
# You can also use find directive to automatically find packages:
# [tool.setuptools.packages.find]
# include = ["calculator*"]
# exclude = ["tests*"]

Key Differences and When to Use Each Approach

  1. Poetry Approach:
    • Simplified dependency management with automatic resolution
    • Built-in virtual environment handling
    • Integrated build and publish commands
    • Best for: New projects where you have full control and want a modern workflow
  2. Setuptools Approach:
    • Industry standard with the broadest compatibility
    • Follows the latest Python Packaging Authority standards (PEP 621)
    • Better for transitioning existing projects
    • Best for: Projects that need to maintain compatibility with older tooling

Common Fields Explained

  • name: Package name on PyPI (use lowercase, hyphens instead of underscores)
  • version: Following semantic versioning (MAJOR.MINOR.PATCH)
  • requires-python: Minimum Python version needed
  • classifiers: Categorization tags for PyPI (see full list)
  • dependencies: Libraries your package needs to function
  • scripts: Command-line entry points your package provides

The configuration you choose largely depends on your workflow preferences and project requirements, but both approaches produce compatible, standards-compliant packages.

Essential Project Files

README.md

# Text Calculator

A simple text-based calculator application built with Python.

## Installation
```bash
pip install text-calculator
```

## Usage
```bash
calculator add 5 3
```

## Features
- Addition, subtraction, multiplication, and division operations
- Command-line interface
- Error handling for division by zero

## Development

```bash
# Clone repository
git clone https://github.com/yourusername/text-calculator.git
cd text-calculator

# Set up development environment
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate
pip install -e .
```

Create .gitignore file

__pycache__/
*.py[cod]
dist/
build/
*.egg-info/
.venv/

Creating the Calculator Application

Core Application Structure

The core calculator functionality is implemented in calculator/core.py:

class Calculator:
    def add(self, x: float, y: float) -> float:
        """Add two numbers."""
        return x + y
    
    def subtract(self, x: float, y: float) -> float:
        """Subtract y from x."""
        return x - y
    
    def multiply(self, x: float, y: float) -> float:
        """Multiply two numbers."""
        return x * y
    
    def divide(self, x: float, y: float) -> float:
        """Divide x by y. Raises ValueError if y is zero."""
        if y == 0:
            raise ValueError("Cannot divide by zero")
        return x / y

Building the CLI Interface

The command-line interface is implemented in calculator/cli.py:

import argparse
from .core import Calculator

def main():
    """Entry point for the calculator CLI application."""
    parser = argparse.ArgumentParser(description="Text-based calculator")
    parser.add_argument('operation', choices=['add', 'subtract', 'multiply', 'divide'])
    parser.add_argument('x', type=float, help="First number")
    parser.add_argument('y', type=float, help="Second number")
    
    args = parser.parse_args()
    calc = Calculator()
    
    operations = {
        'add': calc.add,
        'subtract': calc.subtract,
        'multiply': calc.multiply,
        'divide': calc.divide
    }
    
    try:
        result = operations[args.operation](args.x, args.y)
        print(f"Result: {result}")
    except ValueError as e:
        print(f"Error: {e}")
        return 1
    return 0

if __name__ == '__main__':
    exit(main())

Creating the Entry Point

Create a new file calculator/__main__.py:

from .cli import main

if __name__ == "__main__":
    exit(main())

Writing Tests

Create tests in tests/test_calculator.py:

import pytest
from calculator.core import Calculator

def test_add():
    calc = Calculator()
    assert calc.add(2, 3) == 5
    assert calc.add(-1, 1) == 0

def test_subtract():
    calc = Calculator()
    assert calc.subtract(5, 2) == 3
    assert calc.subtract(1, 1) == 0

def test_multiply():
    calc = Calculator()
    assert calc.multiply(2, 3) == 6
    assert calc.multiply(-1, 5) == -5

def test_divide():
    calc = Calculator()
    assert calc.divide(6, 2) == 3
    assert calc.divide(5, 2) == 2.5

def test_divide_by_zero():
    calc = Calculator()
    with pytest.raises(ValueError):
        calc.divide(1, 0)

Development Build Process

Setting up Development Environment

A proper development environment ensures your package can be built, tested, and modified without affecting your system’s Python installation.

  1. Create a virtual environment:
# Create an isolated Python environment
python -m venv .venv

# Activate the environment
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Verify activation (should show virtual environment path)
which python  # On Windows: where python

The virtual environment:

  • Creates an isolated Python installation
  • Prevents dependency conflicts with other projects
  • Makes it easy to test installation and uninstallation
  • Keeps your system Python clean
  1. Install development dependencies:
# For Poetry projects
poetry install  # Installs all dependencies including dev dependencies

# For setuptools projects
pip install -e ".[dev]"  # Installs the package in editable mode with dev extras
# Or without extras defined:
pip install -e .         # Installs the package in editable mode
pip install pytest       # Install additional dev dependencies manually

What these commands do:

  • -e flag creates an “editable” installation that reflects your code changes immediately
  • Poetry’s install command reads all dependencies from pyproject.toml
  • The .[dev] syntax installs optional development dependencies defined in pyproject.toml

Development Workflow Tools

Modern Python projects benefit from several development tools that can be configured in your pyproject.toml:

# Code formatting
python -m black calculator tests

# Linting
python -m flake8 calculator tests

# Type checking
python -m mypy calculator

Black is used to ensure proper code formatting and mypy to check for typing in Python applications. For more information on formatting, linting, and type checking please read Enhancing Python Code Quality with Ruff, Black, and Mypy.

You can add configuration for these tools in pyproject.toml:

[tool.black]
line-length = 88
target-version = ['py38']

[tool.isort]
profile = "black"
line_length = 88

[tool.mypy]
python_version = "3.8.1"
warn_return_any = true
warn_unused_configs = true

[tool.pytest.ini_options]
minversion = "6.0"
testpaths = ["tests"]
python_files = "test_*.py"

Running the Application During Development

There are several ways to run your application during development, each with different use cases:

  1. Direct module execution:
# Run as a module (uses the current directory version)
python -m calculator add 5 3

# This works because the __main__.py file in your package 
# or the appropriate entry point is called when running as a module
  1. After installing in development mode:
# This uses the CLI entry point defined in pyproject.toml
calculator add 5 3

# Behind the scenes, setuptools/poetry has created a script or console entry
# that points to your calculator.cli:main function
  1. Run specific script directly during development:
# Sometimes useful for debugging
python calculator/cli.py add 5 3

# Note: This may require adjustments to your import statements
# as the module is not being run through the package

Managing Dependencies

As your project evolves, you’ll need to add, update, or remove dependencies:

  1. With Poetry:
# Add a new runtime dependency
poetry add requests

# Add a development dependency
poetry add --dev black

# Update dependencies
poetry update

# Show dependency tree
poetry show --tree
  1. With pip/setuptools:
# Install and update pyproject.toml manually, then
pip install -e .

# To upgrade a specific package
pip install --upgrade requests

Remember to update your pyproject.toml file when manually installing packages with pip to keep it in sync with your actual dependencies.

Package Building and Distribution

Preparing for Distribution

Before building, verify:

  1. All tests pass: pytest tests/
  2. Documentation is up to date
  3. Version number is correct in pyproject.toml
  4. README.md contains current information

Building Distribution Packages

Option 1: Using Poetry

With Poetry, building distribution packages is straightforward:

# Build both wheel and source distribution
poetry build

This creates two files in the dist/ directory:

  • calculator-1.0.0.tar.gz (source distribution)
  • calculator-1.0.0-py3-none-any.whl (wheel distribution)

Option 2: Using Standard Build Tools

Create both source and wheel distributions using the recommended build module:

python -m pip install --upgrade build
python -m build

This creates two files in the dist/ directory:

  • text_calculator-1.0.0.tar.gz (source distribution)
  • text_calculator-1.0.0-py3-none-any.whl (wheel distribution)

Distribution Options

Option 1: Using Poetry

Poetry can publish directly to PyPI:

# Publish to Test PyPI first
poetry config repositories.testpypi https://test.pypi.org/legacy/
poetry publish --repository testpypi

# Publish to production PyPI
poetry publish

Option 2: Using Twine

Upload to PyPI using twine:

python -m pip install --upgrade twine

# Test PyPI first
python -m twine upload --repository testpypi dist/*

# Production PyPI
python -m twine upload dist/*

Testing and Validation

Package Installation Testing

Test installation in a fresh virtual environment:

python -m venv test_env
source test_env/bin/activate
pip install text-calculator

Functionality Testing

Verify core operations:

calculator add 5 3
calculator multiply 4 2
calculator divide 10 2

Distribution Testing

  1. Install from Test PyPI:
pip install --index-url https://test.pypi.org/simple/ text-calculator
  1. Verify installation and imports:
from calculator.core import Calculator
calc = Calculator()
assert calc.add(2, 2) == 4

Best Practices and Tips

Project Organization

  • Keep code modular and focused
  • Use type hints for better code clarity
  • Document functions and classes with docstrings
  • Follow PEP 8 style guidelines

Version Control

  • Use semantic versioning (MAJOR.MINOR.PATCH)
  • Tag releases in git:
git tag -a v1.0.0 -m "Initial release"
git push origin v1.0.0

Maintenance Considerations

  • Regularly update dependencies
  • Monitor security advisories
  • Maintain a changelog
  • Respond to bug reports promptly

Conclusion

Creating a distributable Python application involves careful planning and attention to detail. Through our calculator example, we’ve covered the essential aspects of modern Python packaging:

  • Project structure and organization
  • Development workflow with both Poetry and setuptools approaches
  • Testing and validation
  • Distribution and maintenance

You May Also Like