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 codetests/
: Directory for test filespyproject.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
- 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
- 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.
- 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
- 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:
- 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
- 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
- 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:
- 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
- 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:
- All tests pass:
pytest tests/
- Documentation is up to date
- Version number is correct in
pyproject.toml
- 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
- Install from Test PyPI:
pip install --index-url https://test.pypi.org/simple/ text-calculator
- 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