Python type hints, Protocols, Pydantic, async/await, pytest, ruff, mypy strict.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add irahardianto/antigravity-setup --skill python-idioms --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Python Idioms?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/irahardianto-python-idioms)More formats (shields.io, HTML) on the badges page.
---
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.
```python
# ❌ Anti-pattern: Untyped or legacy typing
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): ... # Overrides base class? Maybe.
```
```python
# ✅ Recommended pattern: Modern typing syntax
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.
```python
# ❌ Anti-pattern: Broad except, swallowing errors, assigning None
def load_data():
try:
data = fetch()
return data
except Exception as e:
print(f"Failed: {e}")
finally:
return None # Swallows exception!
res = dict.get("key") # Might return None, then what?
```
```python
# ✅ Recommended pattern: Specific exceptions, exception groups, add_note
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
# Exception groups (3.11+)
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.
```python
# ✅ Recommended pattern: Dataclasses
from dataclasses import dataclass
@dataclass(frozen=True, slots=True, kw_only=True)
class UserConfig:
id: int
username: str
active: bool = True
```
```python
# ✅ Recommended pattern: Pydantic Validation
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.
```python
# ✅ Recommended pattern: Dependency Injection with Protocols
from typing import Protocol
class MessageSender(Protocol):
def send(self, msg: str) -> None: ...
class EmailSender:
def send(self, msg: str) -> None:
pass # Implementation
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.
```python
# ❌ Anti-pattern: Unstructured concurrency
import asyncio
async def main():
await asyncio.gather(task1(), task2()) # Errors in one task don't cancel the other easily
```
```python
# ✅ Recommended pattern: Structured concurrency with TaskGroup
import asyncio
async def main():
try:
async with asyncio.TaskGroup() as tg:
task1 = tg.create_task(fetch_data())
task2 = tg.create_task(process_data())
# tg automatically waits for all tasks. If one fails, others are cancelled.
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)
```python
# ❌ Anti-pattern: Mutable default argument
def add_item(item: str, items: list = []) -> list:
items.append(item)
return items
# ✅ Recommended pattern: None sentinel
def add_item(item: str, items: list | None = None) -> list:
if items is None:
items = []
items.append(item)
return items
```
```python
# ✅ Recommended pattern: Pattern matching & itertools.batched
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.
```python
# ❌ Anti-pattern: Undocumented public API
def calculate_discount(price: float, rate: float) -> float:
return price * (1 - rate)
```
```python
# ✅ Recommended pattern: Documented public API
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
1. Minimize dependency count — each dependency is an attack surface.
2. Audit regularly — run `pip-audit` in CI.
3. Use `pyproject.toml` as the single source of truth for project metadata.
4. Commit lockfiles for applications (`uv.lock`, `requirements.lock`).
5. Prefer stdlib over third-party when feature parity exists.
6. Check for unused dependencies with import analysis.
> For the full curated dependency list with versions, see `references/recommended-dependencies.md`.
### Configuration and Environment
1. Never scatter `os.environ` / `os.getenv()` calls throughout the codebase.
2. Use pydantic-settings `BaseSettings` for validated, typed config.
3. Fail fast on missing required config at boot, not at first use.
```python
# ❌ Anti-pattern: Scattered os.getenv calls
import os
def connect_db():
db_url = os.getenv("DATABASE_URL") # Fails later if missing
# connect...
```
```python
# ✅ Recommended pattern: Centralized typed config
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
api_key: str
# Fails immediately at startup if env vars are missing or invalid
settings = Settings()
def connect_db():
db_url = settings.database_url
# connect...
```
### 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`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!