Python architect
Skill ralvarezdev/ralvaskills/skills/languages/python-architect
My personal, ever-growing collection of AI skills for OpenCode and Claude Code. Enforces strict clean architecture and professional standards.
npx -y skills add ralvarezdev/ralvaskills --skill python-architectAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Python 3.14 enterprise standards — modern typing (PEP 649), immutable dataclasses, Protocol-based DI, asyncio discipline, pytest 9, psycopg + .sql files via importlib.resources. Use when writing, reviewing, or scaffolding Python code.
SKILL.md
13.2 KB, as published. Nobody here has run it
Python Architecture Standards
Targets Python 3.14. See STACK.md for pinned dependency versions.
1. Typing & Domain Safety
- Modern syntax: Built-in generics (
list[str],dict[K, V],X | None). Never the legacytyping.List/typing.Optional. - Deferred annotations (PEP 649, 3.14): Annotations are no longer eagerly evaluated — forward references no longer need quotes (
def f(arg: NotYetDefined)works). Inspect viaannotationlib.get_annotations(), not__annotations__directly. - Domain types:
typing.NewTypeto separate distinct concepts (UserIdvsOrderId). - Enums: Default to
Enum(with__str__overridden) for closed sets of domain states — members are distinct identities, not interchangeable with raw primitives, which catches accidental comparisons against arbitrary strings/ints. Reach forStrEnum(3.11+) when members must interoperate directly with strings — JSON payloads, query params, f-strings — without a.valuecall at every site. Reach forIntEnumwhen members must support arithmetic or ordering against plain integers (HTTP status codes, priority levels, wire values from an external system). Both tradeEnum's identity-safety for primitive compatibility — reach for them only when that interop is a real requirement, not by default. - Constraints:
Literalfor a narrow, function-local set of string flags that doesn't warrant a fullEnum. - Structured payloads:
TypedDictoverdict[str, Any]for known-shape mappings (see §3 for the broader dict/tuple-avoidance principle). - Subclassing safety:
typing.overridedecorator (3.12+) on every overriding method — mypy flags broken overrides.
2. Generators & Iterators
- Return
Iterator[T]/ write a generator when: the sequence is large or unbounded, the consumer might short-circuit (break, earlyreturn), or it's backed by a cursor (DB pagination, paginated HTTP APIs, file streaming). Laziness avoids materializing the whole sequence in memory. - Return
list[T]when: the result is small, bounded, and the caller almost always consumes the whole thing — don't wrap it in a generator just to look idiomatic. - Async generators:
AsyncIterator[T]/async def ... yieldfor streaming I/O (paginated API clients, chunked reads) — pairs withasync for. See §5 for asyncio discipline. itertools: default toolkit for lazy composition (chain,islice,groupby,pairwise) over manual index bookkeeping.- Anti-pattern: collecting a generator into a
listimmediately after producing it (list(gen())) just to satisfy a type checker — return the concrete type the caller actually needs instead of round-tripping through both.
3. Data Structures & Memory
- Immutability: Default to
@dataclass(slots=True, frozen=True)for DTOs and value objects. - Mutable defaults: Never use a mutable literal (
[],{},set()) as a function parameter default or a bare dataclass field default — it's shared across every call/instance. UseNoneand assign inside the function body, orfield(default_factory=list)on dataclasses. Enforced by ruff'sB006/B008(bugbear, see §11). - Pydantic vs dataclass boundary: Pydantic only at application boundaries (API request/response, DB row parsing, config). Standard dataclasses for core domain logic — keeps the domain free of validation-framework coupling.
- Typed models over dicts/tuples: Prefer a
dataclass/NamedTuple/TypedDict(see §1) todict[str, Any]or a rawtuplefor anything with a stable shape — attribute access catches typos and missing fields that dict keys and tuple indices can't. Reserve bare dicts/tuples for genuinely dynamic or anonymous data (arbitrary JSON blobs,zip()output consumed immediately, coordinate pairs). - Ordering:
@dataclass(order=True)for value objects that need comparison operators — avoid hand-rolled__lt__/__gt__/__le__/__ge__chains. For one-off custom sort keys, pass a plain function tosorted(key=...)rather than implementing a full ordering protocol. - Memory:
__slots__(explicit or viadataclass(slots=True)) on high-volume instances.
4. Interfaces & DI
- Protocols:
typing.Protocol(structural typing) over deepabc.ABCinheritance. Define protocols where consumed. - DI: Pass dependencies into
__init__. Never instantiate external clients inside a class. - State: No globals.
contextvarsonly when request-scoped state is unavoidable.
5. Concurrency & Resources
- Asyncio discipline: Never block the event loop. Offload sync I/O or CPU work via
asyncio.to_thread(). - Task groups:
asyncio.TaskGroupfor concurrent coroutines — handles cancellation and exception aggregation properly. Avoid bareasyncio.gather. - Multiple interpreters (PEP 734, 3.14): Use
concurrent.interpretersfor CPU-bound parallelism — true multi-core withoutmultiprocessing's overhead, no GIL contention. - Introspection: Debug live async apps with
python -m asyncio ps <PID>/pstree <PID>(3.14). - Free-threaded builds (PEP 703): Be aware of the no-GIL variant. Design hot paths to avoid shared mutable state regardless of GIL presence.
- Resources: Wrap I/O in
with/async with. Usecontextlibfor compositions.
6. Packages & Imports
- Imports: Three groups separated by blank lines — stdlib, third-party, local. Prefer absolute imports.
__init__.py: Minimal. Use__all__ = [...]to declare the public API explicitly.- Bundled resources: Use
importlib.resources.files(__package__).joinpath("...").read_text()for embedded files (SQL, templates). Survives wheel and zipapp packaging — never use__file__-relative paths for shipped assets.
7. Errors & Testing
- Exceptions: A base custom exception per module. Always chain (
raise NewError(...) from err). Never bareexcept:. - Exception groups (PEP 654, 3.11+):
asyncio.TaskGroup(§5) raisesExceptionGroupwhen child tasks fail — catch withexcept*(e.g.except* TimeoutError:), never a bareexcept Exception, or concurrent failures from separate tasks collapse into one swallowed exception. - Bracketless except (PEP 758, 3.14):
except TimeoutError, ConnectionRefusedError:is now valid without parens when noasclause. - Finally hazards (PEP 765, 3.14):
return/break/continueinsidefinallynow emits SyntaxWarning — refactor it out. - Iterables:
map(strict=True)(3.14) when consuming parallel iterables, matchingzip(strict=True). - Testing:
pytest 9withconftest.pyfixtures. Never the legacyunittestmodule.pytest-asynciofor async tests. - Integration tests against Docker dependencies:
testcontainers-python— spins up real Postgres/Redis/Kafka/etc. containers per test run instead of mocking the driver or relying on a shared dev instance. Mark these with a dedicatedpytestmarker (e.g.@pytest.mark.integration) and exclude by default sopyteststays fast.
8. Documentation
- Docstrings: Google style (Args, Returns, Raises).
- DRY: Don't repeat type info already in hints.
- Focus: Explain why (domain rules, edge cases), not what.
9. Stdlib defaults
Prefer stdlib when it covers the use case.
pathlib.Pathfor all paths — neveros.pathstrings. New in 3.14:Path.copy(),Path.move(),Path.copy_into(),Path.move_into()for recursive operations.compression.zstd(3.14) overgzip/bz2for new payloads —gzip/bz2/lzma/zlibare now re-exported undercompression.*.importlib.resourcesfor shipped files (see §6).contextlibfor resource lifecycle composition.dataclassesfor data containers (see §3).
10. Database access — SQL files + importlib.resources
Recommended pattern, not mandatory. Mirrors the Go sqlx + //go:embed philosophy: raw SQL in .sql files, loaded once at module import, executed via psycopg 3. No ORM by default — keeps queries auditable in git and gives editors full SQL syntax highlighting and linting.
from importlib.resources import files
import psycopg
from psycopg.rows import class_row
GET_USER_BY_ID = files(__package__).joinpath("queries/get_user_by_id.sql").read_text()
class UserRepo:
def __init__(self, conn: psycopg.AsyncConnection) -> None:
self._conn = conn
async def get_by_id(self, user_id: int) -> User | None:
async with self._conn.cursor(row_factory=class_row(User)) as cur:
await cur.execute(GET_USER_BY_ID, (user_id,))
return await cur.fetchone()
Layout:
src/myapp/userrepo/
├── __init__.py
├── repo.py
└── queries/
├── get_user_by_id.sql
├── insert_user.sql
└── list_users.sql
- Driver:
psycopg 3— sync + async, server-side cursors, COPY, prepared statements. - Migrations:
alembic— versioned, works with raw SQL (no SQLAlchemy ORM required). - Dynamic queries: Compose
.sqlfragments in Python; never concatenate user input — bind parameters. - When an ORM is genuinely needed: SQLAlchemy 2.x (Core or ORM). Record the decision in an ADR.
11. Tooling
- Environment + packaging:
uv— replacespip,pip-tools,virtualenv,pyenv. Single binary, fast. Commituv.lock; runuv sync --frozenin CI. - Lint + format:
ruff— replacesblack,isort,flake8,pyupgrade. One config, one tool. Drop-in template:assets/ruff.toml— copy to your project root asruff.toml(or fold intopyproject.tomlunder[tool.ruff]) and setknown-first-partyto your package name. Runruff checkandruff format --checkon every commit and in CI; treat warnings as errors.- Correctness & bugs:
F(pyflakes),B(bugbear, incl.B006/B008mutable defaults — see §3, andB904exception chaining — see §7),ASYNC(asyncio anti-patterns — see §5),RUF(ruff-specific, e.g.RUF012mutable class defaults). - Security:
S(flake8-bandit) — SQL/command injection, hardcoded secrets, weak crypto. Test files relaxS101/S105-S107viaper-file-ignoressince asserts and fixture creds are expected there. - Typing discipline:
ANN(typed signatures — the mypy--strictbaseline),TC(TYPE_CHECKINGguards, withruntime-evaluated-base-classescarved out for Pydantic/Settings — see §3),PYI(stub-file quality). - Modernization:
UP(pyupgrade — see §1),FA(future annotations),FURB(refurb),PERF(perflint). - Style & structure:
I(isort),N(pep8-naming),C4(comprehensions),SIM(simplify),RET/RSE(control-flow and raise style),PIE,PTH(pathlib overos.path— see §9),ISC,TID,A(no builtin shadowing). - Complexity & size:
PL(pylint subset), thresholds tuned in[lint.pylint](max-args = 7,max-branches = 12,max-returns = 6,max-statements = 50) — split functions instead of suppressing. - Test style:
PT(flake8-pytest-style) — fixture/mark parenthesis conventions tuned in[lint.flake8-pytest-style](see §7). - Docs & dead code:
D(pydocstyle, Google convention — see §8),ERA(eradicate — no commented-out code). - Test-file relaxations:
per-file-ignoresdropsANN,D,S101,PLR2004,SLF001,INP001undertests/**— type hints and docstrings on test functions add noise without value; asserts, magic numbers, and private-member access are the point of a test. - Auto-fix guardrails:
fixable = ["ALL"], butunfixableexcludesERA,F401,F841— never let--fixsilently delete commented-out code or unused imports/locals; those need a human decision.
- Correctness & bugs:
- Type checking:
mypy --strictas the baseline — noAny-by-default escape hatches. Drop-in template:assets/mypy.ini— copy to your project root (or fold[mypy]intopyproject.toml's[tool.mypy]) and setpackagesto your package name.- Beyond
--strict:warn_unreachable,warn_redundant_casts,warn_unused_ignores,strict_equality,extra_checks— catch dead branches, stale# type: ignorecomments, and cross-type equality bugs that--strictalone misses. - Per-module overrides: relax
disallow_untyped_defsundertests.*(fixtures and@pytest.mark.parametrizeroutinely defeat full inference);ignore_errorsundermigrations.*(Alembic-generated, not hand-typed); scopeignore_missing_importsto named untyped dependencies instead of a blanket override, which would silently swallow first-party import typos too. - CI parity: run
ruff check,ruff format --check, andmypyas three separate, mandatory CI gates — a formatting fix should never ride along with a type fix in the same commit.
- Beyond
- Test:
pytest 9+pytest-asynciofor async paths.pytest-covfor coverage gating in CI.
Canonical libraries
See STACK.md for the full pinned list — pydantic, pydantic-settings, fastapi, uvicorn, httpx, pytest, pytest-asyncio, mypy, ruff, uv, typer, psycopg, alembic.