Skip to content
Back to skills

Python Patterns

ASecurity

Python idioms — type hints, dataclasses, async/await, generators, pytest, common pitfalls. Activate when writing or reviewing Python code.

  • 3 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 9, 2026
ai-agentspythongobashsqlexpressdjangotestingapidatabasebackend

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned October 9, 2026

npx -y skills add alisattorov06/ali-kiro --skill python-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Python Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Python Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/alisattorov06-python-patterns/badge)](https://www.skillsdirectory.com/skills/alisattorov06-python-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: python-patterns
description: Python idioms — type hints, dataclasses, async/await, generators, pytest, common pitfalls. Activate when writing or reviewing Python code.
origin: FlowDeck
---

# Python Patterns Skill

Idiomatic Python for production-grade code. Covers modern Python 3.10+ practices.

## When to Activate

Activate when:
- Writing new Python modules or packages
- Reviewing Python code for correctness and idiom
- Deciding between data modeling approaches (dataclass vs TypedDict vs Pydantic)
- Designing async services or background workers
- Setting up testing infrastructure

## Type Hints

Python's type system (PEP 484, 526, 544) makes code self-documenting and enables static analysis with mypy or pyright.

### Basic Annotations

```python
# Variables (PEP 526)
count: int = 0
names: list[str] = []
mapping: dict[str, int] = {}

# Functions — always annotate public API
def greet(name: str, times: int = 1) -> str:
    return (f"Hello, {name}!\n" * times).rstrip()

# Optional and Union (Python 3.10+ union syntax preferred)
def find_user(user_id: int) -> "User | None":
    ...

# Use TypeAlias for reused complex types
type UserId = int          # Python 3.12+
UserId = NewType("UserId", int)  # pre-3.12
```

### Protocols (PEP 544) — Structural Subtyping

Prefer Protocol over ABC when you don't control the implementor.

```python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Serializable(Protocol):
    def to_dict(self) -> dict[str, object]: ...

def save(obj: Serializable) -> None:
    data = obj.to_dict()
    ...

# Any class with to_dict() satisfies Serializable — no inheritance required
```

### Generics

```python
from typing import TypeVar, Generic

T = TypeVar("T")

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()
```

## Data Modeling: Dataclass vs TypedDict vs Pydantic

Choose based on where the data lives and what guarantees you need.

### Dataclass — in-memory objects with behavior

```python
from dataclasses import dataclass, field

@dataclass
class Order:
    id: str
    items: list[str] = field(default_factory=list)
    total: float = 0.0

    def add_item(self, item: str, price: float) -> None:
        self.items.append(item)
        self.total += price

# Use @dataclass(frozen=True) for immutable value objects
@dataclass(frozen=True)
class Money:
    amount: int   # stored in cents
    currency: str = "USD"
```

### TypedDict — typed dictionaries, no runtime overhead

Best for function signatures that accept/return dict-shaped data (JSON responses, kwargs).

```python
from typing import TypedDict, NotRequired

class UserPayload(TypedDict):
    id: str
    email: str
    name: NotRequired[str]  # optional key

def process(payload: UserPayload) -> None:
    print(payload["id"])   # static checker knows the type
```

### Pydantic — validation at the boundary

Use at I/O boundaries: API request bodies, config files, deserialized data.

```python
from pydantic import BaseModel, Field, field_validator

class CreateUserRequest(BaseModel):
    email: str
    age: int = Field(gt=0, lt=150)
    name: str = Field(min_length=1, max_length=100)

    @field_validator("email")
    @classmethod
    def email_must_have_at(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("not a valid email")
        return v.lower()

# Pydantic raises ValidationError with structured detail on bad input
req = CreateUserRequest(email="ALICE@EXAMPLE.COM", age=30, name="Alice")
# req.email == "alice@example.com"
```

**Decision rule:** dataclass for domain objects with behavior → TypedDict for dicts that stay dicts → Pydantic for external input validation.

## Context Managers

The `with` statement guarantees cleanup whether or not an exception occurs.

### Using `contextlib`

```python
from contextlib import contextmanager, asynccontextmanager

@contextmanager
def managed_connection(dsn: str):
    conn = connect(dsn)
    try:
        yield conn
    finally:
        conn.close()

# Usage
with managed_connection("postgresql://...") as conn:
    conn.execute("SELECT 1")
```

### Class-Based Context Managers

```python
class Timer:
    def __enter__(self) -> "Timer":
        import time
        self._start = time.perf_counter()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
        self.elapsed = time.perf_counter() - self._start
        return False  # don't suppress exceptions

with Timer() as t:
    expensive_operation()
print(f"Took {t.elapsed:.3f}s")
```

### Suppressing Exceptions

```python
from contextlib import suppress

with suppress(FileNotFoundError):
    Path("optional.txt").unlink()
```

## Generator and Iterator Patterns

Generators yield values lazily — use them for large sequences or pipelines.

### Basic Generator

```python
def read_chunks(path: str, size: int = 8192):
    with open(path, "rb") as f:
        while chunk := f.read(size):
            yield chunk

# Entire file never loaded at once
for chunk in read_chunks("large.bin"):
    process(chunk)
```

### Generator Pipelines

```python
def parse_lines(lines):
    for line in lines:
        yield line.strip()

def filter_comments(lines):
    for line in lines:
        if not line.startswith("#"):
            yield line

def process_file(path: str):
    raw = open(path)
    stripped = parse_lines(raw)
    meaningful = filter_comments(stripped)
    return meaningful  # lazy, no I/O yet
```

### `itertools` for Composition

```python
import itertools

# Flatten a list of lists
flat = list(itertools.chain.from_iterable([[1, 2], [3, 4]]))

# Sliding window
def windows(iterable, n):
    iters = itertools.tee(iterable, n)
    for i, it in enumerate(iters):
        next(itertools.islice(it, i, i), None)
    return zip(*iters)
```

## Asyncio Patterns

Use `async/await` for I/O-bound concurrency. Don't use it for CPU-bound work (use `multiprocessing` instead).

### Async HTTP: httpx vs aiohttp

```python
# httpx — recommended for most cases (sync and async API, HTTP/2)
import httpx

async def fetch_user(user_id: int) -> dict:
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"https://api.example.com/users/{user_id}")
        resp.raise_for_status()
        return resp.json()

# aiohttp — use when you need streaming or websockets
import aiohttp

async def stream_large_file(url: str):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            async for chunk in resp.content.iter_chunked(8192):
                process(chunk)
```

### Concurrent Tasks

```python
import asyncio

# Run independent I/O operations concurrently
async def fetch_all(ids: list[int]) -> list[dict]:
    async with httpx.AsyncClient() as client:
        tasks = [fetch_one(client, id) for id in ids]
        return await asyncio.gather(*tasks)

# Timeout and cancellation
async def with_timeout(coro, seconds: float):
    try:
        return await asyncio.wait_for(coro, timeout=seconds)
    except asyncio.TimeoutError:
        raise TimeoutError(f"operation exceeded {seconds}s")
```

### Event Loop Best Practices

```python
# Don't call asyncio.get_event_loop() in new code — use asyncio.run()
if __name__ == "__main__":
    asyncio.run(main())

# For libraries, accept a running loop rather than creating one
async def library_function() -> None:
    loop = asyncio.get_running_loop()  # raises if not inside async context
    ...
```

## Comprehensions — When to Use vs Loops

Comprehensions communicate intent at a glance. Loops are clearer for side effects.

```python
# ✅ Comprehension: transform + filter, no side effects
active_names = [u.name for u in users if u.is_active]
index = {u.id: u for u in users}
unique_tags = {tag for post in posts for tag in post.tags}

# ✅ Generator expression: same as list comprehension but lazy
total = sum(item.price for item in cart)

# ❌ Comprehension with side effects — use a loop
[print(x) for x in items]   # bad
for x in items:              # good
    print(x)

# ❌ Deeply nested comprehensions — use a loop
matrix = [[col * row for col in range(5)] for row in range(5)]  # fine
# but three levels deep: write a loop
```

## Exception Hierarchy and Custom Exceptions

Define a base exception per module/package and branch from it.

```python
# exceptions.py
class AppError(Exception):
    """Base for all application errors."""

class NotFoundError(AppError):
    def __init__(self, resource: str, id: object) -> None:
        super().__init__(f"{resource} {id!r} not found")
        self.resource = resource
        self.id = id

class ValidationError(AppError):
    def __init__(self, field: str, message: str) -> None:
        super().__init__(f"{field}: {message}")
        self.field = field

# Catching
try:
    get_user(user_id)
except NotFoundError as exc:
    return 404, {"error": str(exc)}
except AppError as exc:
    logger.exception("unexpected app error")
    return 500, {"error": "internal error"}

# Exception chaining — always preserve cause
try:
    result = json.loads(raw)
except json.JSONDecodeError as exc:
    raise ValidationError("body", "invalid JSON") from exc
```

## Dependency Management

### pyproject.toml (PEP 518/621)

```toml
[project]
name = "my-service"
version = "1.0.0"
requires-python = ">=3.11"
dependencies = [
    "httpx>=0.27",
    "pydantic>=2.0",
]

[project.optional-dependencies]
dev = ["pytest>=8", "mypy", "ruff"]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
```

### uv — fast package manager

```bash
# Create and activate venv
uv venv && source .venv/bin/activate

# Install project with dev deps
uv pip install -e ".[dev]"

# Add a dependency (updates pyproject.toml)
uv add httpx

# Lock for reproducibility
uv lock
```

### poetry — alternative with integrated lock file

```bash
poetry new my-project
poetry add httpx pydantic
poetry add --group dev pytest mypy
poetry run pytest
```

## Testing with pytest

### Fixtures

```python
import pytest
from myapp.db import Database

@pytest.fixture
def db():
    database = Database(":memory:")
    database.migrate()
    yield database
    database.close()

@pytest.fixture
def user(db):
    return db.create_user(email="alice@example.com", name="Alice")

def test_user_lookup(db, user):
    found = db.get_user(user.id)
    assert found.email == user.email
```

### Parametrize

```python
@pytest.mark.parametrize("email,valid", [
    ("alice@example.com", True),
    ("no-at-sign", False),
    ("@nodomain", False),
    ("spaces @x.com", False),
])
def test_email_validation(email: str, valid: bool) -> None:
    assert validate_email(email) == valid
```

### Mocking

```python
from unittest.mock import AsyncMock, MagicMock, patch

def test_send_notification(mocker):  # with pytest-mock
    mock_send = mocker.patch("myapp.email.send_email")
    notify_user(user_id=1)
    mock_send.assert_called_once_with(
        to="alice@example.com",
        subject="Welcome",
    )

async def test_async_fetch():
    with patch("myapp.client.httpx.AsyncClient") as MockClient:
        instance = MockClient.return_value.__aenter__.return_value
        instance.get.return_value = AsyncMock(
            status_code=200,
            json=lambda: {"id": 1},
        )
        result = await fetch_user(1)
    assert result["id"] == 1
```

## Common Pitfalls

### Mutable Default Arguments

```python
# ❌ The list is created ONCE and shared across all calls
def append_to(item, lst=[]):
    lst.append(item)
    return lst

# ✅ Use None as sentinel
def append_to(item, lst=None):
    if lst is None:
        lst = []
    lst.append(item)
    return lst
```

### Late Binding in Closures

```python
# ❌ All lambdas capture the same `i` variable
funcs = [lambda: i for i in range(5)]
# funcs[0]() == 4  (not 0!)

# ✅ Bind at definition time with default argument
funcs = [lambda i=i: i for i in range(5)]
# funcs[0]() == 0
```

### The GIL and CPU-Bound Work

```python
# The GIL prevents true parallel execution of Python bytecode.
# For CPU-bound work, use multiprocessing or ProcessPoolExecutor.
from concurrent.futures import ProcessPoolExecutor

def cpu_bound(n: int) -> int:
    return sum(range(n))

with ProcessPoolExecutor() as pool:
    results = list(pool.map(cpu_bound, [10**7, 10**7, 10**7]))
```

### Circular Imports

```python
# ❌ module_a imports module_b, module_b imports module_a at module level
# ✅ Move the import inside the function that needs it
def get_thing():
    from myapp.other_module import Thing  # deferred import
    return Thing()

# ✅ Or restructure: extract shared types into a third module
```

## Related Skills
- backend-patterns
- django-patterns
- python-testing

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.