Skip to content

Usage Examples

This guide provides real-world examples of creating different types of Python packages using this template.

Basic Library Package

Let's create a simple math utilities library:

cookiecutter https://github.com/s-celles/cookiecutter-python-package.git

Configuration:

full_name [Your Name]: Jane Smith
email [your.email@example.com]: jane.smith@example.com
github_username [yourusername]: janesmith
project_name [My Python Package]: Math Utilities
project_slug [math_utilities]:
project_short_description [A modern Python package with best practices built-in.]: A collection of mathematical utility functions
version [0.1.0]:
python_requires [>=3.9]:
license [MIT]:
build_backend [setuptools]:
use_ruff [y]:
use_mypy [y]:
use_pytest [y]:
use_coverage [y]:
use_pre_commit [y]:
use_bandit [n]:
use_safety [n]:
use_github_actions [y]:
command_line_interface [none]:
use_mkdocs [y]:

Generated structure:

math_utilities/
├── src/
│   └── math_utilities/
│       ├── __init__.py
│       ├── core.py
│       └── py.typed
├── tests/
│   ├── test_core.py
│   └── test_math_utilities.py
├── docs/
├── pyproject.toml
├── README.md
└── .github/workflows/ci.yml

Example implementation (src/math_utilities/core.py):

"""Core mathematical utilities."""

from typing import List, Union

Number = Union[int, float]


def factorial(n: int) -> int:
    """Calculate the factorial of a number.

    Args:
        n: A non-negative integer

    Returns:
        The factorial of n

    Raises:
        ValueError: If n is negative
        TypeError: If n is not an integer
    """
    if not isinstance(n, int):
        raise TypeError("Input must be an integer")
    if n < 0:
        raise ValueError("Input must be non-negative")

    if n <= 1:
        return 1
    return n * factorial(n - 1)


def mean(numbers: List[Number]) -> float:
    """Calculate the arithmetic mean of a list of numbers.

    Args:
        numbers: List of numbers

    Returns:
        The arithmetic mean

    Raises:
        ValueError: If the list is empty
    """
    if not numbers:
        raise ValueError("Cannot calculate mean of empty list")

    return sum(numbers) / len(numbers)


def is_prime(n: int) -> bool:
    """Check if a number is prime.

    Args:
        n: Integer to check

    Returns:
        True if n is prime, False otherwise
    """
    if not isinstance(n, int):
        return False
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False

    for i in range(3, int(n**0.5) + 1, 2):
        if n % i == 0:
            return False
    return True

CLI Application

Creating a file processing tool with a command-line interface:

cookiecutter https://github.com/s-celles/cookiecutter-python-package.git

Configuration:

project_name: File Processor
project_slug: file_processor
project_short_description: A command-line tool for processing text files
command_line_interface: typer
use_ruff: y
use_mypy: y
use_pytest: y
use_coverage: y
use_pre_commit: y
use_bandit: y
use_safety: y

Example CLI implementation (src/file_processor/cli.py):

"""Command-line interface for file processor."""

import typer
from pathlib import Path
from typing import Optional
from rich.console import Console
from rich.progress import Progress

from .core import FileProcessor

app = typer.Typer(help="Process text files with various operations")
console = Console()


@app.command()
def count_words(
    file_path: Path = typer.Argument(..., help="Path to the text file"),
    output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path"),
    verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose output")
) -> None:
    """Count words in a text file."""
    if not file_path.exists():
        console.print(f"[red]Error: File {file_path} does not exist[/red]")
        raise typer.Exit(1)

    try:
        processor = FileProcessor(file_path)
        word_count = processor.count_words()

        result = f"Word count: {word_count}"

        if output:
            output.write_text(result)
            console.print(f"[green]Results saved to {output}[/green]")
        else:
            console.print(result)

        if verbose:
            console.print(f"[blue]Processed file: {file_path}[/blue]")

    except Exception as e:
        console.print(f"[red]Error processing file: {e}[/red]")
        raise typer.Exit(1)


