dotmd

Python Project — Copilot Instructions

GitHub Copilot instructions for general Python projects with modern typing and testable design patterns.

By dotmd TeamCC0Published Feb 19, 2026View source ↗

Install path

Use this file for each supported tool in your project.

  • GitHub Copilot: Save as copilot-instructions.md in your project at .github/copilot-instructions.md.

Configuration

copilot-instructions.md

1# Python Project — Copilot Instructions
2
3Python 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.
4
5## Quick Reference
6
7| 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 |
18
19## Project Structure
20
21```
22├── src/
23│ └── myproject/ # Source package (src layout)
24│ ├── __init__.py
25│ ├── config.py # Settings / env parsing
26│ ├── errors.py # Custom exceptions
27│ ├── models/ # Domain models (dataclasses, Pydantic)
28│ ├── services/ # Business logic
29│ ├── repositories/ # Data access
30│ └── utils/ # Pure utility functions
31├── tests/
32│ ├── conftest.py # Shared fixtures
33│ ├── unit/
34│ └── integration/
35├── pyproject.toml
36├── README.md
37└── Makefile # or justfile
38```
39
40If the project uses a flat layout (`myproject/` at root instead of `src/myproject/`), follow that convention.
41
42## Type Annotations
43
44### Use Modern Syntax (Python 3.12+)
45
46```python
47# ✅ Modern — use built-in generics and union syntax
48def find_user(users: list[User], user_id: str) -> User | None:
49 return next((u for u in users if u.id == user_id), None)
50
51def process_items(items: list[str | int]) -> dict[str, int]:
52 return {str(item): len(str(item)) for item in items}
53
54# ✅ Use collections.abc for abstract types in function signatures
55from collections.abc import Sequence, Mapping, Callable, Iterator
56
57def transform(items: Sequence[str], fn: Callable[[str], str]) -> list[str]:
58 return [fn(item) for item in items]
59```
60
61### TypeVar and Protocols
62
63```python
64from typing import Protocol, TypeVar, runtime_checkable
65
66T = TypeVar("T")
67
68# ✅ Protocol for structural typing — no inheritance required
69@runtime_checkable
70class 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: ...
74
75# Any class with matching methods satisfies this — no base class needed
76class 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 ...
83
84# ✅ TypeVar for generic functions
85def 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```
90
91### Annotate Everything Public
92
93```python
94# ✅ Public functions — explicit return types, docstrings
95def calculate_discount(price: Decimal, tier: CustomerTier) -> Decimal:
96 """Apply tier-based discount to the given price.
97
98 Returns the discounted price (never negative).
99 """
100 rate = DISCOUNT_RATES[tier]
101 return max(price * (1 - rate), Decimal("0"))
102
103# ✅ Private helpers — type hints required, docstrings optional
104def _normalize_email(email: str) -> str:
105 return email.strip().lower()
106```
107
108## Data Modeling
109
110### Dataclasses for Domain Objects
111
112```python
113from dataclasses import dataclass, field
114from datetime import datetime
115from decimal import Decimal
116
117@dataclass(frozen=True, slots=True)
118class Money:
119 amount: Decimal
120 currency: str = "USD"
121
122 def __post_init__(self) -> None:
123 if self.amount < 0:
124 raise ValueError(f"Amount cannot be negative: {self.amount}")
125
126@dataclass(slots=True)
127class Order:
128 id: str
129 customer_id: str
130 items: list[OrderItem] = field(default_factory=list)
131 status: OrderStatus = OrderStatus.PENDING
132 created_at: datetime = field(default_factory=datetime.utcnow)
133
134 @property
135 def total(self) -> Money:
136 return Money(sum(item.subtotal.amount for item in self.items))
137
138 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```
143
144### Pydantic for External Data (API input/output, config)
145
146```python
147from pydantic import BaseModel, Field, field_validator
148
149class CreateOrderRequest(BaseModel):
150 model_config = {"strict": True}
151
152 customer_id: str = Field(min_length=1, max_length=50)
153 items: list[OrderItemRequest] = Field(min_length=1)
154 notes: str | None = None
155
156 @field_validator("items")
157 @classmethod
158 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 items
163```
164
165### Enums for Fixed Choices
166
167```python
168from enum import StrEnum
169
170class OrderStatus(StrEnum):
171 PENDING = "pending"
172 CONFIRMED = "confirmed"
173 SHIPPED = "shipped"
174 DELIVERED = "delivered"
175 CANCELLED = "cancelled"
176```
177
178## Error Handling
179
180### Define a Hierarchy — Never Raise Bare `Exception`
181
182```python
183# src/myproject/errors.py
184
185class AppError(Exception):
186 """Base error for all application-specific exceptions."""
187
188 def __init__(self, message: str, code: str = "INTERNAL_ERROR") -> None:
189 super().__init__(message)
190 self.code = code
191
192class 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 = resource
196 self.resource_id = resource_id
197
198class 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 {}
202
203class ConflictError(AppError):
204 def __init__(self, message: str) -> None:
205 super().__init__(message, code="CONFLICT")
206```
207
208### Catch Specific, Re-raise With Context
209
210```python
211# ✅ Catch specific exceptions, add context
212async 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 exc
217
218 if row is None:
219 raise NotFoundError("User", user_id)
220
221 return User(**row)
222
223# ❌ Never do this
224try:
225 result = do_something()
226except Exception:
227 pass # Swallowed error — silent data corruption
228```
229
230## Context Managers
231
232### Use for Resource Lifecycle
233
234```python
235from contextlib import asynccontextmanager, contextmanager
236from collections.abc import AsyncGenerator, Generator
237
238@asynccontextmanager
239async def database_transaction(pool: AsyncPool) -> AsyncGenerator[Connection, None]:
240 conn = await pool.acquire()
241 tx = await conn.begin()
242 try:
243 yield conn
244 await tx.commit()
245 except Exception:
246 await tx.rollback()
247 raise
248 finally:
249 await pool.release(conn)
250
251# Usage
252async 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```
257
258## Configuration
259
260### Use Pydantic Settings for Environment Config
261
262```python
263from pydantic_settings import BaseSettings
264from pydantic import Field
265
266class Settings(BaseSettings):
267 model_config = {"env_prefix": "APP_", "env_file": ".env"}
268
269 database_url: str
270 redis_url: str = "redis://localhost:6379"
271 debug: bool = False
272 log_level: str = "INFO"
273 api_key: str = Field(repr=False) # Excluded from repr/logs
274
275 @property
276 def is_production(self) -> bool:
277 return not self.debug
278
279# Load once at startup, inject into services
280settings = Settings()
281```
282
283## Testing
284
285### Fixtures and Factories
286
287```python
288# tests/conftest.py
289import pytest
290from myproject.models import User, Order
291
292@pytest.fixture
293def sample_user() -> User:
294 return User(id="usr_001", email="alice@example.com", name="Alice")
295
296@pytest.fixture
297def 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 _create
307```
308
309### Test Structure — Arrange / Act / Assert
310
311```python
312class TestOrderService:
313 async def test_create_order_success(
314 self, order_service: OrderService, sample_user: User
315 ) -> None:
316 # Arrange
317 request = CreateOrderRequest(
318 customer_id=sample_user.id,
319 items=[OrderItemRequest(product_id="prod_1", quantity=2)],
320 )
321
322 # Act
323 order = await order_service.create(request)
324
325 # Assert
326 assert order.customer_id == sample_user.id
327 assert len(order.items) == 1
328 assert order.status == OrderStatus.PENDING
329
330 async def test_create_order_empty_items_raises(
331 self, order_service: OrderService
332 ) -> 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```
338
339### Parametrize for Coverage
340
341```python
342@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) == expected
352```
353
354### Mock External Dependencies, Not Internal Logic
355
356```python
357from unittest.mock import AsyncMock
358
359async def test_send_notification(notification_service: NotificationService) -> None:
360 # ✅ Mock the external email client, not internal service methods
361 mock_client = AsyncMock()
362 service = NotificationService(email_client=mock_client)
363
364 await service.send_welcome("alice@example.com", "Alice")
365
366 mock_client.send.assert_called_once_with(
367 to="alice@example.com",
368 subject="Welcome, Alice!",
369 template="welcome",
370 )
371```
372
373## Common Patterns
374
375### Prefer Comprehensions and Standard Library Itertools
376
377```python
378from itertools import batched # Python 3.12+
379
380# ✅ Use comprehensions over map/filter when readable
381active_emails = [user.email for user in users if user.is_active]
382user_lookup: dict[str, User] = {user.id: user for user in users}
383
384# ✅ Process in batches
385async def bulk_insert(items: list[dict], batch_size: int = 100) -> int:
386 inserted = 0
387 for batch in batched(items, batch_size):
388 await db.insert_many(batch)
389 inserted += len(batch)
390 return inserted
391```
392
393## Code Generation Guidelines
394
3951. **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: