| name | pep8 |
| description | Enforces modern Python 3.11+ coding standards, PEP 8 compliance, and type-hinting best practices automatically. This skill should be used when writing, reviewing, or refactoring Python code to ensure consistency with PEP 8, proper type hints, Google-style docstrings, and modern Python idioms. |
Python Style & PEP 8 Enforcement
Auto-enforce Python 3.11+ standards.
When to Use
- Writing/reviewing Python code
- Type hint issues or style violations
- User requests PEP 8 check
Core Standards
| Standard | Desc |
|---|
| PEP 8 | Naming, imports, spacing |
| PEP 484/585 | Type hints (modern) |
| PEP 257 | Docstrings |
| PEP 604 | Union | |
| PEP 570/3102 | / positional, * keyword |
Naming
class UserAccount: pass
class HTTPClient: pass
def calculate_total(): pass
async def fetch_data(): pass
user_name = "john"
MAX_RETRIES = 3
def _internal(): pass
__mangled = "hidden"
T = TypeVar("T")
UserT = TypeVar("UserT", bound="User")
Type Hints (3.11+)
Modern Syntax (Required)
def process(items: list[str]) -> dict[str, int]: ...
def find_user(id: str) -> User | None: ...
from typing import Self
class Builder:
def chain(self) -> Self: return self
Patterns
from collections.abc import Callable, Awaitable
from typing import TypedDict, Literal, TypeAlias, ParamSpec, Generic
Handler = Callable[[Request], Response]
AsyncHandler = Callable[[Request], Awaitable[Response]]
class UserData(TypedDict):
id: str
email: Required[str]
phone: NotRequired[str | None]
Status = Literal["pending", "active", "disabled"]
JsonValue: TypeAlias = str | int | float | bool | None | list["JsonValue"] | dict[str, "JsonValue"]
P = ParamSpec("P")
def logged(fn: Callable[P, T]) -> Callable[P, T]: ...
class Repo(Generic[T]):
def get(self, id: str) -> T | None: ...
Deprecated (Never Use)
List[str]
Optional[int]
Dict[str, int]
Tuple[int, str]
Union[int, str]
Docstrings (Google Style)
def calculate_discount(price: float, percent: float, min_price: float = 0.0) -> float:
"""Calculate discounted price w/ floor.
Args:
price: Original price.
percent: Discount (0-100).
min_price: Min allowed price.
Returns:
Final price, never below min_price.
Raises:
ValueError: If invalid inputs.
"""
Skip docstrings for: self-documenting fns, _private methods, trivial @property
Imports
import asyncio
from pathlib import Path
from typing import Any
import httpx
from fastapi import Depends, HTTPException
from pydantic import BaseModel
from app.core.config import settings
from app.models import User
Rules: No wildcards (*), group from same module, parentheses for long imports
Function Signatures (PEP 570/3102)
def api_fn(
x: int, y: int,
/,
z: int = 0,
*,
strict: bool = False,
) -> Result: ...
Overloads
from typing import overload
@overload
def process(v: int) -> int: ...
@overload
def process(v: str) -> str: ...
def process(v: int | str) -> int | str:
return v * 2 if isinstance(v, int) else v.upper()
Function Design
| Lines | Status |
|---|
| < 20 | Ideal |
| 20-30 | OK |
| 30-50 | Split |
| > 50 | Refactor |
Params: Max 5 → use dataclass/config obj for more
Returns: Always annotate; no flag-based return types
Exception Handling
try:
user = await db.get(User, id)
except IntegrityError as e:
raise UserExistsError(id) from e
async with AsyncSession(engine) as session:
async with session.begin(): ...
except:
except Exception:
pass
Constants
MAX_RETRIES = 3
DEFAULT_TIMEOUT = timedelta(seconds=30)
FORMATS = frozenset({"json", "xml"})
class Status(StrEnum):
PENDING = "pending"
ACTIVE = "active"
await asyncio.sleep(5)
await asyncio.sleep(INTERVAL)
Async
async with httpx.AsyncClient() as client:
resp = await client.get(url)
results = await asyncio.gather(fetch_a(), fetch_b(), return_exceptions=True)
async with asyncio.TaskGroup() as tg:
tg.create_task(fetch_a())
tg.create_task(fetch_b())
async with asyncio.timeout(5.0):
await slow_op()
await asyncio.to_thread(blocking_io)
await asyncio.sleep(1)
Pathlib (NOT os.path)
from pathlib import Path
data = Path("data") / "config.json"
text = data.read_text(encoding="utf-8")
data.write_text(json.dumps(obj))
path.parent
path.stem
path.suffix
path.name
Logging
logger = logging.getLogger(__name__)
logger.info("Processing %s items", count)
logger.info(f"Processing {count}")
logger.exception("Failed")
Data Models
| Type | Use Case |
|---|
| TypedDict | External JSON/dicts |
| dataclass | Internal DTOs |
| Pydantic | Validation needed |
| NamedTuple | Immutable, hashable |
Context Managers
from contextlib import suppress, asynccontextmanager
with suppress(FileNotFoundError):
Path("temp.txt").unlink()
@asynccontextmanager
async def connection():
conn = await create()
try: yield conn
finally: await conn.close()
Anti-Patterns
| Bad | Fix |
|---|
| No type hints | Type all params & returns |
List, Optional | list, | None |
Bare except: | Specific exceptions |
| Magic numbers | Named constants |
d, x, temp | Descriptive names |
process() | process_orders() |
| > 50 lines | Split fn |
| Mutable defaults | None + factory |
== None | is None |
| f-strings in logger | %s formatting |
| os.path | pathlib |
Python 3.11+ Features
match cmd:
case {"action": "create", "data": d}: create(d)
case _: raise ValueError()
except* ValueError as eg:
for e in eg.exceptions: handle(e)
import tomllib
config = tomllib.load(open("config.toml", "rb"))
from typing import Self
def build(self) -> Self: return self
Formatting
- Line: 88 (Black) or 79 (strict PEP 8)
- Indent: 4 spaces
- Blanks: 2 top-level, 1 methods
- Trailing commas in multi-line
Scripts
Available in scripts/:
| Script | Purpose | Usage |
|---|
check_style.py | Full check (ruff + pycodestyle + mypy) | python check_style.py src/ --fix |
check_pep8.sh | Quick PEP 8 only | ./check_pep8.sh script.py |
check_types.sh | Type hints only | ./check_types.sh src/ --strict |
fix_style.sh | Auto-fix issues | ./fix_style.sh src/ |
Install deps: pip install ruff pycodestyle mypy
Tooling
pyproject.toml
[tool.ruff]
target-version = "py311"
line-length = 88
[tool.ruff.lint]
select = ["E", "W", "F", "I", "B", "UP", "N", "RUF", "ASYNC", "S"]
ignore = ["E501"]
[tool.ruff.lint.isort]
known-first-party = ["app"]
[tool.mypy]
python_version = "3.11"
strict = true
Pre-commit
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
hooks:
- id: mypy