with one click
python-backend-dev
// Comprehensive Python backend development guide with CI/CD workflows, testing, documentation, and best practices using uv, flake8, mypy, pytest, and Sphinx.
// Comprehensive Python backend development guide with CI/CD workflows, testing, documentation, and best practices using uv, flake8, mypy, pytest, and Sphinx.
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | python-backend-dev |
| description | Comprehensive Python backend development guide with CI/CD workflows, testing, documentation, and best practices using uv, flake8, mypy, pytest, and Sphinx. |
| argument-hint | [{"task":"setup-project | run-tests | build-docs | lint"}] |
| claude-skill-version | 1 |
This skill guide provides production-ready workflows, CI/CD pipelines, testing strategies, and documentation standards for Python backend development. It covers modern Python tooling including uv package management, flake8 linting, mypy type checking, pytest testing, and Sphinx documentation generation.
Backend developers working on Python projects who need:
Purpose: Enforce code style following PEP 8 standards
Configuration (.flake8):
[flake8]
max-line-length = 100
ignore = E203, E266, E501, W503
max-complexity = 10
exclude =
.git,
__pycache__,
build,
dist,
.venv,
venv,
.eggs,
*.egg-info
Run Command:
flake8 src/ tests/
CI Integration: See complete workflow in section 4.1
Purpose: Static type analysis for type safety
Configuration (in pyproject.toml):
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_any_generics = true
check_untyped_defs = true
no_implicit_reexport = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
Run Command:
mypy src/
CI Integration: See complete workflow in section 4.1
Purpose: Comprehensive test coverage with detailed reporting
Configuration (in pyproject.toml):
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = [
"--cov=src",
"--cov-report=html",
"--cov-report=xml",
"--cov-report=term-missing",
"--cov-fail-under=80",
"--junitxml=test-results/junit.xml",
"-v"
]
Run Command:
pytest tests/
CI Integration: See complete workflow in section 4.1
Purpose: Generate API documentation with GitHub Pages deployment
Initial Setup:
# Install Sphinx
uv add --dev sphinx sphinx-rtd-theme
# Initialize Sphinx
cd docs
sphinx-quickstart --quiet --project="My Project" --author="Your Name" -v "1.0"
Build Command:
sphinx-build -b html docs/source docs/build/html
CI Integration: See complete workflow in section 4.2
# Initialize new project
uv init my-project
cd my-project
# Initialize in existing directory
uv init
# Add production dependency
uv add requests
# Add development dependency
uv add --dev pytest pytest-cov
# Add specific version
uv add "fastapi==0.109.0"
# Install all dependencies
uv sync
# Update dependencies
uv lock --upgrade
uv automatically manages uv.lock file for reproducible builds:
# Build distribution packages
uv build
# Creates:
# - dist/*.whl (wheel)
# - dist/*.tar.gz (source distribution)
[project]
name = "my-backend-project"
version = "1.0.0"
description = "A production-ready Python backend"
authors = [
{name = "Your Name", email = "your.email@example.com"}
]
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.109.0",
"uvicorn>=0.27.0",
"sqlalchemy>=2.0.25",
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.4",
"pytest-cov>=4.1.0",
"flake8>=7.0.0",
"mypy>=1.8.0",
"sphinx>=7.2.6",
"sphinx-rtd-theme>=2.0.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv]
dev-dependencies = [
"pytest>=7.4.4",
"pytest-cov>=4.1.0",
"flake8>=7.0.0",
"mypy>=1.8.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = [
"--cov=src",
"--cov-report=html",
"--cov-report=xml",
"--cov-report=term-missing",
"--cov-fail-under=80",
"--junitxml=test-results/junit.xml",
"-v"
]
[tool.mypy]
python_version = "3.12"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_any_generics = true
check_untyped_defs = true
no_implicit_reexport = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
[tool.coverage.run]
source = ["src"]
omit = [
"*/tests/*",
"*/__pycache__/*",
"*/site-packages/*",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
from typing import Optional, List, Dict, Any, Union, Tuple
from datetime import datetime
def process_user_data(
user_id: int,
username: str,
email: Optional[str] = None,
tags: List[str] = None,
metadata: Dict[str, Any] = None
) -> Dict[str, Union[str, int, List[str]]]:
"""
Process user data and return formatted result.
:param user_id: Unique identifier for the user
:type user_id: int
:param username: Username for the account
:type username: str
:param email: Optional email address
:type email: Optional[str]
:param tags: List of user tags
:type tags: Optional[List[str]]
:param metadata: Additional metadata dictionary
:type metadata: Optional[Dict[str, Any]]
:return: Formatted user data dictionary
:rtype: Dict[str, Union[str, int, List[str]]]
:raises ValueError: If user_id is negative
:raises TypeError: If username is not a string
.. note::
This function validates all input parameters before processing.
.. warning::
Large metadata dictionaries may impact performance.
.. seealso::
:func:`validate_user_data` for validation logic
**Example Usage:**
.. code-block:: python
result = process_user_data(
user_id=123,
username="john_doe",
email="john@example.com",
tags=["admin", "verified"]
)
print(result)
# {'id': 123, 'name': 'john_doe', 'email': 'john@example.com', 'tags': ['admin', 'verified']}
"""
if user_id < 0:
raise ValueError("user_id must be non-negative")
if not isinstance(username, str):
raise TypeError("username must be a string")
tags = tags or []
metadata = metadata or {}
return {
"id": user_id,
"name": username,
"email": email or "not_provided",
"tags": tags,
}
from typing import Optional, List
from datetime import datetime
class DatabaseConnection:
"""
Manages database connection lifecycle with context manager support.
This class provides a robust connection manager with automatic resource
cleanup and error handling.
:ivar host: Database host address
:vartype host: str
:ivar port: Database port number
:vartype port: int
:ivar database: Database name
:vartype database: str
:ivar connected: Connection status flag
:vartype connected: bool
:ivar connection_time: Timestamp of last connection
:vartype connection_time: Optional[datetime]
.. note::
Always use this class as a context manager to ensure proper cleanup.
.. warning::
Connections are not thread-safe. Create separate instances per thread.
**Example Usage:**
.. code-block:: python
with DatabaseConnection("localhost", 5432, "mydb") as conn:
result = conn.execute("SELECT * FROM users")
print(result)
"""
def __init__(self, host: str, port: int, database: str) -> None:
"""
Initialize database connection parameters.
:param host: Database server hostname or IP
:type host: str
:param port: Database server port
:type port: int
:param database: Target database name
:type database: str
:raises ValueError: If port is not in valid range (1-65535)
"""
if not 1 <= port <= 65535:
raise ValueError("Port must be between 1 and 65535")
self.host: str = host
self.port: int = port
self.database: str = database
self.connected: bool = False
self.connection_time: Optional[datetime] = None
def __enter__(self) -> "DatabaseConnection":
"""
Enter context manager and establish connection.
:return: The connection instance
:rtype: DatabaseConnection
:raises ConnectionError: If connection fails
"""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
"""
Exit context manager and close connection.
:param exc_type: Exception type if raised
:param exc_val: Exception value if raised
:param exc_tb: Exception traceback if raised
:return: False to propagate exceptions
:rtype: bool
"""
self.disconnect()
return False
def connect(self) -> None:
"""
Establish database connection.
:raises ConnectionError: If connection cannot be established
.. note::
Connection parameters are validated before attempting connection.
"""
# Connection logic here
self.connected = True
self.connection_time = datetime.now()
def disconnect(self) -> None:
"""
Close database connection and cleanup resources.
.. note::
Safe to call multiple times - idempotent operation.
"""
if self.connected:
# Cleanup logic here
self.connected = False
def execute(self, query: str) -> List[Dict[str, Any]]:
"""
Execute SQL query and return results.
:param query: SQL query string
:type query: str
:return: List of result dictionaries
:rtype: List[Dict[str, Any]]
:raises RuntimeError: If not connected
:raises ValueError: If query is empty
.. warning::
This method does not provide SQL injection protection.
Use parameterized queries in production.
"""
if not self.connected:
raise RuntimeError("Not connected to database")
if not query.strip():
raise ValueError("Query cannot be empty")
# Query execution logic here
return []
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
lint-and-type-check:
name: Lint and Type Check
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
uv sync --dev
- name: Run flake8
run: |
uv run flake8 src/ tests/
- name: Run mypy
run: |
uv run mypy src/
test:
name: Test Suite
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
uv sync --dev
- name: Run pytest
run: |
uv run pytest tests/ --cov=src --cov-report=xml --cov-report=html --cov-report=term-missing --junitxml=test-results/junit.xml
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.python-version }}
path: test-results/
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report-${{ matrix.python-version }}
path: htmlcov/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
flags: unittests
name: codecov-${{ matrix.python-version }}
fail_ci_if_error: false
build:
name: Build Package
runs-on: ubuntu-latest
needs: [lint-and-type-check, test]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Build package
run: |
uv build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist-packages
path: dist/
name: Documentation
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-docs:
name: Build Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
- name: Install dependencies
run: |
uv sync --dev
uv add --dev sphinx sphinx-rtd-theme
- name: Generate API documentation
run: |
uv run sphinx-apidoc -f -o docs/source/api src/
- name: Build Sphinx documentation
run: |
uv run sphinx-build -b html docs/source docs/build/html
- name: Upload documentation artifact
uses: actions/upload-artifact@v4
with:
name: documentation
path: docs/build/html/
- name: Deploy to GitHub Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/build/html
tests/test_user_service.py:
import pytest
from typing import List, Dict, Any
from unittest.mock import Mock, patch
from src.services.user_service import UserService, User
class TestUserService:
"""Test suite for UserService class."""
def setup_method(self) -> None:
"""Set up test fixtures before each test method."""
self.service = UserService()
def teardown_method(self) -> None:
"""Clean up after each test method."""
self.service = None
def test_create_user_success(self) -> None:
"""Test successful user creation."""
user = self.service.create_user(
username="testuser",
email="test@example.com"
)
assert user.username == "testuser"
assert user.email == "test@example.com"
assert user.id is not None
def test_create_user_duplicate_username(self) -> None:
"""Test that duplicate usernames raise ValueError."""
self.service.create_user(username="testuser", email="test1@example.com")
with pytest.raises(ValueError, match="Username already exists"):
self.service.create_user(username="testuser", email="test2@example.com")
@pytest.mark.parametrize("username,email,expected_error", [
("", "test@example.com", "Username cannot be empty"),
("testuser", "", "Email cannot be empty"),
("ab", "test@example.com", "Username must be at least 3 characters"),
("testuser", "invalid-email", "Invalid email format"),
])
def test_create_user_validation_errors(
self,
username: str,
email: str,
expected_error: str
) -> None:
"""Test user creation validation with various invalid inputs."""
with pytest.raises(ValueError, match=expected_error):
self.service.create_user(username=username, email=email)
def test_get_user_by_id_found(self) -> None:
"""Test retrieving an existing user by ID."""
created_user = self.service.create_user(
username="testuser",
email="test@example.com"
)
retrieved_user = self.service.get_user_by_id(created_user.id)
assert retrieved_user is not None
assert retrieved_user.id == created_user.id
assert retrieved_user.username == created_user.username
def test_get_user_by_id_not_found(self) -> None:
"""Test retrieving a non-existent user returns None."""
user = self.service.get_user_by_id(99999)
assert user is None
@patch('src.services.user_service.send_email')
def test_create_user_sends_welcome_email(self, mock_send_email: Mock) -> None:
"""Test that user creation triggers welcome email."""
user = self.service.create_user(
username="testuser",
email="test@example.com"
)
mock_send_email.assert_called_once_with(
to=user.email,
subject="Welcome!",
body=pytest.approx("Welcome testuser", rel=1e-3)
)
tests/conftest.py:
import pytest
from typing import Generator
from src.database import Database
from src.services.user_service import UserService
@pytest.fixture
def database() -> Generator[Database, None, None]:
"""
Provide a test database instance.
Yields:
Database: A configured test database instance
"""
db = Database(url="sqlite:///:memory:")
db.create_tables()
yield db
db.drop_tables()
db.close()
@pytest.fixture
def user_service(database: Database) -> UserService:
"""
Provide a UserService instance with test database.
Args:
database: Test database fixture
Returns:
UserService: Configured service instance
"""
return UserService(database=database)
@pytest.fixture
def sample_users() -> List[Dict[str, str]]:
"""
Provide sample user data for testing.
Returns:
List of user dictionaries
"""
return [
{"username": "user1", "email": "user1@example.com"},
{"username": "user2", "email": "user2@example.com"},
{"username": "user3", "email": "user3@example.com"},
]
Coverage settings are in pyproject.toml (see section 2.5).
Verify Coverage:
# Run tests with coverage
pytest --cov=src --cov-report=html --cov-report=term-missing
# Open HTML report
open htmlcov/index.html
# Add Sphinx dependencies
uv add --dev sphinx sphinx-rtd-theme
# Create docs directory
mkdir -p docs/source
# Initialize Sphinx (or create manually)
cd docs
sphinx-quickstart --quiet --project="My Backend" --author="Your Name" -v "1.0"
# Configuration file for the Sphinx documentation builder.
import os
import sys
sys.path.insert(0, os.path.abspath('../../src'))
# -- Project information -----------------------------------------------------
project = 'My Backend Project'
copyright = '2026, Your Name'
author = 'Your Name'
release = '1.0.0'
# -- General configuration ---------------------------------------------------
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.napoleon',
'sphinx.ext.viewcode',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
]
templates_path = ['_templates']
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
html_theme = 'sphinx_rtd_theme'
html_static_path = ['_static']
# -- Extension configuration -------------------------------------------------
# Napoleon settings for Google/NumPy style docstrings
napoleon_google_docstring = True
napoleon_numpy_docstring = True
napoleon_include_init_with_doc = True
napoleon_include_private_with_doc = False
napoleon_include_special_with_doc = True
napoleon_use_admonition_for_examples = True
napoleon_use_admonition_for_notes = True
napoleon_use_admonition_for_references = False
napoleon_use_ivar = True
napoleon_use_param = True
napoleon_use_rtype = True
napoleon_type_aliases = None
# Autodoc settings
autodoc_default_options = {
'members': True,
'member-order': 'bysource',
'special-members': '__init__',
'undoc-members': True,
'exclude-members': '__weakref__'
}
# Intersphinx mapping
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'sphinx': ('https://www.sphinx-doc.org/en/master', None),
}
# Todo extension
todo_include_todos = True
Welcome to My Backend Project Documentation
===========================================
.. toctree::
:maxdepth: 2
:caption: Contents:
installation
quickstart
api/modules
examples
changelog
Overview
--------
This is a comprehensive Python backend project with full CI/CD integration,
type safety, and extensive documentation.
Features
--------
* **Type-Safe**: Full mypy strict mode compliance
* **Well-Tested**: >80% code coverage with pytest
* **CI/CD Ready**: GitHub Actions workflows included
* **API Documentation**: Auto-generated with Sphinx
Quick Start
-----------
Install the package:
.. code-block:: bash
pip install my-backend-project
Basic usage:
.. code-block:: python
from my_backend import UserService
service = UserService()
user = service.create_user("john_doe", "john@example.com")
print(user)
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
# Generate API documentation from source code
sphinx-apidoc -f -o docs/source/api src/
# Build HTML documentation
sphinx-build -b html docs/source docs/build/html
# View documentation
open docs/build/html/index.html
Documentation is automatically deployed via the workflow in section 4.2. To enable GitHub Pages:
https://<username>.github.io/<repo>/[flake8]
max-line-length = 100
ignore = E203, E266, E501, W503
max-complexity = 10
exclude =
.git,
__pycache__,
build,
dist,
.venv,
venv,
.eggs,
*.egg-info,
.tox,
.pytest_cache
per-file-ignores =
__init__.py:F401
Explanation:
E203: Whitespace before ':' (conflicts with Black)E266: Too many leading '#' for block commentE501: Line too long (handled by max-line-length)W503: Line break before binary operator (conflicts with PEP 8 update)max-complexity: McCabe complexity thresholdSee section 2.5 for the complete configuration file including:
from typing import Optional, List, Dict, Any
from datetime import datetime
from dataclasses import dataclass
@dataclass
class User:
"""
User domain model.
:ivar id: Unique user identifier
:vartype id: int
:ivar username: User's username
:vartype username: str
:ivar email: User's email address
:vartype email: str
:ivar created_at: Account creation timestamp
:vartype created_at: datetime
"""
id: int
username: str
email: str
created_at: datetime
class UserRepository:
"""
Repository for user data access.
Handles all database operations for User entities.
.. note::
This repository uses connection pooling for efficiency.
"""
def __init__(self, connection_string: str) -> None:
"""
Initialize repository with database connection.
:param connection_string: Database connection URL
:type connection_string: str
"""
self.connection_string = connection_string
self.users: Dict[int, User] = {}
self.next_id: int = 1
def create(self, username: str, email: str) -> User:
"""
Create a new user.
:param username: Desired username
:type username: str
:param email: User email address
:type email: str
:return: Created user instance
:rtype: User
:raises ValueError: If username or email is invalid
"""
user = User(
id=self.next_id,
username=username,
email=email,
created_at=datetime.now()
)
self.users[self.next_id] = user
self.next_id += 1
return user
def find_by_id(self, user_id: int) -> Optional[User]:
"""
Find user by ID.
:param user_id: User identifier
:type user_id: int
:return: User if found, None otherwise
:rtype: Optional[User]
"""
return self.users.get(user_id)
def find_all(self) -> List[User]:
"""
Retrieve all users.
:return: List of all users
:rtype: List[User]
"""
return list(self.users.values())
from typing import Optional
from contextlib import contextmanager
class TransactionManager:
"""
Manages database transactions with context manager support.
:ivar connection: Database connection instance
:vartype connection: Any
:ivar in_transaction: Transaction active flag
:vartype in_transaction: bool
.. warning::
Nested transactions are not supported.
**Example Usage:**
.. code-block:: python
with TransactionManager(conn) as txn:
txn.execute("INSERT INTO users VALUES (...)")
txn.execute("UPDATE accounts SET balance = ...")
# Automatically commits on success, rolls back on exception
"""
def __init__(self, connection: Any) -> None:
"""
Initialize transaction manager.
:param connection: Database connection
:type connection: Any
"""
self.connection = connection
self.in_transaction: bool = False
def __enter__(self) -> "TransactionManager":
"""
Begin transaction.
:return: Transaction manager instance
:rtype: TransactionManager
:raises RuntimeError: If already in transaction
"""
if self.in_transaction:
raise RuntimeError("Already in transaction")
self.connection.begin()
self.in_transaction = True
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
"""
Commit or rollback transaction.
:param exc_type: Exception type if raised
:param exc_val: Exception value if raised
:param exc_tb: Exception traceback if raised
:return: False to propagate exceptions
:rtype: bool
"""
if exc_type is None:
self.connection.commit()
else:
self.connection.rollback()
self.in_transaction = False
return False
def execute(self, query: str) -> Any:
"""
Execute SQL query within transaction.
:param query: SQL query string
:type query: str
:return: Query result
:rtype: Any
:raises RuntimeError: If not in transaction
"""
if not self.in_transaction:
raise RuntimeError("Not in transaction")
return self.connection.execute(query)
import pytest
from typing import List
from unittest.mock import Mock, patch, MagicMock
from src.services.user_service import UserService, User
from src.repositories.user_repository import UserRepository
class TestUserRepository:
"""Test suite for UserRepository."""
@pytest.fixture
def repository(self) -> UserRepository:
"""Provide a test repository instance."""
return UserRepository("sqlite:///:memory:")
def test_create_user(self, repository: UserRepository) -> None:
"""Test user creation."""
user = repository.create("testuser", "test@example.com")
assert user.id == 1
assert user.username == "testuser"
assert user.email == "test@example.com"
def test_find_by_id_exists(self, repository: UserRepository) -> None:
"""Test finding existing user."""
created = repository.create("testuser", "test@example.com")
found = repository.find_by_id(created.id)
assert found is not None
assert found.id == created.id
def test_find_by_id_not_exists(self, repository: UserRepository) -> None:
"""Test finding non-existent user."""
found = repository.find_by_id(999)
assert found is None
@pytest.mark.parametrize("username,email", [
("user1", "user1@example.com"),
("user2", "user2@example.com"),
("user3", "user3@example.com"),
])
def test_create_multiple_users(
self,
repository: UserRepository,
username: str,
email: str
) -> None:
"""Test creating multiple users with different data."""
user = repository.create(username, email)
assert user.username == username
assert user.email == email
class TestTransactionManager:
"""Test suite for TransactionManager."""
@pytest.fixture
def mock_connection(self) -> Mock:
"""Provide a mock database connection."""
connection = Mock()
connection.begin = Mock()
connection.commit = Mock()
connection.rollback = Mock()
connection.execute = Mock(return_value="result")
return connection
def test_successful_transaction(self, mock_connection: Mock) -> None:
"""Test successful transaction commits."""
from src.transaction import TransactionManager
with TransactionManager(mock_connection) as txn:
result = txn.execute("SELECT * FROM users")
mock_connection.begin.assert_called_once()
mock_connection.commit.assert_called_once()
mock_connection.rollback.assert_not_called()
assert result == "result"
def test_failed_transaction_rollback(self, mock_connection: Mock) -> None:
"""Test failed transaction rolls back."""
from src.transaction import TransactionManager
with pytest.raises(ValueError):
with TransactionManager(mock_connection) as txn:
txn.execute("INSERT INTO users VALUES (...)")
raise ValueError("Simulated error")
mock_connection.begin.assert_called_once()
mock_connection.rollback.assert_called_once()
mock_connection.commit.assert_not_called()
def test_nested_transaction_raises(self, mock_connection: Mock) -> None:
"""Test that nested transactions are not allowed."""
from src.transaction import TransactionManager
txn = TransactionManager(mock_connection)
with txn:
with pytest.raises(RuntimeError, match="Already in transaction"):
with txn:
pass
Do:
typing module: Optional, List, Dict, UnionNone as return type for functions without return value-> None instead of omitting return typeOptional[T] for nullable typesUnion[T1, T2] for multi-type parametersDon't:
Any unless absolutely necessarylist=[])list[str] vs List[str])Do:
:param:, :type:, :return:, :rtype:, :raises:.. note::, .. warning::, .. seealso:::ivar: and :vartype:Don't:
Do:
test_<method>_<scenario>_<expected_result>Don't:
Do:
Don't:
from module import *)Do:
uv.lock to version control--dev flaguv sync after pulling changesuv lock --upgradeDon't:
uv.lockpip alongside uv in the same project# Initialize new project
uv init my-project
cd my-project
# Add dependencies
uv add fastapi uvicorn sqlalchemy pydantic
# Add dev dependencies
uv add --dev pytest pytest-cov flake8 mypy sphinx sphinx-rtd-theme
# Install all dependencies
uv sync
# Run flake8
flake8 src/ tests/
# Run mypy
mypy src/
# Run both
flake8 src/ tests/ && mypy src/
# Run all tests
pytest tests/
# Run with coverage
pytest tests/ --cov=src --cov-report=html --cov-report=term-missing
# Run specific test file
pytest tests/test_user_service.py
# Run specific test
pytest tests/test_user_service.py::TestUserService::test_create_user
# Run with verbose output
pytest tests/ -v
# Run and stop on first failure
pytest tests/ -x
# Generate API docs
sphinx-apidoc -f -o docs/source/api src/
# Build HTML documentation
sphinx-build -b html docs/source docs/build/html
# Serve documentation locally
cd docs/build/html && python -m http.server 8000
# Open in browser
open http://localhost:8000
# Build distribution packages
uv build
# Verify build
ls dist/
# Run all checks (CI simulation)
flake8 src/ tests/ && \
mypy src/ && \
pytest tests/ --cov=src --cov-report=term-missing --cov-fail-under=80 && \
uv build
Problem: error: Library stubs not installed for "requests"
Solution:
# Install type stubs
uv add --dev types-requests
# For other packages, search for types-<package>
uv add --dev types-PyYAML types-redis
Alternative: Add to pyproject.toml:
[tool.mypy]
ignore_missing_imports = true # Not recommended, use as last resort
Problem: flake8 complains about formatting that Black enforces
Solution: Configure .flake8 to ignore conflicting rules:
[flake8]
max-line-length = 100
ignore = E203, E266, W503
extend-ignore = E203, W503
Problem: WARNING: autodoc: failed to import module 'mymodule'
Solution: Ensure correct Python path in conf.py:
import os
import sys
sys.path.insert(0, os.path.abspath('../../src'))
Alternative: Install package in editable mode:
uv pip install -e .
Problem: ModuleNotFoundError: No module named 'src'
Solution 1: Add src to Python path in conftest.py:
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
Solution 2: Install package in editable mode:
uv pip install -e .
Problem: Coverage report shows lower coverage than expected
Solution: Configure source paths in pyproject.toml:
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/__pycache__/*"]
Problem: Merge conflicts in uv.lock
Solution:
# Accept one side of the conflict
git checkout --theirs uv.lock # or --ours
# Regenerate lock file
uv lock
# Commit regenerated file
git add uv.lock
git commit -m "Regenerate uv.lock after merge"
Problem: CI pipeline fails on GitHub Actions
Debugging Steps:
uv sync --dev
uv run flake8 src/ tests/
uv run mypy src/
uv run pytest tests/
pyproject.tomlProblem: Sphinx builds with warnings about missing references
Solution: Enable intersphinx mapping in conf.py:
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'requests': ('https://requests.readthedocs.io/en/latest/', None),
}
This skill guide provides a complete, production-ready setup for Python backend development with:
✅ Modern Tooling: uv, flake8, mypy, pytest, Sphinx
✅ CI/CD Ready: Complete GitHub Actions workflows
✅ Type Safety: Strict mypy configuration with comprehensive examples
✅ Documentation: Sphinx with auto-generated API docs and GitHub Pages deployment
✅ Testing: pytest with >80% coverage target and parametrized tests
✅ Best Practices: PEP 8, PEP 257, PEP 484 compliance
Follow this guide to build maintainable, well-tested, and well-documented Python backend applications that align with industry standards and the hexagonal architecture principles of the db-explorer project.