Python Project — Copilot Instructions
GitHub Copilot instructions for general Python projects with modern typing and testable design patterns.
Install path
Use this file for each supported tool in your project.
- GitHub Copilot: Save as
copilot-instructions.mdin your project at.github/copilot-instructions.md.
Configuration
copilot-instructions.md
1# Python Project — Copilot Instructions23Python 3.12+ project with full type annotations. Prioritize readability, explicit error handling, and tested code. Every function has type hints. Every public module has docstrings.45## Quick Reference67| Area | Convention |8|---|---|9| Language | Python 3.12+ |10| Type checking | `mypy --strict` or `pyright` (check `pyproject.toml`) |11| Linting | Ruff (`ruff check .`) — replaces flake8, isort, etc. |12| Formatting | Ruff format (`ruff format .`) or Black |13| Testing | pytest with `pytest-cov` |14| Dependency management | Check for `uv.lock` → uv, `poetry.lock` → Poetry, `requirements.txt` → pip |15| Config | `pyproject.toml` for all tool config (ruff, mypy, pytest) |16| Virtual env | `.venv/` in project root |17| Import sorting | Ruff handles this — stdlib → third-party → local, one blank line between groups |1819## Project Structure2021```22├── src/23│ └── myproject/ # Source package (src layout)24│ ├── __init__.py25│ ├── config.py # Settings / env parsing26│ ├── errors.py # Custom exceptions27│ ├── models/ # Domain models (dataclasses, Pydantic)28│ ├── services/ # Business logic29│ ├── repositories/ # Data access30│ └── utils/ # Pure utility functions31├── tests/32│ ├── conftest.py # Shared fixtures33│ ├── unit/34│ └── integration/35├── pyproject.toml36├── README.md37└── Makefile # or justfile38```3940If the project uses a flat layout (`myproject/` at root instead of `src/myproject/`), follow that convention.4142## Type Annotations4344### Use Modern Syntax (Python 3.12+)4546```python47# ✅ Modern — use built-in generics and union syntax48def find_user(users: list[User], user_id: str) -> User | None:49 return next((u for u in users if u.id == user_id), None)5051def process_items(items: list[str | int]) -> dict[str, int]:52 return {str(item): len(str(item)) for item in items}5354# ✅ Use collections.abc for abstract types in function signatures55from collections.abc import Sequence, Mapping, Callable, Iterator5657def transform(items: Sequence[str], fn: Callable[[str], str]) -> list[str]:58 return [fn(item) for item in items]59```6061### TypeVar and Protocols6263```python64from typing import Protocol, TypeVar, runtime_checkable6566T = TypeVar("T")6768# ✅ Protocol for structural typing — no inheritance required69@runtime_checkable70class Repository(Protocol[T]):71 def get(self, id: str) -> T | None: ...72 def save(self, entity: T) -> T: ...73 def delete(self, id: str) -> bool: ...7475# Any class with matching methods satisfies this — no base class needed76class UserRepository:77 def get(self, id: str) -> User | None:78 ...79 def save(self, entity: User) -> User:80 ...81 def delete(self, id: str) -> bool:82 ...8384# ✅ TypeVar for generic functions85def first_or_raise(items: Sequence[T], error_msg: str = "Empty sequence") -> T:86 if not items:87 raise ValueError(error_msg)88 return items[0]89```9091### Annotate Everything Public9293```python94# ✅ Public functions — explicit return types, docstrings95def calculate_discount(price: Decimal, tier: CustomerTier) -> Decimal:96 """Apply tier-based discount to the given price.9798 Returns the discounted price (never negative).99 """100 rate = DISCOUNT_RATES[tier]101 return max(price * (1 - rate), Decimal("0"))102103# ✅ Private helpers — type hints required, docstrings optional104def _normalize_email(email: str) -> str:105 return email.strip().lower()106```107108## Data Modeling109110### Dataclasses for Domain Objects111112```python113from dataclasses import dataclass, field114from datetime import datetime115from decimal import Decimal116117@dataclass(frozen=True, slots=True)118class Money:119 amount: Decimal120 currency: str = "USD"121122 def __post_init__(self) -> None:123 if self.amount < 0:124 raise ValueError(f"Amount cannot be negative: {self.amount}")125126@dataclass(slots=True)127class Order:128 id: str129 customer_id: str130 items: list[OrderItem] = field(default_factory=list)131 status: OrderStatus = OrderStatus.PENDING132 created_at: datetime = field(default_factory=datetime.utcnow)133134 @property135 def total(self) -> Money:136 return Money(sum(item.subtotal.amount for item in self.items))137138 def add_item(self, item: OrderItem) -> None:139 if self.status != OrderStatus.PENDING:140 raise InvalidOperationError(f"Cannot modify order in {self.status} state")141 self.items.append(item)142```143144### Pydantic for External Data (API input/output, config)145146```python147from pydantic import BaseModel, Field, field_validator148149class CreateOrderRequest(BaseModel):150 model_config = {"strict": True}151152 customer_id: str = Field(min_length=1, max_length=50)153 items: list[OrderItemRequest] = Field(min_length=1)154 notes: str | None = None155156 @field_validator("items")157 @classmethod158 def validate_unique_products(cls, items: list[OrderItemRequest]) -> list[OrderItemRequest]:159 product_ids = [item.product_id for item in items]160 if len(product_ids) != len(set(product_ids)):161 raise ValueError("Duplicate product IDs")162 return items163```164165### Enums for Fixed Choices166167```python168from enum import StrEnum169170class OrderStatus(StrEnum):171 PENDING = "pending"172 CONFIRMED = "confirmed"173 SHIPPED = "shipped"174 DELIVERED = "delivered"175 CANCELLED = "cancelled"176```177178## Error Handling179180### Define a Hierarchy — Never Raise Bare `Exception`181182```python183# src/myproject/errors.py184185class AppError(Exception):186 """Base error for all application-specific exceptions."""187188 def __init__(self, message: str, code: str = "INTERNAL_ERROR") -> None:189 super().__init__(message)190 self.code = code191192class NotFoundError(AppError):193 def __init__(self, resource: str, resource_id: str) -> None:194 super().__init__(f"{resource} '{resource_id}' not found", code="NOT_FOUND")195 self.resource = resource196 self.resource_id = resource_id197198class ValidationError(AppError):199 def __init__(self, message: str, fields: dict[str, str] | None = None) -> None:200 super().__init__(message, code="VALIDATION_ERROR")201 self.fields = fields or {}202203class ConflictError(AppError):204 def __init__(self, message: str) -> None:205 super().__init__(message, code="CONFLICT")206```207208### Catch Specific, Re-raise With Context209210```python211# ✅ Catch specific exceptions, add context212async def get_user(self, user_id: str) -> User:213 try:214 row = await self.db.fetchone("SELECT * FROM users WHERE id = $1", user_id)215 except ConnectionError as exc:216 raise AppError("Database unavailable") from exc217218 if row is None:219 raise NotFoundError("User", user_id)220221 return User(**row)222223# ❌ Never do this224try:225 result = do_something()226except Exception:227 pass # Swallowed error — silent data corruption228```229230## Context Managers231232### Use for Resource Lifecycle233234```python235from contextlib import asynccontextmanager, contextmanager236from collections.abc import AsyncGenerator, Generator237238@asynccontextmanager239async def database_transaction(pool: AsyncPool) -> AsyncGenerator[Connection, None]:240 conn = await pool.acquire()241 tx = await conn.begin()242 try:243 yield conn244 await tx.commit()245 except Exception:246 await tx.rollback()247 raise248 finally:249 await pool.release(conn)250251# Usage252async def transfer_funds(pool: AsyncPool, from_id: str, to_id: str, amount: Decimal) -> None:253 async with database_transaction(pool) as conn:254 await debit_account(conn, from_id, amount)255 await credit_account(conn, to_id, amount)256```257258## Configuration259260### Use Pydantic Settings for Environment Config261262```python263from pydantic_settings import BaseSettings264from pydantic import Field265266class Settings(BaseSettings):267 model_config = {"env_prefix": "APP_", "env_file": ".env"}268269 database_url: str270 redis_url: str = "redis://localhost:6379"271 debug: bool = False272 log_level: str = "INFO"273 api_key: str = Field(repr=False) # Excluded from repr/logs274275 @property276 def is_production(self) -> bool:277 return not self.debug278279# Load once at startup, inject into services280settings = Settings()281```282283## Testing284285### Fixtures and Factories286287```python288# tests/conftest.py289import pytest290from myproject.models import User, Order291292@pytest.fixture293def sample_user() -> User:294 return User(id="usr_001", email="alice@example.com", name="Alice")295296@pytest.fixture297def order_factory() -> Callable[..., Order]:298 def _create(**overrides: Any) -> Order:299 defaults = {300 "id": "ord_001",301 "customer_id": "usr_001",302 "items": [],303 "status": OrderStatus.PENDING,304 }305 return Order(**(defaults | overrides))306 return _create307```308309### Test Structure — Arrange / Act / Assert310311```python312class TestOrderService:313 async def test_create_order_success(314 self, order_service: OrderService, sample_user: User315 ) -> None:316 # Arrange317 request = CreateOrderRequest(318 customer_id=sample_user.id,319 items=[OrderItemRequest(product_id="prod_1", quantity=2)],320 )321322 # Act323 order = await order_service.create(request)324325 # Assert326 assert order.customer_id == sample_user.id327 assert len(order.items) == 1328 assert order.status == OrderStatus.PENDING329330 async def test_create_order_empty_items_raises(331 self, order_service: OrderService332 ) -> None:333 with pytest.raises(ValidationError, match="at least one item"):334 await order_service.create(335 CreateOrderRequest(customer_id="usr_001", items=[])336 )337```338339### Parametrize for Coverage340341```python342@pytest.mark.parametrize(343 "input_email, expected",344 [345 ("Alice@Example.COM", "alice@example.com"),346 (" bob@test.com ", "bob@test.com"),347 ("UPPER@DOMAIN.ORG", "upper@domain.org"),348 ],349)350def test_normalize_email(input_email: str, expected: str) -> None:351 assert normalize_email(input_email) == expected352```353354### Mock External Dependencies, Not Internal Logic355356```python357from unittest.mock import AsyncMock358359async def test_send_notification(notification_service: NotificationService) -> None:360 # ✅ Mock the external email client, not internal service methods361 mock_client = AsyncMock()362 service = NotificationService(email_client=mock_client)363364 await service.send_welcome("alice@example.com", "Alice")365366 mock_client.send.assert_called_once_with(367 to="alice@example.com",368 subject="Welcome, Alice!",369 template="welcome",370 )371```372373## Common Patterns374375### Prefer Comprehensions and Standard Library Itertools376377```python378from itertools import batched # Python 3.12+379380# ✅ Use comprehensions over map/filter when readable381active_emails = [user.email for user in users if user.is_active]382user_lookup: dict[str, User] = {user.id: user for user in users}383384# ✅ Process in batches385async def bulk_insert(items: list[dict], batch_size: int = 100) -> int:386 inserted = 0387 for batch in batched(items, batch_size):388 await db.insert_many(batch)389 inserted += len(batch)390 return inserted391```392393## Code Generation Guidelines3943951. **Follow existing project patterns.** If the codebase uses `attrs` instead of `dataclasses`, use `attrs`. If it uses `httpx` instead of `requests`, match that.3962. **Use the standard library first.** `pathlib` over `os.path`. `tomllib` for TOML (3.11+). `datetime.timezone.utc` over `pytz`.3973. **Don't add dependencies without stating why.** Suggest the package and the reason.3984. **Docstrings on public APIs.** Use Google style (`Args:`, `Returns:`, `Raises:`) — match existing style if present.3995. **One class/concept per module.** `user_service.py` contains `UserService` and its closely related helpers. Not three unrelated services.4006. **Prefer explicit over clever.** A `for` loop is fine when a nested comprehension would be unreadable. Named variables beat chained expressions.401
Community feedback
0 found this helpful
Works with: