Pythurion helps coding agents write Python you’ll want to maintain: correct, minimal, and idiomatic.
An always-on Python engineering discipline for coding agents:
- inspect the real context;
- establish the behavioral contract;
- for performance work, locate the measured cause and define the budget and guardrails;
- reproduce failures before fixing them;
- solve the shared cause with the smallest coherent mechanism;
- avoid speculative abstractions and unrelated churn;
- verify every claim with focused checks and observed evidence.
npx skills add -g arlegotin/pythurionPythurion is designed to activate automatically whenever your agent works with Python code, tests, project configuration, or Python performance work. It favors modern Python 3.12+ practices, the smallest design that fits, explicit risk boundaries, representative measurement, and executable evidence.
Use a function until state or lifecycle earns a class.
# Before
class Slugifier:
def __call__(self, text: str) -> str:
return text.lower().replace(" ", "-")
slugify = Slugifier()
# After
def slugify(text: str) -> str:
return text.lower().replace(" ", "-")When rows are a stable tuple containing only exact built-in str keys and int amounts, aggregate once rather than running a full scan for each input row. This preserves integer totals and first-seen category order.
# Before: rescans every row for every input row
def totals_by_category(
rows: tuple[tuple[str, int], ...],
) -> dict[str, int]:
return {
category: sum(
amount
for row_category, amount in rows
if row_category == category
)
for category, _ in rows
}
# After: one pass
def totals_by_category(
rows: tuple[tuple[str, int], ...],
) -> dict[str, int]:
totals: dict[str, int] = {}
for category, amount in rows:
totals[category] = totals.get(category, 0) + amount
return totalsThis changes repeated full scans into one pass. Verify the gain at representative row counts and keep the rewrite only when it meets the target budget.
When values is a stable tuple containing only exact built-in integers and only the first even value matters, stop after that match instead of building every result.
# Before: evaluates every value and materializes every match
def first_even(values: tuple[int, ...]) -> int | None:
matches = [value for value in values if value % 2 == 0]
return matches[0] if matches else None
# After: evaluates only through the first match
def first_even(values: tuple[int, ...]) -> int | None:
return next((value for value in values if value % 2 == 0), None)Measure representative input sizes and match positions. Use this pattern only when evaluating later items is not part of the contract.
Accept the weakest useful interface and enforce equal lengths.
from collections.abc import Iterable
# Before
def prices(names: list[str], values: list[int]) -> dict[str, int]:
return dict(zip(names, values))
# After
def prices(names: Iterable[str], values: Iterable[int]) -> dict[str, int]:
return dict(zip(names, values, strict=True))Catch only the failure you can translate, and preserve its cause.
from pathlib import Path
# Before
def load_config(path: Path) -> str | None:
try:
return path.read_text()
except Exception:
return None
# After
class ConfigError(RuntimeError):
pass
def load_config(path: Path) -> str:
try:
return path.read_text()
except OSError as error:
raise ConfigError(f"cannot read {path}") from errorParameterize input and make both transaction and connection lifetimes explicit.
from contextlib import closing
import sqlite3
# Before
def add_label(path: str, label: str) -> None:
connection = sqlite3.connect(path)
connection.execute(f"INSERT INTO labels(value) VALUES ('{label}')")
connection.commit()
# After
def add_label(path: str, label: str) -> None:
with closing(sqlite3.connect(path)) as connection:
with connection:
connection.execute(
"INSERT INTO labels(value) VALUES (?)",
(label,),
)Own a small fail-together task set so errors and cancellation clean up the group.
import asyncio
# Before
async def refresh() -> None:
asyncio.create_task(refresh_users())
asyncio.create_task(refresh_orders())
# After
async def refresh() -> None:
async with asyncio.TaskGroup() as group:
group.create_task(refresh_users())
group.create_task(refresh_orders())Use aware UTC instants for persisted or cross-host time.
from datetime import UTC, datetime, timedelta
# Before
expires_at = datetime.now() + timedelta(minutes=5)
# After
expires_at = datetime.now(UTC) + timedelta(minutes=5)