Python Idioms and Patterns
Python rewards explicitness and readability. Follow Zen of Python. If it reads like plain English, it's idiomatic.
Scope: Python coding idioms. Layout: @.gemini/skills/project-structure-python/SKILL.md. Tests: GEMINI.md § Testing Strategy. Logging: @.gemini/skills/logging-and-observability-principles/SKILL.md.
Type Hints — Non-Negotiable
Always annotate function signatures and public APIs. Use from __future__ import annotations.
from __future__ import annotations
from collections.abc import Sequence
def calculate_discount(items: Sequence[Item], coupon: Coupon) -> float: ...
X | None over Optional[X] (3.10+):
def find_user(user_id: str) -> User | None: ...
TypeAlias and TypeVar:
from typing import TypeVar, TypeAlias
T = TypeVar("T")
UserId: TypeAlias = str
Protocol for structural interfaces (duck-typing):
from typing import Protocol
class TaskStorage(Protocol):
def get_by_id(self, task_id: str) -> Task: ...
def save(self, task: Task) -> None: ...
TypedDict for structured boundary dicts:
from typing import TypedDict
class CreateTaskRequest(TypedDict):
title: str
priority: Literal["low", "medium", "high"]
Error Handling
General: GEMINI.md § Error Handling Principles. Python-specific below.
Specific exceptions over except Exception:
try:
task = storage.get_by_id(task_id)
except TaskNotFoundError:
raise HTTPException(status_code=404, detail="Task not found")
Domain exception hierarchies:
class FathError(Exception):
"""Base exception for all domain errors."""
class NotFoundError(FathError):
def __init__(self, resource: str, resource_id: str) -> None:
self.resource = resource
self.resource_id = resource_id
super().__init__(f"{resource} '{resource_id}' not found")
class ValidationError(FathError):
def __init__(self, field: str, message: str) -> None:
self.field = field
self.message = message
super().__init__(f"Validation failed on '{field}': {message}")
Never silence exceptions — if caught and not re-raised, log explicitly:
try:
notify_user(user_id)
except NotificationError:
logger.warning("notification_failed", user_id=user_id, exc_info=True)
contextlib.suppress only for truly expected, inconsequential exceptions:
with suppress(FileNotFoundError):
cache_path.unlink() # cleanup, not business logic
Dataclasses and Pydantic
dataclasses for internal domain models:
@dataclass(frozen=True)
class Task:
id: str
title: str
priority: str
tags: tuple[str, ...] = field(default_factory=tuple)
Pydantic BaseModel for boundary data (API, config):
from pydantic import BaseModel, Field
class CreateTaskRequest(BaseModel):
title: str = Field(min_length=1, max_length=200)
priority: Literal["low", "medium", "high"] = "medium"
due_date: datetime | None = None
model_config = ConfigDict(frozen=True)
Keep separate: models.py = dataclasses (domain), schemas.py = Pydantic (API boundary).
Interfaces and DI
Define Protocol where used:
# task/storage.py ← consumer feature
class TaskStorage(Protocol):
def get_by_id(self, task_id: str) -> Task: ...
def save(self, task: Task) -> None: ...
def delete(self, task_id: str) -> None: ...
Inject via __init__:
class TaskService:
def __init__(self, storage: TaskStorage) -> None:
self._storage = storage
Wire in entry point:
storage = PostgresTaskStorage(db=database)
service = TaskService(storage=storage)
router.include_router(build_task_router(service))
Async / Await
General: GEMINI.md § Concurrency and Threading Mandate. Python-specific below.
One async paradigm, stay consistent.
Never blocking I/O in async fn: use aiofiles or executor.
async with aiofiles.open(path) as f:
return await f.read()
asyncio.gather for concurrent ops:
user, tasks = await asyncio.gather(get_user(user_id), get_tasks(user_id))
asyncio.TaskGroup (3.11+) for structured concurrency:
async with asyncio.TaskGroup() as tg:
user_task = tg.create_task(get_user(user_id))
tasks_task = tg.create_task(get_tasks(user_id))
user = user_task.result()
tasks = tasks_task.result()
Naming — PEP 8, No Exceptions
| Construct |
Convention |
Example |
| Module/Package |
snake_case |
task_service.py |
| Class |
PascalCase |
TaskService |
| Function/Method |
snake_case |
get_by_id |
| Private |
_snake_case |
_validate_title |
| Constant |
UPPER_SNAKE_CASE |
MAX_TITLE_LENGTH = 200 |
| Type alias |
PascalCase |
UserId = str |
| Protocol |
PascalCase |
TaskStorage |
- No single-letter names outside comprehensions/math.
- Avoid
data, info, obj, result standalone — use domain concepts.
- Booleans read as yes/no:
is_active, has_permission, can_edit().
Idiomatic Patterns
- Context managers for resource cleanup —
with over manual close().
- Generator expressions for lazy evaluation:
(task.id for task in tasks if task.is_active).
dataclasses.replace() for immutable updates: replace(task, title="New Title").
functools.cache/lru_cache for pure fn memoization.
__slots__ on hot-path, frequently instantiated classes.
enum.StrEnum (3.11+) for domain constants:class Priority(StrEnum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
Testing
Naming/pyramid: GEMINI.md § Testing Strategy. Python-specific below.
pytest only — never unittest.TestCase:
def test_calculate_discount_returns_zero_for_no_items() -> None:
result = calculate_discount(items=[], coupon=Coupon(code="SAVE10"))
assert result == 0.0
@pytest.mark.parametrize:
@pytest.mark.parametrize("priority,expected_score", [
("low", 1), ("medium", 5), ("high", 10),
])
def test_priority_score(priority: str, expected_score: int) -> None:
assert priority_score(priority) == expected_score
pytest-mock (mocker fixture):
def test_task_service_creates_task(mocker: MockerFixture) -> None:
mock_storage = mocker.create_autospec(TaskStorage, instance=True)
service = TaskService(storage=mock_storage)
service.create(title="Test", priority="high")
mock_storage.save.assert_called_once()
In-memory test adapter:
class InMemoryTaskStorage:
def __init__(self) -> None:
self._store: dict[str, Task] = {}
def get_by_id(self, task_id: str) -> Task:
if task_id not in self._store:
raise NotFoundError("Task", task_id)
return self._store[task_id]
def save(self, task: Task) -> None:
self._store[task.id] = task
def delete(self, task_id: str) -> None:
self._store.pop(task_id, None)
pytest-asyncio:
@pytest.mark.asyncio
async def test_async_create_task() -> None:
service = TaskService(storage=InMemoryTaskStorage())
task = await service.create(title="Async Task", priority="low")
assert task.title == "Async Task"
Fixtures for reusable setup — no repeated Arrange blocks.
Formatting and Static Analysis
Must pass zero warnings/errors. See GEMINI.md § Code Completion Mandate.
| Tool |
Purpose |
Command |
ruff format |
Formatting |
ruff format . |
ruff check |
Lint |
ruff check . --fix |
mypy |
Type checking |
mypy src/ --strict |
bandit |
Security scan |
bandit -r src/ -c pyproject.toml |
pip-audit |
CVE scanning |
pip-audit |
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "S", "B", "ANN"]
ignore = []
[tool.mypy]
strict = true
python_version = "3.11"
[tool.pytest.ini_options]
asyncio_mode = "auto"
Never print() in production. Use logging/structlog for structured JSON. See @.gemini/skills/logging-and-observability-principles/SKILL.md.
Related
- Code Idioms and Conventions GEMINI.md § Code Idioms and Conventions
- Project Structure — Python Backend @.gemini/skills/project-structure-python/SKILL.md
- Testing Strategy GEMINI.md § Testing Strategy
- Error Handling Principles GEMINI.md § Error Handling Principles
- Concurrency and Threading Mandate GEMINI.md § Concurrency and Threading Mandate
- Logging and Observability Principles @.gemini/skills/logging-and-observability-principles/SKILL.md
- Security Principles GEMINI.md § Security Principles
- Dependency Management Principles @.gemini/skills/dependency-management-principles/SKILL.md
1---2name: python-idioms3description: Python Idioms and Patterns4---56## Python Idioms and Patterns78Python rewards explicitness and readability. Follow Zen of Python. If it reads like plain English, it's idiomatic.910> Scope: Python coding idioms. Layout: `@.gemini/skills/project-structure-python/SKILL.md`. Tests: GEMINI.md § Testing Strategy. Logging: `@.gemini/skills/logging-and-observability-principles/SKILL.md`.1112### Type Hints — Non-Negotiable1314Always annotate function signatures and public APIs. Use `from __future__ import annotations`.1516```python17from __future__ import annotations18from collections.abc import Sequence1920def calculate_discount(items: Sequence[Item], coupon: Coupon) -> float: ...21```22231. **`X | None` over `Optional[X]`** (3.10+):24 ```python25 def find_user(user_id: str) -> User | None: ...26 ```27282. **`TypeAlias` and `TypeVar`:**29 ```python30 from typing import TypeVar, TypeAlias31 T = TypeVar("T")32 UserId: TypeAlias = str33 ```34353. **`Protocol` for structural interfaces** (duck-typing):36 ```python37 from typing import Protocol3839 class TaskStorage(Protocol):40 def get_by_id(self, task_id: str) -> Task: ...41 def save(self, task: Task) -> None: ...42 ```43444. **`TypedDict` for structured boundary dicts:**45 ```python46 from typing import TypedDict4748 class CreateTaskRequest(TypedDict):49 title: str50 priority: Literal["low", "medium", "high"]51 ```5253### Error Handling5455> General: GEMINI.md § Error Handling Principles. Python-specific below.56571. **Specific exceptions over `except Exception`:**58 ```python59 try:60 task = storage.get_by_id(task_id)61 except TaskNotFoundError:62 raise HTTPException(status_code=404, detail="Task not found")63 ```64652. **Domain exception hierarchies:**66 ```python67 class FathError(Exception):68 """Base exception for all domain errors."""6970 class NotFoundError(FathError):71 def __init__(self, resource: str, resource_id: str) -> None:72 self.resource = resource73 self.resource_id = resource_id74 super().__init__(f"{resource} '{resource_id}' not found")7576 class ValidationError(FathError):77 def __init__(self, field: str, message: str) -> None:78 self.field = field79 self.message = message80 super().__init__(f"Validation failed on '{field}': {message}")81 ```82833. **Never silence exceptions** — if caught and not re-raised, log explicitly:84 ```python85 try:86 notify_user(user_id)87 except NotificationError:88 logger.warning("notification_failed", user_id=user_id, exc_info=True)89 ```90914. **`contextlib.suppress`** only for truly expected, inconsequential exceptions:92 ```python93 with suppress(FileNotFoundError):94 cache_path.unlink() # cleanup, not business logic95 ```9697### Dataclasses and Pydantic98991. **`dataclasses` for internal domain models:**100 ```python101 @dataclass(frozen=True)102 class Task:103 id: str104 title: str105 priority: str106 tags: tuple[str, ...] = field(default_factory=tuple)107 ```1081092. **Pydantic `BaseModel` for boundary data (API, config):**110 ```python111 from pydantic import BaseModel, Field112113 class CreateTaskRequest(BaseModel):114 title: str = Field(min_length=1, max_length=200)115 priority: Literal["low", "medium", "high"] = "medium"116 due_date: datetime | None = None117 model_config = ConfigDict(frozen=True)118 ```1191203. **Keep separate:** `models.py` = dataclasses (domain), `schemas.py` = Pydantic (API boundary).121122### Interfaces and DI1231241. **Define Protocol where used:**125 ```python126 # task/storage.py ← consumer feature127 class TaskStorage(Protocol):128 def get_by_id(self, task_id: str) -> Task: ...129 def save(self, task: Task) -> None: ...130 def delete(self, task_id: str) -> None: ...131 ```1321332. **Inject via `__init__`:**134 ```python135 class TaskService:136 def __init__(self, storage: TaskStorage) -> None:137 self._storage = storage138 ```1391403. **Wire in entry point:**141 ```python142 storage = PostgresTaskStorage(db=database)143 service = TaskService(storage=storage)144 router.include_router(build_task_router(service))145 ```146147### Async / Await148149> General: GEMINI.md § Concurrency and Threading Mandate. Python-specific below.1501511. **One async paradigm, stay consistent.**1522. **Never blocking I/O in async fn:** use `aiofiles` or executor.153 ```python154 async with aiofiles.open(path) as f:155 return await f.read()156 ```1571583. **`asyncio.gather` for concurrent ops:**159 ```python160 user, tasks = await asyncio.gather(get_user(user_id), get_tasks(user_id))161 ```1621634. **`asyncio.TaskGroup` (3.11+) for structured concurrency:**164 ```python165 async with asyncio.TaskGroup() as tg:166 user_task = tg.create_task(get_user(user_id))167 tasks_task = tg.create_task(get_tasks(user_id))168 user = user_task.result()169 tasks = tasks_task.result()170 ```171172### Naming — PEP 8, No Exceptions173174| Construct | Convention | Example |175|---|---|---|176| Module/Package | `snake_case` | `task_service.py` |177| Class | `PascalCase` | `TaskService` |178| Function/Method | `snake_case` | `get_by_id` |179| Private | `_snake_case` | `_validate_title` |180| Constant | `UPPER_SNAKE_CASE` | `MAX_TITLE_LENGTH = 200` |181| Type alias | `PascalCase` | `UserId = str` |182| Protocol | `PascalCase` | `TaskStorage` |183184- No single-letter names outside comprehensions/math.185- Avoid `data`, `info`, `obj`, `result` standalone — use domain concepts.186- Booleans read as yes/no: `is_active`, `has_permission`, `can_edit()`.187188### Idiomatic Patterns1891901. **Context managers** for resource cleanup — `with` over manual `close()`.1912. **Generator expressions** for lazy evaluation: `(task.id for task in tasks if task.is_active)`.1923. **`dataclasses.replace()`** for immutable updates: `replace(task, title="New Title")`.1934. **`functools.cache`/`lru_cache`** for pure fn memoization.1945. **`__slots__`** on hot-path, frequently instantiated classes.1956. **`enum.StrEnum`** (3.11+) for domain constants:196 ```python197 class Priority(StrEnum):198 LOW = "low"199 MEDIUM = "medium"200 HIGH = "high"201 ```202203### Testing204205> Naming/pyramid: GEMINI.md § Testing Strategy. Python-specific below.2062071. **`pytest` only** — never `unittest.TestCase`:208 ```python209 def test_calculate_discount_returns_zero_for_no_items() -> None:210 result = calculate_discount(items=[], coupon=Coupon(code="SAVE10"))211 assert result == 0.0212 ```2132142. **`@pytest.mark.parametrize`:**215 ```python216 @pytest.mark.parametrize("priority,expected_score", [217 ("low", 1), ("medium", 5), ("high", 10),218 ])219 def test_priority_score(priority: str, expected_score: int) -> None:220 assert priority_score(priority) == expected_score221 ```2222233. **`pytest-mock` (`mocker` fixture):**224 ```python225 def test_task_service_creates_task(mocker: MockerFixture) -> None:226 mock_storage = mocker.create_autospec(TaskStorage, instance=True)227 service = TaskService(storage=mock_storage)228 service.create(title="Test", priority="high")229 mock_storage.save.assert_called_once()230 ```2312324. **In-memory test adapter:**233 ```python234 class InMemoryTaskStorage:235 def __init__(self) -> None:236 self._store: dict[str, Task] = {}237238 def get_by_id(self, task_id: str) -> Task:239 if task_id not in self._store:240 raise NotFoundError("Task", task_id)241 return self._store[task_id]242243 def save(self, task: Task) -> None:244 self._store[task.id] = task245246 def delete(self, task_id: str) -> None:247 self._store.pop(task_id, None)248 ```2492505. **`pytest-asyncio`:**251 ```python252 @pytest.mark.asyncio253 async def test_async_create_task() -> None:254 service = TaskService(storage=InMemoryTaskStorage())255 task = await service.create(title="Async Task", priority="low")256 assert task.title == "Async Task"257 ```2582596. **Fixtures for reusable setup** — no repeated Arrange blocks.260261### Formatting and Static Analysis262263Must pass zero warnings/errors. See GEMINI.md § Code Completion Mandate.264265| Tool | Purpose | Command |266|---|---|---|267| `ruff format` | Formatting | `ruff format .` |268| `ruff check` | Lint | `ruff check . --fix` |269| `mypy` | Type checking | `mypy src/ --strict` |270| `bandit` | Security scan | `bandit -r src/ -c pyproject.toml` |271| `pip-audit` | CVE scanning | `pip-audit` |272273```toml274[tool.ruff]275line-length = 100276target-version = "py311"277278[tool.ruff.lint]279select = ["E", "F", "I", "N", "UP", "S", "B", "ANN"]280ignore = []281282[tool.mypy]283strict = true284python_version = "3.11"285286[tool.pytest.ini_options]287asyncio_mode = "auto"288```289290> Never `print()` in production. Use `logging`/`structlog` for structured JSON. See `@.gemini/skills/logging-and-observability-principles/SKILL.md`.291292### Related293- Code Idioms and Conventions GEMINI.md § Code Idioms and Conventions294- Project Structure — Python Backend @.gemini/skills/project-structure-python/SKILL.md295- Testing Strategy GEMINI.md § Testing Strategy296- Error Handling Principles GEMINI.md § Error Handling Principles297- Concurrency and Threading Mandate GEMINI.md § Concurrency and Threading Mandate298- Logging and Observability Principles @.gemini/skills/logging-and-observability-principles/SKILL.md299- Security Principles GEMINI.md § Security Principles300- Dependency Management Principles @.gemini/skills/dependency-management-principles/SKILL.md