Python Package Development Best Practices
Well-structured packages make code reusable, testable, and shareable. Whether for internal tooling or open source, these practices ensure your packages are maintainable.
Modern Project Structure
my-package/
├── src/
│ └── my_package/
│ ├── __init__.py
│ ├── core.py
│ └── utils.py
├── tests/
│ ├── __init__.py
│ ├── test_core.py
│ └── conftest.py
├── docs/
├── pyproject.toml
├── README.md
└── LICENSE
pyproject.toml Configuration
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "my-package"
version = "0.1.0"
description = "A useful package"
readme = "README.md"
requires-python = ">=3.9"
license = "MIT"
authors = [
{ name = "Your Name", email = "you@example.com" }
]
dependencies = [
"pandas>=2.0",
"requests>=2.28"
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"pytest-cov",
"ruff",
"mypy"
]
[project.scripts]
my-tool = "my_package.cli:main"
[tool.ruff]
target-version = "py39"
line-length = 88
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.mypy]
python_version = "3.9"
strict = true
Package Initialization
# src/my_package/__init__.py
"""My Package - A useful package for data operations."""
from my_package.core import DataProcessor, transform
from my_package.utils import validate, format_output
__version__ = "0.1.0"
__all__ = [
"DataProcessor",
"transform",
"validate",
"format_output",
]
Type Hints
from typing import Optional, Union
from pathlib import Path
import pandas as pd
def process_file(
filepath: Union[str, Path],
output_format: str = "csv",
columns: Optional[list[str]] = None
) -> pd.DataFrame:
"""
Process a data file.
Args:
filepath: Path to input file
output_format: Output format (csv, parquet)
columns: Columns to include
Returns:
Processed DataFrame
Raises:
FileNotFoundError: If file doesn't exist
ValueError: If output_format not supported
"""
path = Path(filepath)
if not path.exists():
raise FileNotFoundError(f"File not found: {path}")
# Implementation
...
Testing
# tests/test_core.py
import pytest
from my_package import DataProcessor
class TestDataProcessor:
@pytest.fixture
def processor(self):
return DataProcessor()
def test_basic_processing(self, processor, sample_data):
result = processor.process(sample_data)
assert len(result) > 0
@pytest.mark.parametrize("input,expected", [
("valid", True),
("", False),
(None, False),
])
def test_validation(self, processor, input, expected):
assert processor.validate(input) == expected
Development Workflow
# Create virtual environment
python -m venv .venv
source .venv/bin/activate
# Install in editable mode with dev dependencies
pip install -e ".[dev]"
# Run tests
pytest --cov=my_package
# Type checking
mypy src/
# Linting
ruff check src/
# Build package
python -m build
# Publish to PyPI
twine upload dist/*
Invest in package structure early—it pays off as your codebase grows.