Modern Python 3.12+ patterns your AI agent should use. Type hints, async/await, Pydantic v2, uv, match statements, and project structure.
How this skill is triggered — by the user, by Claude, or both
Slash command
/python-best-practices:python-best-practicesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this skill when writing or reviewing Python code targeting Python 3.12+. It enforces modern type
Use this skill when writing or reviewing Python code targeting Python 3.12+. It enforces modern type hints, async patterns, Pydantic v2 API, project structure, and standard library usage. Agents trained on older codebases often emit outdated patterns; this skill corrects that.
Wrong:
from typing import Union, List, Dict, Optional
def process(items: List[str]) -> Optional[Dict[str, Union[int, str]]]:
...
Correct:
def process(items: list[str]) -> dict[str, int | str] | None:
...
Why: list[str] and X | Y are built-in in Python 3.9+ (PEP 585, PEP 604). Importing
typing.List, typing.Union, typing.Optional is verbose and deprecated for built-in generics.
Wrong:
from typing import TypeVar
T = TypeVar("T")
def max(args: list[T]) -> T:
...
Correct:
def max[T](args: list[T]) -> T:
...
Why: PEP 695 type parameters are simpler and local to the function/class. Same for generic
classes: class Bag[T]: instead of class Bag(Generic[T]):.
Wrong:
def http_error(status: int) -> str:
if status == 400:
return "Bad request"
elif status == 404:
return "Not found"
elif status == 418:
return "I'm a teapot"
else:
return "Something's wrong"
Correct:
def http_error(status: int) -> str:
match status:
case 400:
return "Bad request"
case 404:
return "Not found"
case 418:
return "I'm a teapot"
case _:
return "Something's wrong"
Why: Structural pattern matching (PEP 634) is clearer for disjoint cases and supports
destructuring. Use case 401 | 403 | 404: for multiple literals.
Wrong:
from pydantic import BaseModel, validator, root_validator
class Model(BaseModel):
x: list[int]
class Config:
validate_assignment = True
@validator("x", each_item=True)
def validate_x(cls, v):
return v * 2
@root_validator
def check_a_b(cls, values):
...
Correct:
from pydantic import BaseModel, field_validator, model_validator, ConfigDict
class Model(BaseModel):
model_config = ConfigDict(validate_assignment=True)
x: list[int]
@field_validator("x", mode="each")
@classmethod
def validate_x(cls, v: int) -> int:
return v * 2
@model_validator(mode="after")
def check_a_b(self) -> "Model":
...
return self
Why: Pydantic v1 decorators and class Config are deprecated. v2 uses model_config,
field_validator, model_validator with explicit modes.
Wrong:
pip install -r requirements.txt
poetry init
Correct:
uv init
uv add requests pydantic
uv sync
Why: uv is fast, has a modern lockfile, and supports pyproject.toml natively. Prefer it for new projects.
Wrong:
# setup.py
from setuptools import setup
setup(name="myapp", version="0.1", ...)
Correct:
# pyproject.toml
[project]
name = "myapp"
version = "0.1"
dependencies = ["requests"]
Why: setup.py and setup.cfg are legacy. pyproject.toml (PEP 517/518) is the standard.
Wrong:
import os
path = os.path.join(os.getcwd(), "data", "file.txt")
if os.path.exists(path):
with open(path) as f:
...
Correct:
from pathlib import Path
path = Path.cwd() / "data" / "file.txt"
if path.exists():
path.read_text()
Why: pathlib is object-oriented, clearer, and cross-platform. Prefer
Path.read_text()/write_text() over open() for simple reads/writes.
Wrong:
"User %s has %d items" % (name, count)
"User {} has {} items".format(name, count)
Correct:
f"User {name} has {count} items"
Why: f-strings are faster and more readable.
Wrong:
def get_user() -> dict:
return {"name": "Alice", "age": 30}
Correct:
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
def get_user() -> User:
return User(name="Alice", age=30)
Why: Structured types give type safety and IDE support. Use Pydantic when you need validation.
Wrong:
import asyncio
results = await asyncio.gather(f1(), f2(), f3())
Correct:
import asyncio
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(f1())
t2 = tg.create_task(f2())
t3 = tg.create_task(f3())
results = (t1.result(), t2.result(), t3.result())
Why: TaskGroup propagates exceptions correctly and cancels other tasks on failure.
Wrong:
try:
...
except (ValueError, TypeError) as e:
...
Correct (when dealing with ExceptionGroup from concurrency):
try:
...
except* TypeError as e:
print(f"caught {type(e)} with nested {e.exceptions}")
except* OSError as e:
...
Why: except* handles ExceptionGroups from asyncio and concurrent tasks. Use when catching from
TaskGroup or similar.
Wrong:
import toml
data = toml.load("pyproject.toml")
Correct:
import tomllib
with open("pyproject.toml", "rb") as f:
data = tomllib.load(f)
Why: tomllib is built-in; no extra dependency. Read in binary mode.
Wrong:
class Child(Parent):
def method(self) -> str:
return "child"
Correct:
from typing import override
class Child(Parent):
@override
def method(self) -> str:
return "child"
Why: @override makes intent explicit and catches typos in method names.
Wrong:
match x:
case (a, b):
if a > b:
...
Correct:
match x:
case (a, b) if a > b:
...
Why: Guards (if) keep logic in the pattern and avoid nested conditionals.
Wrong:
from typing import Iterable, Mapping
Correct:
from collections.abc import Iterable, Mapping
Why: collections.abc is the canonical source for ABCs. typing re-exports them but collections.abc is preferred for runtime checks.
class Bag[T]:
def __iter__(self) -> Iterator[T]:
...
def add(self, arg: T) -> None:
...
type ListOrSet[T] = list[T] | set[T]
@model_validator(mode="before")
@classmethod
def check_card_number_not_present(cls, data: Any) -> Any:
if isinstance(data, dict) and "card_number" in data:
raise ValueError("'card_number' should not be included")
return data
case 401 | 403 | 404:
return "Not allowed"
from typing import List, Dict, Union, Optional for built-in generics; use list,
dict, X | Y, X | None.@validator or @root_validator or class Config with Pydantic; use v2 API.os.path for new code; use pathlib.Path.setup.py or setup.cfg; use pyproject.toml.% or .format() when f-strings are available.toml package; use stdlib tomllib (Python 3.11+).TypeVar for simple generics when Python 3.12+ type parameter syntax applies.asyncio.gather for structured concurrency when TaskGroup is available (3.11+).from typing import Iterable, Mapping; use from collections.abc import ....Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub ofershap/python-best-practices