This document provides comprehensive guidance for AI coding assistants working on this Python project. It covers project structure, tooling, testing patterns, and development workflows.
Technology Stack:
- Language: Python 3.12+
- Package Manager: uv (fast Python package manager)
- Build System: Hatchling
- Testing: pytest
- Code Quality: Ruff (formatting & linting)
- Type Checking: basedpyright (optional)
.
├── src/
│ └── <package_name>/ # Main package source code
│ ├── __init__.py
│ ├── main.py # CLI entry point
│ └── *.py # Module files
├── tests/
│ ├── unit/ # Unit tests with mocks
│ │ └── test_*.py
│ └── integration/ # Integration tests (real APIs/services)
│ └── test_*.py
├── .python-version # Python version (e.g., 3.12)
├── pyproject.toml # Project metadata & dependencies
├── uv.lock # Locked dependencies (DO NOT edit manually)
├── Makefile # Development task automation
├── .gitignore # Git ignore patterns
└── README.md # Project documentation
# Install uv (if not already installed)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Clone repository and navigate to it
cd <project-directory>
# Sync dependencies (creates .venv and installs packages)
uv sync| Command | Purpose |
|---|---|
uv sync |
Install/sync all dependencies from lockfile to .venv |
uv add <package> |
Add a new dependency to pyproject.toml and install it |
uv add --dev <package> |
Add a development dependency |
uv remove <package> |
Remove a dependency |
uv run <command> |
Run a command in the project environment (auto-syncs) |
uv lock |
Update the lockfile without installing |
uv lock --upgrade-package <pkg> |
Upgrade a specific package |
uv python install 3.12 |
Install a specific Python version |
# Run a Python script (uv handles environment automatically)
uv run python -m <package_name>.main
# Run with arguments
uv run python -m <package_name>.main --arg value
# Run any command in the project environment
uv run <command>Note: uv run automatically ensures the environment is synced before execution. No need to manually activate the virtual environment.
The Makefile provides convenient shortcuts for common tasks:
test: test-unit test-integration
@echo "All tests completed successfully"
pre-commit: test-unit format lint
@echo "Pre-commit checks passed"
test-unit:
@echo "Running unit tests..."
uv run pytest -s tests/unit
test-integration:
@echo "Running integration tests..."
uv run pytest -s tests/integration
format:
@echo "Formatting code..."
uv run ruff format
lint:
@echo "Running linter..."
uv run ruff checkUsage:
make test # Run all tests
make test-unit # Unit tests only
make test-integration # Integration tests only
make format # Auto-format code
make lint # Check code quality
make pre-commit # Run before committing-
Unit Tests (
tests/unit/): Fast, isolated tests using mocks- Test individual functions/classes in isolation
- Use
unittest.mock.Mockfor external dependencies - Should run in milliseconds
-
Integration Tests (
tests/integration/): Test real interactions- Test with actual APIs, databases, or external services
- May require API keys or running services
- Slower but verify real-world behavior
- Don't mock anything in integration tests unless explicitly required by the user
import pytest
from unittest.mock import Mock
@pytest.fixture
def mock_api_client():
"""Create a mock API client."""
client = Mock()
client.fetch_data.return_value = {"status": "success"}
return client
def test_function_with_mock(mock_api_client):
result = my_function(mock_api_client)
assert result == expected_value
mock_api_client.fetch_data.assert_called_once()@pytest.mark.parametrize(
"input_value, expected_output",
[
(1, 2),
(5, 10),
(0, 0),
],
)
def test_function_with_params(input_value, expected_output):
assert my_function(input_value) == expected_outputdef test_invalid_input_raises():
with pytest.raises(ValueError):
my_function(invalid_input)def test_api_integration():
"""Test with real API (requires API key in environment)."""
client = create_api_client()
result = client.fetch_data()
assert isinstance(result, dict)
assert "status" in result# All tests
make test
# or
uv run pytest
# Unit tests only
make test-unit
# or
uv run pytest tests/unit
# Integration tests only
make test-integration
# or
uv run pytest tests/integration
# Run specific test file
uv run pytest tests/unit/test_specific.py
# Run with verbose output
uv run pytest -v
# Run with coverage
uv run pytest --cov=src/<package_name># Auto-format all code
make format
# or
uv run ruff format
# Check formatting without changes
uv run ruff format --check# Run linter
make lint
# or
uv run ruff check
# Auto-fix issues where possible
uv run ruff check --fixBefore committing code, run:
make pre-commitThis runs unit tests, formatting, and linting to ensure code quality.
[project]
name = "package-name"
version = "0.1.0"
description = "Package description"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"package1>=1.0.0",
"package2>=2.0.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/<package_name>"]
[tool.pytest.ini_options]
addopts = "-v --capture=no"
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]
ignore = []Runtime dependencies:
uv add requests
uv add "pandas>=2.0.0"Development dependencies:
uv add --dev pytest
uv add --dev ruffFrom requirements.txt:
uv add -r requirements.txtUse protocols for dependency injection and testing:
from typing import Protocol
class DataProvider(Protocol):
"""Protocol for data providers."""
def fetch_data(self, query: str) -> dict: ...
class MyService:
def __init__(self, provider: DataProvider):
self.provider = provider
def process(self, query: str):
data = self.provider.fetch_data(query)
return self._transform(data)from dataclasses import dataclass
@dataclass
class Result:
"""Result of an operation."""
success: bool
data: dict
error_message: str | None = None#!/usr/bin/env python3
"""Main CLI entry point."""
import argparse
import sys
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description="Description")
parser.add_argument("--option", type=str, help="Option help")
args = parser.parse_args()
try:
# Main logic
pass
except KeyboardInterrupt:
print("\n⚠️ Interrupted by user", file=sys.stderr)
sys.exit(130)
except Exception as e:
print(f"\n❌ Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()# src/package_name/
├── __init__.py # Package initialization
├── main.py # CLI entry point
├── core.py # Core business logic
├── models.py # Data models (dataclasses, protocols)
├── api.py # External API integrations
├── utils.py # Utility functions
└── config.py # Configuration managementUse .env file for sensitive configuration (add to .gitignore):
# .env
API_KEY=your-secret-key
DATABASE_URL=postgresql://localhost/dbLoad with python-dotenv:
from dotenv import load_dotenv
import os
load_dotenv()
api_key = os.getenv("API_KEY")Standard .gitignore for Python projects:
# Virtual environment
.venv
venv/
ENV/
# Python cache
__pycache__/
*.py[cod]
*$py.class
*.so
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDE
.vscode/
.idea/
*.swp
*.swo
# Environment variables
.env
.env.local
# Build artifacts
dist/
build/
*.egg-info/
# uv
.ruff_cache/- Add dependencies if needed:
uv add <package> - Implement feature in
src/<package_name>/ - Write unit tests in
tests/unit/test_<feature>.py - Write integration tests if needed in
tests/integration/ - Run tests:
make test - Format and lint:
make format && make lint
# Update a specific package
uv lock --upgrade-package requests
# Update all packages (careful!)
uv lock --upgrade
# Sync after updating
uv sync# Run with Python debugger
uv run python -m pdb -m <package_name>.main
# Run tests with output
uv run pytest -s -v
# Run specific test with debugging
uv run pytest tests/unit/test_file.py::test_function -sCRITICAL: Follow SOLID principles as closely as possible in all code:
-
Single Responsibility Principle (SRP): Each class/function should have one reason to change
# Good: Separate concerns class UserRepository: def save(self, user: User) -> None: ... class UserValidator: def validate(self, user: User) -> bool: ... # Bad: Multiple responsibilities class UserManager: def save(self, user: User) -> None: ... def validate(self, user: User) -> bool: ... def send_email(self, user: User) -> None: ...
-
Open/Closed Principle (OCP): Open for extension, closed for modification
# Good: Use protocols/abstract base classes class DataProcessor(Protocol): def process(self, data: dict) -> dict: ... class JSONProcessor: def process(self, data: dict) -> dict: ... class XMLProcessor: def process(self, data: dict) -> dict: ...
-
Liskov Substitution Principle (LSP): Subtypes must be substitutable for base types
# Ensure derived classes don't break base class contracts class Bird(Protocol): def move(self) -> None: ... class Sparrow: def move(self) -> None: # Implementation that flies pass
-
Interface Segregation Principle (ISP): Many specific interfaces over one general
# Good: Specific protocols class Readable(Protocol): def read(self) -> str: ... class Writable(Protocol): def write(self, data: str) -> None: ... # Bad: Fat interface class FileOperations(Protocol): def read(self) -> str: ... def write(self, data: str) -> None: ... def delete(self) -> None: ... def compress(self) -> None: ...
-
Dependency Inversion Principle (DIP): Depend on abstractions, not concretions
# Good: Depend on protocol class Service: def __init__(self, repository: DataRepository): self.repository = repository # Bad: Depend on concrete implementation class Service: def __init__(self): self.repository = PostgreSQLRepository()
MANDATORY: Keep all Python files under 150 lines of code (excluding comments and docstrings). If a file exceeds this limit:
- Refactor immediately - split into multiple focused modules
- Extract classes/functions into separate files
- Group related functionality into subpackages
- Use clear naming for new modules
Also, tests file are not subject to this limit.
Example refactoring:
# Before: large_module.py (200+ lines)
# After: Split into:
# - large_module/core.py
# - large_module/validators.py
# - large_module/processors.py
# - large_module/__init__.py (exports public API)Use type hints for better code clarity and IDE support:
from typing import Literal
def process_data(
data: list[dict],
mode: Literal["fast", "accurate"] = "fast"
) -> dict[str, int]:
"""Process data and return statistics."""
return {"count": len(data)}Be explicit with error handling:
try:
result = risky_operation()
except SpecificError as e:
logger.error(f"Operation failed: {e}")
raise
except Exception as e:
logger.error(f"Unexpected error: {e}")
raise RuntimeError("Operation failed") from eUse docstrings for modules, classes, and functions ALWAYS:
def calculate_score(
correct: int,
total: int,
weight: float = 1.0
) -> float:
"""
Calculate weighted score.
Args:
correct: Number of correct answers.
total: Total number of questions.
weight: Score weight multiplier.
Returns:
Weighted score between 0 and 1.
Raises:
ValueError: If total is zero or negative.
"""
if total <= 0:
raise ValueError("Total must be positive")
return (correct / total) * weight- Write tests first (TDD) when possible
- Mock external dependencies in unit tests
- Test edge cases and error conditions
- Keep tests fast - unit tests should run in milliseconds
- Integration tests should verify real-world scenarios
- Extract repeated code into functions/classes
- Use inheritance or composition to share behavior
- Create utility modules for common operations
- Don't add functionality until it's needed
- Avoid over-engineering solutions
- Keep implementations simple and focused
# Prefer composition
class EmailService:
def __init__(self, sender: MessageSender, formatter: MessageFormatter):
self.sender = sender
self.formatter = formatter
# Over deep inheritance hierarchies
class EmailService(BaseService, LoggingMixin, ValidationMixin):
passfrom dataclasses import dataclass
@dataclass(frozen=True) # Immutable
class Config:
api_key: str
timeout: int# Good: Clear and explicit
def process_user(user_id: int, send_email: bool = False) -> User:
user = get_user(user_id)
if send_email:
send_welcome_email(user)
return user
# Bad: Hidden side effects
def process_user(user_id: int) -> User:
user = get_user(user_id)
send_welcome_email(user) # Unexpected side effect
return userdef divide(a: int, b: int) -> float:
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b# Good: Automatic resource cleanup
with open("file.txt") as f:
data = f.read()
# For custom resources
from contextlib import contextmanager
@contextmanager
def database_connection():
conn = create_connection()
try:
yield conn
finally:
conn.close()# Good: Named constants
MAX_RETRIES = 3
TIMEOUT_SECONDS = 30
def fetch_data():
for attempt in range(MAX_RETRIES):
try:
return request_with_timeout(TIMEOUT_SECONDS)
except TimeoutError:
continue
# Bad: Magic numbers
def fetch_data():
for attempt in range(3):
try:
return request_with_timeout(30)
except TimeoutError:
continue# Good: Early returns
def process_order(order: Order) -> None:
if not order.is_valid():
raise ValueError("Invalid order")
if order.is_cancelled():
return
if order.is_completed():
return
# Main logic here
process_payment(order)
# Bad: Nested conditions
def process_order(order: Order) -> None:
if order.is_valid():
if not order.is_cancelled():
if not order.is_completed():
process_payment(order)Issue: uv sync fails with dependency conflicts
# Solution: Check pyproject.toml for conflicting versions
# Update specific package
uv lock --upgrade-package problematic-packageIssue: Tests fail with import errors
# Solution: Ensure package is installed in editable mode
uv sync
# Or explicitly
uv pip install -e .Issue: Ruff formatting conflicts
# Solution: Check .ruff.toml or [tool.ruff] in pyproject.toml
# Run format to auto-fix
uv run ruff format- uv Documentation: https://docs.astral.sh/uv/
- pytest Documentation: https://docs.pytest.org/
- Ruff Documentation: https://docs.astral.sh/ruff/
- Python Packaging Guide: https://packaging.python.org/
When working on this project:
- Always use
uv runfor executing Python commands - Run tests after making changes:
make test-unit - Format code before committing:
make format - Check linting:
make lint - Update tests when modifying functionality
- Use type hints for new functions
- Follow existing patterns in the codebase
- Add docstrings for public APIs
- Mock external dependencies in unit tests
- Keep integration tests separate from unit tests
When implementing new features:
- Add dependencies with
uv add <package> - Create corresponding test files
- Use protocols for interfaces
- Use dataclasses for data structures
- Follow the existing module organization
- Run
make pre-commitbefore finishing
- Unit tests pass:
make test-unit - Integration tests pass (if applicable):
make test-integration - Code formatted:
make format - Linting passes:
make lint - Type hints added for new functions
- Docstrings added for public APIs
- Edge cases tested
- Error handling tested