@app.command()
def find_duplicates(
    directory: Path = typer.Argument(..., help="Directory to search"),
    pattern: str = typer.Option("*.txt", "--pattern", "-p", help="File pattern to match"),
    output_format: str = typer.Option("table", "--format", "-f", help="Output format (table, json, csv)")
) -> None:
    """Find duplicate lines across multiple files."""
    if not directory.is_dir():
        console.print(f"[red]Error: {directory} is not a directory[/red]")
        raise typer.Exit(1)

    files = list(directory.glob(pattern))
    if not files:
        console.print(f"[yellow]No files found matching pattern {pattern}[/yellow]")
        return

    with Progress() as progress:
        task = progress.add_task("Processing files...", total=len(files))

        processor = FileProcessor()
        duplicates = processor.find_duplicates(files)

        progress.update(task, completed=len(files))

    if output_format == "json":
        import json
        console.print(json.dumps(duplicates, indent=2))
    elif output_format == "csv":
        # CSV output implementation
        pass
    else:
        # Table output (default)
        from rich.table import Table
        table = Table(title="Duplicate Lines")
        table.add_column("Line")
        table.add_column("Files")

        for line, file_list in duplicates.items():
            table.add_row(line, ", ".join(file_list))

        console.print(table)


if __name__ == "__main__":
    app()

Data Science Package

Creating a package for data analysis with Jupyter notebook support:

Configuration:

project_name: Data Analysis Toolkit
project_slug: data_analysis_toolkit
project_short_description: Tools for data analysis and visualization
use_jupyter: y
use_pandas: y
use_numpy: y
use_matplotlib: y

Example core module (src/data_analysis_toolkit/core.py):

"""Core data analysis functions."""

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from typing import List, Dict, Any, Optional
from pathlib import Path


class DataAnalyzer:
    """Main class for data analysis operations."""

    def __init__(self, data: Optional[pd.DataFrame] = None):
        """Initialize with optional data."""
        self.data = data

    def load_csv(self, file_path: Path, **kwargs) -> pd.DataFrame:
        """Load data from CSV file.

        Args:
            file_path: Path to CSV file
            **kwargs: Additional arguments for pd.read_csv

        Returns:
            Loaded DataFrame
        """
        self.data = pd.read_csv(file_path, **kwargs)
        return self.data

    def basic_stats(self) -> Dict[str, Any]:
        """Generate basic statistics for the dataset.

        Returns:
            Dictionary containing basic statistics
        """
        if self.data is None:
            raise ValueError("No data loaded")

        return {
            'shape': self.data.shape,
            'columns': list(self.data.columns),
            'dtypes': self.data.dtypes.to_dict(),
            'missing_values': self.data.isnull().sum().to_dict(),
            'memory_usage': self.data.memory_usage(deep=True).sum(),
            'numeric_summary': self.data.describe().to_dict()
        }

    def plot_missing_values(self, figsize: tuple = (12, 6)) -> plt.Figure:
        """Create visualization of missing values.

        Args:
            figsize: Figure size for the plot

        Returns:
            Matplotlib figure object
        """
        if self.data is None:
            raise ValueError("No data loaded")

        fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)

        # Missing values by column
        missing_counts = self.data.isnull().sum()
        missing_counts = missing_counts[missing_counts > 0].sort_values(ascending=False)

        if len(missing_counts) > 0:
            missing_counts.plot(kind='bar', ax=ax1)
            ax1.set_title('Missing Values by Column')
            ax1.set_ylabel('Count')

            # Missing values heatmap
            missing_matrix = self.data.isnull()
            ax2.imshow(missing_matrix, cmap='viridis', aspect='auto')
            ax2.set_title('Missing Values Pattern')
            ax2.set_xlabel('Columns')
            ax2.set_ylabel('Rows')
        else:
            ax1.text(0.5, 0.5, 'No missing values', ha='center', va='center')
            ax2.text(0.5, 0.5, 'No missing values', ha='center', va='center')

        plt.tight_layout()
        return fig

    def correlation_analysis(self, method: str = 'pearson') -> pd.DataFrame:
        """Calculate correlation matrix for numeric columns.

        Args:
            method: Correlation method ('pearson', 'spearman', 'kendall')

        Returns:
            Correlation matrix
        """
        if self.data is None:
            raise ValueError("No data loaded")

        numeric_data = self.data.select_dtypes(include=[np.number])
        return numeric_data.corr(method=method)

Web API Package

Creating a FastAPI-based web service:

Configuration:

project_name: Task API
project_slug: task_api
project_short_description: RESTful API for task management
use_fastapi: y
use_sqlalchemy: y
use_alembic: y
use_pydantic: y

Example API implementation (src/task_api/api.py):

"""FastAPI application for task management."""

from fastapi import FastAPI, HTTPException, Depends
from sqlalchemy.orm import Session
from typing import List
from .database import get_db
from .models import Task
from .schemas import TaskCreate, TaskResponse, TaskUpdate

app = FastAPI(
    title="Task API",
    description="A simple task management API",
    version="0.1.0"
)


@app.post("/tasks/", response_model=TaskResponse)
def create_task(task: TaskCreate, db: Session = Depends(get_db)):
    """Create a new task."""
    db_task = Task(**task.dict())
    db.add(db_task)
    db.commit()
    db.refresh(db_task)
    return db_task


@app.get("/tasks/", response_model=List[TaskResponse])
def list_tasks(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    """List all tasks."""
    tasks = db.query(Task).offset(skip).limit(limit).all()
    return tasks


@app.get("/tasks/{task_id}", response_model=TaskResponse)
def get_task(task_id: int, db: Session = Depends(get_db)):
    """Get a specific task by ID."""
    task = db.query(Task).filter(Task.id == task_id).first()
    if task is None:
        raise HTTPException(status_code=404, detail="Task not found")
    return task


@app.put("/tasks/{task_id}", response_model=TaskResponse)
def update_task(task_id: int, task_update: TaskUpdate, db: Session = Depends(get_db)):
    """Update a task."""
    task = db.query(Task).filter(Task.id == task_id).first()
    if task is None:
        raise HTTPException(status_code=404, detail="Task not found")

    for field, value in task_update.dict(exclude_unset=True).items():
        setattr(task, field, value)

    db.commit()
    db.refresh(task)
    return task


@app.delete("/tasks/{task_id}")
def delete_task(task_id: int, db: Session = Depends(get_db)):
    """Delete a task."""
    task = db.query(Task).filter(Task.id == task_id).first()
    if task is None:
        raise HTTPException(status_code=404, detail="Task not found")

    db.delete(task)
    db.commit()
    return {"message": "Task deleted successfully"}

Machine Learning Package

Creating a scikit-learn compatible estimator:

Configuration:

project_name: ML Estimators
project_slug: ml_estimators
project_short_description: Custom machine learning estimators
use_sklearn: y
use_numpy: y
use_pandas: y
use_jupyter: y

Example estimator (src/ml_estimators/estimators.py):

"""Custom machine learning estimators."""

import numpy as np
from sklearn.base import BaseEstimator, TransformerMixin, ClassifierMixin
from sklearn.utils.validation import check_X_y, check_array, check_is_fitted
from typing import Optional


class SimpleScaler(BaseEstimator, TransformerMixin):
    """A simple feature scaler that normalizes features to [0, 1] range."""

    def __init__(self):
        """Initialize the scaler."""
        self.min_: Optional[np.ndarray] = None
        self.scale_: Optional[np.ndarray] = None

    def fit(self, X, y=None):
        """Fit the scaler to the data.

        Args:
            X: Training data
            y: Ignored (for compatibility)

        Returns:
            self
        """
        X = check_array(X)

        self.min_ = np.min(X, axis=0)
        self.scale_ = np.max(X, axis=0) - self.min_

        # Handle case where max == min (constant features)
        self.scale_[self.scale_ == 0] = 1

        return self

    def transform(self, X):
        """Transform the data.

        Args:
            X: Data to transform

        Returns:
            Transformed data
        """
        check_is_fitted(self, ['min_', 'scale_'])
        X = check_array(X)

        return (X - self.min_) / self.scale_

    def inverse_transform(self, X):
        """Inverse transform the data.

        Args:
            X: Transformed data

        Returns:
            Original scale data
        """
        check_is_fitted(self, ['min_', 'scale_'])
        X = check_array(X)

        return X * self.scale_ + self.min_


class KNearestCentroid(BaseEstimator, ClassifierMixin):
    """K-Nearest Centroid classifier."""

    def __init__(self, n_neighbors: int = 3):
        """Initialize the classifier.

        Args:
            n_neighbors: Number of neighbors to consider
        """
        self.n_neighbors = n_neighbors

    def fit(self, X, y):
        """Fit the classifier.

        Args:
            X: Training features
            y: Training labels

        Returns:
            self
        """
        X, y = check_X_y(X, y)

        self.classes_ = np.unique(y)
        self.centroids_ = np.array([
            np.mean(X[y == class_label], axis=0)
            for class_label in self.classes_
        ])

        return self

    def predict(self, X):
        """Predict class labels.

        Args:
            X: Features to predict

        Returns:
            Predicted labels
        """
        check_is_fitted(self, ['classes_', 'centroids_'])
        X = check_array(X)

        # Calculate distances to all centroids
        distances = np.sqrt(((X[:, np.newaxis, :] - self.centroids_[np.newaxis, :, :]) ** 2).sum(axis=2))

        # Find nearest centroids
        nearest_indices = np.argmin(distances, axis=1)

        return self.classes_[nearest_indices]

    def predict_proba(self, X):
        """Predict class probabilities.

        Args:
            X: Features to predict

        Returns:
            Class probabilities
        """
        check_is_fitted(self, ['classes_', 'centroids_'])
        X = check_array(X)

        # Calculate distances to all centroids
        distances = np.sqrt(((X[:, np.newaxis, :] - self.centroids_[np.newaxis, :, :]) ** 2).sum(axis=2))

        # Convert distances to probabilities (inverse distance weighting)
        probabilities = 1 / (distances + 1e-8)  # Add small epsilon to avoid division by zero
        probabilities = probabilities / probabilities.sum(axis=1, keepdims=True)

        return probabilities

Testing Examples

Each package type should include comprehensive tests:

Test for library package (tests/test_core.py):

"""Tests for core math utilities."""

import pytest
from math_utilities.core import factorial, mean, is_prime


class TestFactorial:
    """Tests for factorial function."""

    def test_factorial_base_cases(self):
        """Test factorial base cases."""
        assert factorial(0) == 1
        assert factorial(1) == 1

    def test_factorial_positive_integers(self):
        """Test factorial with positive integers."""
        assert factorial(5) == 120
        assert factorial(10) == 3628800

    def test_factorial_negative_input(self):
        """Test factorial with negative input."""
        with pytest.raises(ValueError, match="non-negative"):
            factorial(-1)

    def test_factorial_non_integer_input(self):
        """Test factorial with non-integer input."""
        with pytest.raises(TypeError, match="integer"):
            factorial(3.14)


class TestMean:
    """Tests for mean function."""

    def test_mean_integers(self):
        """Test mean with integers."""
        assert mean([1, 2, 3, 4, 5]) == 3.0

    def test_mean_floats(self):
        """Test mean with floats."""
        assert mean([1.5, 2.5, 3.5]) == 2.5

    def test_mean_empty_list(self):
        """Test mean with empty list."""
        with pytest.raises(ValueError, match="empty list"):
            mean([])


@pytest.mark.parametrize("number,expected", [
    (2, True),
    (3, True),
    (4, False),
    (17, True),
    (25, False),
    (1, False),
    (0, False),
    (-1, False),
])
def test_is_prime(number, expected):
    """Test is_prime function with various inputs."""
    assert is_prime(number) == expected

These examples demonstrate how to create different types of Python packages using this template, from simple libraries to complex applications with web APIs and machine learning components.