Internal skill — loaded automatically by wfc-build, wfc-implement, wfc-test, and wfc-review when ALL conditions are true: 1. TARGET LANGUAGE: Code being written, reviewed, or scaffolded is Python 2. PROJECT CONTEXT: Confirmed WFC project via one of: - `[tool.wfc]` table in pyproject.toml, OR `wfc.yaml` at project root, OR user explicitly requests "WFC standards" or "WFC scaffolding" 3. TASK TYPE: Writing, modifying, or reviewing application code in: - `api/` (FastAPI routes, Pydantic schemas) - `services/` (business logic, orchestration) - `repositories/` (data access, external APIs) - `models/` (domain objects, value types) - Test files that validate the above NOT for: General Python questions; standalone scripts; Jupyter notebooks; CI/CD config; database migrations; build/packaging scripts; auto-generated code (protobufs, gRPC stubs); Poetry/pip projects declining UV migration.
Resources
1Install
npx skillscat add sam-fakhreddine/wfc/wfc-python Install via the SkillsCat registry.
WFC:PYTHON - Python Development Standards
Python-specific standards for WFC projects. Requires wfc-code-standards skill loaded — inherits universal standards (three-tier architecture, SOLID, composition over inheritance, 500-line limit, DRY, structured logging, testing philosophy, async safety, dependency management, documentation).
This skill adds Python-specific: syntax requirements, tooling (UV, black, ruff, mypy), libraries, frameworks, and enforcement patterns.
Python Coding Standards
These are non-negotiable. Every Python file WFC produces must follow these conventions, in addition to the universal standards in wfc-code-standards.
Python 3.12+ Required
Target requires-python = ">=3.12". Use modern features:
# type statement (3.12+) - replaces TypeAlias
type Vector = list[float]
type Handler[T] = Callable[[T], Awaitable[None]]
type Result[T, E] = T | E
# Generic syntax on classes/functions (3.12+)
class Repository[T]:
def get(self, id: str) -> T | None: ...
def list(self, *, limit: int = 100) -> list[T]: ...
def transform[T, R](items: Iterable[T], fn: Callable[[T], R]) -> list[R]:
return [fn(item) for item in items]
# f-string improvements (3.12+) - nested quotes, multiline
msg = f"User {user.name!r} has {len(user.roles)} roles"
query = f"""
SELECT * FROM {table}
WHERE status = {status!r}
"""
# ExceptionGroup + except* (3.11+)
try:
async with TaskGroup() as tg:
tg.create_task(fetch_users())
tg.create_task(fetch_orders())
except* ValueError as eg:
for exc in eg.exceptions:
log.error("validation_failed", error=str(exc))
except* ConnectionError as eg:
log.error("connection_failed", count=len(eg.exceptions))
# match/case (3.10+)
match response.status_code:
case 200:
return orjson.loads(response.content)
case 404:
return None
case 429:
raise RateLimitError(retry_after=response.headers.get("Retry-After"))
case status if status >= 500:
raise ServerError(status)
case _:
raise UnexpectedStatus(response.status_code)
# Structural pattern matching with guards
match event:
case {"type": "click", "target": str(target)} if target.startswith("#"):
handle_anchor(target)
case {"type": "submit", "data": dict(data)}:
process_form(data)Black Formatting (Non-Negotiable)
Black is the only formatter. No exceptions, no overrides. PEP 8 is enforced via
ruff, but black wins on any style conflict. Ruff must ignore the rules that black
owns (line length, indentation, whitespace in expressions).
# pyproject.toml
[tool.black]
line-length = 88
target-version = ["py312"]Black / PEP 8 sync - ruff must ignore rules that black controls:
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "B", "SIM", "TCH"]
# Rules that conflict with black - black is authoritative on these:
# E111 - indentation (black owns this)
# E114 - indentation of comment (black owns this)
# E117 - over-indented (black owns this)
# E501 - line too long (black wraps at 88, ruff must not second-guess)
# W191 - indentation contains tabs (black uses spaces, but owns decision)
# E203 - whitespace before ':' (black intentionally does `x[1 : 2]`)
ignore = ["E111", "E114", "E117", "E501", "W191", "E203"]Run order: black first (formats), then ruff check (lints the result). Never
run ruff's formatter (ruff format) - we use black exclusively.
Rules:
- Line length: 88 (black default) - ruff defers to black via E501 ignore
- No
# fmt: off/# fmt: onunless absolutely unavoidable - No single-line compound statements:
if x: returngoes on two lines - Trailing commas on multi-line collections (black enforces this)
- Double quotes for strings (black default)
- PEP 8 compliance via ruff's
E+Wrules, minus the black-owned subset above
Full Type Annotations (Mandatory)
Every function signature, every return type, every class attribute. No untyped code.
from collections.abc import Callable, Iterable, Sequence
from typing import Any, Self
# Function signatures - always typed
def process_batch(
items: Sequence[dict[str, Any]],
*,
batch_size: int = 100,
on_error: Callable[[Exception], None] | None = None,
) -> list[ProcessResult]:
...
# Class attributes - always typed
class Config:
host: str
port: int
debug: bool = False
tags: list[str] = field(default_factory=list)
# Use modern union syntax
def find_user(user_id: str) -> User | None: # Not Optional[User]
...
# Use collections.abc, not typing
def merge(a: Iterable[int], b: Iterable[int]) -> list[int]: # Not List, Iterable from typing
...
# Use Self for fluent interfaces (3.11+)
class Builder:
def with_name(self, name: str) -> Self:
self.name = name
return selfRules:
str | NonenotOptional[str]list[int]notList[int]dict[str, Any]notDict[str, Any]collections.abc.Callablenottyping.Callable- Use
Selffor methods returning own type - Use
typestatement for type aliases (3.12+) - Use generics syntax on classes
class Foo[T]:(3.12+)
PEP 562 - Module-Level __getattr__
Use PEP 562 for lazy imports in __init__.py files when exporting from submodules that import heavy dependencies (network libraries, crypto, ORMs, large frameworks).
# wfc/skills/wfc-python/__init__.py pattern
__version__ = "0.1.0"
# Lightweight exports available immediately
PREFERRED_LIBRARIES = { ... }
# Heavy imports loaded on demand via PEP 562
def __getattr__(name: str) -> Any:
if name == "PythonStandards":
from wfc.skills.wfc_python.standards import PythonStandards
return PythonStandards
if name == "LibraryRegistry":
from wfc.skills.wfc_python.registry import LibraryRegistry
return LibraryRegistry
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def __dir__() -> list[str]:
return [
*__all__,
"PythonStandards",
"LibraryRegistry",
]Rules:
- Use PEP 562 when exporting modules that import: httpx, pydantic, structlog, orjson, ORMs, crypto libraries
- Keep module-level imports to stdlib and lightweight constants
- Heavy dependencies load lazily through
__getattr__ - Always implement
__dir__alongside__getattr__for discoverability - Avoid circular dependencies: use absolute imports in lazy-loaded modules
Architecture in Python (see wfc-code-standards)
Three-tier, SOLID, composition over inheritance, DRY, 500-line limit, and elegance rules are defined in wfc-code-standards. Below is the Python-specific implementation.
Python project structure (three-tier):
project/
├── api/ # PRESENTATION: FastAPI routes, Pydantic schemas
│ ├── routes.py
│ └── schemas.py
├── services/ # LOGIC: Business rules, orchestration
│ ├── orders.py
│ └── payments.py
├── repositories/ # DATA: Database, APIs, file I/O
│ ├── orders.py
│ └── cache.py
└── models/ # DOMAIN: Dataclasses, enums, value objects
└── order.pyFile naming conventions:
- Module names:
snake_case.py(e.g.,order_service.py, notOrderService.py) - Class names:
PascalCasematching domain concept (e.g.,OrderServiceinorder_service.pyororders.py) - Protocol definitions: Place in same file as consumer, or
protocols.pyif shared across 3+ consumers
SOLID in Python - use Protocol for DIP and ISP:
from typing import Protocol
# Interface segregation via Protocol (not ABC)
class Readable(Protocol):
def get(self, id: str) -> Model: ...
class Writable(Protocol):
def save(self, model: Model) -> None: ...
# Dependency inversion via constructor injection
class OrderService:
def __init__(
self,
repo: OrderRepository, # Protocol, not concrete class
notifier: Notifier, # Protocol, not SmtpNotifier
) -> None:
self.repo = repo
self.notifier = notifier