| name | python-idioms |
| description | Python type hints, Protocols, Pydantic, async/await, pytest, ruff, mypy strict. |
| paths | ["**/*.py","**/pyproject.toml","**/requirements*.txt"] |
Python Idioms and Patterns
Core Philosophy
Python rewards explicitness and readability over cleverness. Follow the Zen of Python. If it reads like plain English, it's probably idiomatic.
Scope: This skill covers Python-specific coding idioms. For file layout see references/project-structure.md. For safety/SAST/performance patterns see references/python-patterns-and-anti-patterns.md. For logging see logging-implementation skill. For quality commands see code-idioms-and-conventions rule.
Loading Guards
- If no
pyproject.toml or *.py files, this skill does not apply
- If Django project (
django in dependencies), co-load django-idioms skill alongside this one
When to Load References
| Situation | Reference to Load |
|---|
| Starting a new project or setting up file layout | references/project-structure.md |
Choosing packages, pyproject.toml setup, or ruff config | references/recommended-dependencies.md |
| Writing code that handles user input, async operations, or I/O | references/python-patterns-and-anti-patterns.md |
Toolchain and Python Version
- Default to latest stable Python. As of August 2026, Python 3.14. Minimum target: 3.13+.
- Key version milestones:
- 3.14+ — Deferred evaluation of annotations (PEP 649, no more
from __future__ import annotations), template strings (PEP 750)
- 3.13+ — Improved error messages, experimental free-threaded build, experimental JIT
- 3.12+ — Type parameter syntax
type X = ... (PEP 695), @override decorator, improved f-strings, itertools.batched
- 3.11+ —
StrEnum, ExceptionGroup + except*, asyncio.TaskGroup, tomllib, fine-grained error locations
- 3.10+ — Pattern matching (
match/case), X | Y union syntax, TypeAlias
Type Hints — Non-Negotiable
Type hints are required for all public APIs, class attributes, and function signatures.
- Use standard collections (
list, dict, set) for typing, not typing.List etc.
- Use
X | Y instead of Union[X, Y] or Optional[X].
- Use PEP 695 type parameter syntax (3.12+) for generic types:
type Vector[T] = list[T]
- Use
@override (3.12+) to ensure methods actually override a base class method.
- Use
TypeVar with constraints and bounds when necessary.
- Use
Never for functions that always raise an exception or never return.
from typing import List, Optional, TypeVar
T = TypeVar('T')
def process_items(items: List[T], strict: Optional[bool] = None) -> List[T]: ...
class Worker(BaseWorker):
def run(self): ...
from typing import override, Never
type Vector[T] = list[T]
def process_items[T](items: Vector[T], strict: bool | None = None) -> Vector[T]: ...
class Worker(BaseWorker):
@override
def run(self) -> None: ...
def crash_and_burn(msg: str) -> Never:
raise RuntimeError(msg)
Protocols for Structural Subtyping
Define required behavior via Protocol instead of inheritance when depending on abstractions.
TypedDict for JSON/Dict payloads
When dealing with dictionaries that have a fixed schema, use TypedDict.
Error Handling
- Raise specific exceptions, not generic
Exception.
- Build a domain-specific exception hierarchy (e.g.
AppError base class).
- Never explicitly silence errors without handling or logging (
except Exception: pass). Use contextlib.suppress() if appropriate and intentional.
- Use exception groups and
except* (3.11+) when multiple errors can occur simultaneously.
- Use
add_note() (3.11+) to attach additional context to exceptions before re-raising.
- Pattern: Never assign the result of functions that return
None (DeepSource bug risk).
- Pattern:
finally blocks should not swallow exceptions; they are for cleanup only.
def load_data():
try:
data = fetch()
return data
except Exception as e:
print(f"Failed: {e}")
finally:
return None
res = dict.get("key")
class AppError(Exception): pass
class NetworkError(AppError): pass
def load_data() -> dict:
try:
return fetch()
except TimeoutError as e:
e.add_note("Timeout while fetching external data")
raise NetworkError("Failed to fetch") from e
try:
raise ExceptionGroup("Multiple failures", [NetworkError(), ValueError()])
except* NetworkError as e:
handle_network(e)
except* ValueError as e:
handle_value(e)
Dataclasses and Pydantic
- Use
@dataclass for internal data structures.
- Use
@dataclass(frozen=True, slots=True) (3.10+) as the recommended default for value objects. slots=True avoids __dict__ creation, saving memory and speeding up attribute access.
- Use
@dataclass(kw_only=True) (3.10+) to require keyword arguments.
- Use Pydantic
BaseModel when data crosses system boundaries (I/O, APIs, config) and requires validation.
- Use Pydantic v2
model_validator and field_validator for complex validation rules.
from dataclasses import dataclass
@dataclass(frozen=True, slots=True, kw_only=True)
class UserConfig:
id: int
username: str
active: bool = True
from pydantic import BaseModel, field_validator, model_validator
class User(BaseModel):
password: str
password_confirm: str
@model_validator(mode="after")
def check_passwords_match(self) -> "User":
if self.password != self.password_confirm:
raise ValueError("Passwords do not match")
return self
When to use which:
| Use Case | Recommendation |
|---|
| Untrusted / external data (API input, config files, webhook payloads) | pydantic.BaseModel |
| Internal value objects, domain entities (no validation needed) | @dataclass(frozen=True, slots=True) |
| Dictionary-shaped typed data (JSON response shapes, kwargs mappings) | TypedDict |
| Named string or integer constants | enum.StrEnum / enum.IntEnum |
Interfaces and Dependency Injection
Prefer composition and dependency injection over deep inheritance hierarchies. Depend on typing.Protocol to define the interface a function or class expects.
from typing import Protocol
class MessageSender(Protocol):
def send(self, msg: str) -> None: ...
class EmailSender:
def send(self, msg: str) -> None:
pass
def notify_user(sender: MessageSender) -> None:
sender.send("Hello")
Async / Await
- Use
asyncio.TaskGroup (3.11+) as the preferred way to run concurrent tasks over asyncio.gather. It provides structured concurrency and better error handling.
- Use
asyncio.Runner (3.11+) for managing the event loop lifecycle instead of raw get_event_loop().
- Never call
asyncio.run() from inside an already running event loop.
- Use
asyncio.to_thread() to offload blocking/CPU-bound work to a thread pool so the event loop is not blocked.
import asyncio
async def main():
await asyncio.gather(task1(), task2())
import asyncio
async def main():
try:
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(fetch_data())
task2 = tg.create_task(process_data())
except* Exception as e:
print(f"Task group failed: {e}")
Naming Conventions
| Entity | Convention | Example |
|---|
| Variables, Functions, Methods | snake_case | calculate_total() |
| Classes, Protocols, TypeAliases | PascalCase | UserRepository |
| Constants | UPPER_SNAKE_CASE | MAX_RETRIES |
| Protected/Private members | _leading_underscore | _internal_cache |
| Dunder methods | __dunder__ | __init__ |
- Be descriptive.
fetch_user_by_id(user_id: int) is better than get_u(i).
Idiomatic Patterns
- Context Managers: Use
with statements for resource management (files, network connections, locks).
- Generators: Use
yield for lazy evaluation and memory efficiency when dealing with large sequences.
dataclasses.replace: Use for immutable updates to dataclasses.
functools.cache / lru_cache: Use for memoizing expensive deterministic function calls.
__slots__: Use via @dataclass(slots=True) or explicitly to save memory on heavily instantiated classes.
StrEnum: (3.11+) Use for string-based enumerations.
- Pattern Matching (3.10+): Use
match/case for structural pattern matching instead of long if/elif/else chains.
- String Affixes (3.9+): Use
str.removeprefix() and str.removesuffix() instead of error-prone slicing or strip().
- Dict Merge Operator (3.9+): Use
dict1 | dict2 to merge dictionaries.
- Walrus Operator
:=: Use for assignment expressions to avoid repeating expensive calls or improving loop conditions.
itertools.batched (3.12+): Use to cleanly chunk iterables into batches.
pathlib.Path: ALWAYS prefer over os.path for file operations.
- Mutable Defaults: NEVER use mutable default arguments (
[], {}). Use None as a sentinel. (DeepSource #1 bug risk)
def add_item(item: str, items: list = []) -> list:
items.append(item)
return items
def add_item(item: str, items: list | None = None) -> list:
if items is None:
items = []
items.append(item)
return items
import itertools
def process(command: dict | list):
match command:
case {"action": "delete", "id": int(id_val)}:
delete_record(id_val)
case list(items):
for batch in itertools.batched(items, 100):
process_batch(batch)
Testing
Write deterministic tests focusing on behavior.
- Test coverage non-negotiable policy (same as Rust/TS).
- Coverage commands:
pytest --cov=src --cov-report=term-missing
- Prefer
@pytest.mark.parametrize for data-driven testing.
- Use
pytest-asyncio for async tests.
- Use typed mock factories or fixtures instead of
patch decorators when possible.
Test Double Selection Table:
| Approach | When to Use |
|---|
| Hand-written fake (implement Protocol) | Simple interface, few methods, need stateful behavior |
pytest-mock (mocker fixture) | Verify call counts, argument matching |
respx | HTTP boundary mocking — intercepts httpx calls |
@pytest.mark.parametrize | Same logic, multiple input/output pairs |
Snapshot (syrupy) | Large outputs — JSON responses, CLI output |
hypothesis | Property-based testing for wide input spaces |
Lint Suppression Policy
NEVER suppress these — they signal structural problems:
| Rule | What It Signals | What To Do Instead |
|---|
F841 (unused variable) | Dead code | Remove the variable |
S rules (security) | Security vulnerability | Fix the vulnerability |
B006 (mutable default) | Shared mutable state bug | Use None sentinel pattern |
ANN (missing annotations) | Untyped public API | Add type annotations |
E712 (== True/False/None) | Identity vs equality confusion | Use is / is not |
Acceptable suppressions (with mandatory # noqa: + reason comment):
| Rule | When Acceptable |
|---|
S101 (assert) | In test files only |
ANN101/ANN102 (self/cls annotations) | Standard convention — self/cls never need annotations |
T20 (print) | In CLI tools or scripts |
ARG (unused argument) | In interface implementations where signature is fixed |
Rule of thumb: If you're about to write # noqa:, stop and ask: "Am I suppressing a real design problem?"
Formatting and Static Analysis — Feedback Loop
Adopt the standard Rust/TS-style static analysis workflow:
| Phase | Command | Purpose |
|---|
| TDD / rapid iteration | mypy src/ --strict | Type-check only — fastest feedback |
| Pre-commit | ruff check . --fix | Lint — must pass with zero warnings |
| Pre-commit | ruff format . | Formatting — non-negotiable |
| Pre-commit | pytest | Unit tests — must all pass |
| Coverage verification | pytest --cov=src --cov-report=term-missing | Verify before merging |
| Security audit | bandit -r src/ -c pyproject.toml | Security scanning |
| Dependency audit | pip-audit | CVE scanning |
Configure all tools in pyproject.toml — never use per-file pragma comments to disable checks without a # noqa: reason comment.
Never use print() in production. Always use a configured logger (see logging-implementation skill).
Documentation
Document all public items:
- Every public function, class, method, and module MUST have a docstring.
- Use Google-style docstrings (recommended) or NumPy-style (for scientific code).
- At minimum: one-line summary. For complex items: summary + Args + Returns + Raises.
def calculate_discount(price: float, rate: float) -> float:
return price * (1 - rate)
def calculate_discount(price: float, rate: float) -> float:
"""Calculates the final price after applying a discount rate.
Args:
price: The original price.
rate: The discount rate as a decimal (e.g., 0.2 for 20%).
Returns:
The final discounted price.
Raises:
ValueError: If the rate is not between 0.0 and 1.0.
"""
if not (0.0 <= rate <= 1.0):
raise ValueError("Rate must be between 0.0 and 1.0")
return price * (1 - rate)
Dependency Management
- Minimize dependency count — each dependency is an attack surface.
- Audit regularly — run
pip-audit in CI.
- Use
pyproject.toml as the single source of truth for project metadata.
- Commit lockfiles for applications (
uv.lock, requirements.lock).
- Prefer stdlib over third-party when feature parity exists.
- Check for unused dependencies with import analysis.
For the full curated dependency list with versions, see references/recommended-dependencies.md.
Configuration and Environment
- Never scatter
os.environ / os.getenv() calls throughout the codebase.
- Use pydantic-settings
BaseSettings for validated, typed config.
- Fail fast on missing required config at boot, not at first use.
import os
def connect_db():
db_url = os.getenv("DATABASE_URL")
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
api_key: str
settings = Settings()
def connect_db():
db_url = settings.database_url
Safety, Security, and Performance
- Key safety rules (non-negotiable):
- Never use
eval() or exec() with untrusted input.
- Never use
pickle on untrusted data.
- Always parameterize SQL queries; never concatenate strings to build SQL.
- Always validate user input at system boundaries.
- See
references/python-patterns-and-anti-patterns.md for the full catalog of safety and security patterns.
- See
perf-optimization skill for profiling and performance guidance.
Related Principles
- Code Idioms and Conventions
@code-idioms-and-conventions.md
- Project Structure — Python Backend
@references/project-structure.md
- Security Principles
@security-principles.md
- Architectural Patterns — Testability-First Design
@architectural-pattern.md
- Testing Strategy
@testing-strategy.md
- Error Handling Principles
@error-handling-principles.md
- Core Design Principles § Concurrency
@core-design-principles.md
- Logging and Observability Mandate
@logging-and-observability-mandate.md
- Logging Implementation
@.agents/skills/logging-implementation/SKILL.md
- Django Idioms
@.agents/skills/django-idioms/SKILL.md
- Testability Patterns
@.agents/skills/testability-patterns/SKILL.md
- Concurrency and Threading Principles
@concurrency-and-threading-principles.md
- Performance Optimization Principles
@performance-optimization-principles.md
- Resource and Memory Management Principles
@resources-and-memory-management-principles.md
- Security Mandate
@security-mandate.md
- Dependency Management Principles
@dependency-management-principles.md
- Recommended Dependencies
@references/recommended-dependencies.md
- Python Patterns and Anti-Patterns
@references/python-patterns-and-anti-patterns.md