Agent Skills: Python Development Guidelines

Python conventions, patterns, and workflows. Use when writing code, reviewing PRs, or setting up new Python projects.

UncategorizedID: alexismanuel/dotfiles/python-dev-guidelines

Install this agent skill to your local

pnpm dlx add-skill https://github.com/alexismanuel/dotfiles/tree/HEAD/.config/opencode/skills/python-dev-guidelines

Skill Files

Browse the full folder contents for python-dev-guidelines.

Download Skill

Loading file tree…

.config/opencode/skills/python-dev-guidelines/SKILL.md

Skill Metadata

Name
python-dev-guidelines
Description
Python conventions, patterns, and workflows. Use when writing code, reviewing PRs, or setting up new Python projects.

Python Development Guidelines

Philosophy

Strong preferences, pragmatic adaptation.

  • On new projects: Apply these patterns fully
  • On existing projects: Respect existing conventions, introduce patterns gradually

When to Use

  • Writing new Python code
  • Writing or reviewing tests
  • Setting up new Python projects
  • Debugging import, exception, or testing issues

Tooling

| Tool | Purpose | |------|---------| | uv | Package management, virtual environments | | ruff | Linting + formatting (replaces flake8, isort, black) | | mypy | Type checking (standard mode) | | alembic | Database migrations (if existing in project) | | pytest | Testing framework | | pytest-asyncio | Async test support |

Code Style

| Category | Standard | |----------|----------| | Line length | 119 characters | | Import order | isort-style (project first, then third-party) | | Type annotations | Full coverage ideal, gradual pragmatically | | Naming | PEP8 (ClassNames, snake_case_functions) | | Docstrings | Google-style on all public functions | | Complexity | Max 12, extract ruthlessly into small functions |

Import Order

# Project imports first
from app.api.v1.users.repository import UserRepository
from app.api.v1.users.exceptions import UserNotFoundError
from app.database import get_session

# Third-party imports
import structlog
from fastapi import HTTPException
from pydantic import BaseModel

Project Structure

Vertical slices mirroring API routes:

app/
├── api/
│   └── v1/
│       ├── users/
│       │   ├── __init__.py
│       │   ├── router.py        # API routes
│       │   ├── service.py       # Business logic
│       │   ├── repository.py    # Data access
│       │   ├── models.py        # SQLAlchemy/Pydantic models
│       │   └── exceptions.py    # Domain exceptions
│       └── orders/
│           └── ...              # Same structure
├── database.py                  # Cross-cutting: DB connection
├── config.py                    # Cross-cutting: Settings
├── logging.py                   # Cross-cutting: Logging setup
└── main.py

Error Handling

For new projects: Error as Value pattern - functions return (result, error) tuples. For existing projects: Respect existing try/except patterns.

Why Error as Value?

  • Explicit handling at every call site (Go-style discipline)
  • Type system forces callers to handle errors
  • No hidden control flow from exceptions

Domain Exceptions

Flat hierarchy in dedicated exceptions.py:

class UserNotFoundError(Exception):
    """Raised when user lookup returns no results."""
    pass

class ValidationError(Exception):
    """Raised when input validation fails."""
    pass

class DatabaseConnectionError(Exception):
    """Raised when database connection fails."""
    pass

Repository Layer

from app.api.v1.users.exceptions import DatabaseConnectionError, DatabaseQueryError

async def get_by_id(
    session: AsyncSession,
    user_id: str,
) -> tuple[User | None, DatabaseConnectionError | DatabaseQueryError | None]:
    try:
        result = await session.execute(select(User).where(User.id == user_id))
        return result.scalar_one_or_none(), None
    except OperationalError as e:
        return None, DatabaseConnectionError(f"Connection failed: {e}")
    except SQLAlchemyError as e:
        return None, DatabaseQueryError(f"Query failed: {e}")

Service Layer

async def get_user(
    session: AsyncSession,
    user_id: str,
) -> tuple[User | None, ServiceError | None]:
    user, error = await repository.get_by_id(session, user_id)
    
    match error:
        case DatabaseConnectionError() | DatabaseQueryError():
            return None, ServiceError(f"Failed to fetch user {user_id}: {error}")
        case None if user is None:
            return None, ServiceError(f"User {user_id} not found")
        case None:
            return user, None

Route Layer

@router.get("/users/{user_id}")
async def get_user(user_id: str, session: AsyncSession = Depends(get_session)):
    user, error = await service.get_user(session, user_id)
    
    match error:
        case ServiceError() if "not found" in str(error):
            raise HTTPException(status_code=404, detail=str(error))
        case ServiceError():
            raise HTTPException(status_code=500, detail=str(error))
        case None:
            return user

Testing

See python-testing-guidelines skill for full details.

Key principles:

  • New code ships with tests - When implementing new functionality (validators, methods, classes), always add corresponding tests in the same task. Don't wait to be asked.
  • Never use mocks - No Mock, MagicMock, AsyncMock, or patch()
  • Dummy classes only - Inherit from real classes, track state for assertions
  • Test structure mirrors source - app/api/v1/users/tests/api/v1/users/

Configuration

New projects: Pydantic Settings

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    database_url: str
    api_key: str
    debug: bool = False
    
    class Config:
        env_file = ".env"

settings = Settings()

Existing projects: Adapt to existing pattern.

Logging

| Context | Approach | |---------|----------| | Services, APIs | Structured logging (structlog) | | Scripts, tools | print() or basic logging |

Async

| Context | Approach | |---------|----------| | Services, APIs | async/await | | Scripts, CLI tools | Sync |

Dependency Injection

Prefer explicit injection:

# Good - explicit
async def get_user(user_id: str, repository: UserRepository):
    return await repository.get_by_id(user_id)

# Acceptable - module singleton for breaking circular deps
_session_factory = None

def get_session_factory():
    global _session_factory
    if _session_factory is None:
        _session_factory = create_session_factory()
    return _session_factory