Skip to content
Development
Agent

python-modern-code

Load this file before you write or edit any Python code. Each rule fixes a failure measured in generated code that passed its tests but failed `ruff`, `mypy --strict`, packaging, or review. Examples use made-up domains (weather stations, parcels, telescopes, library loans,

BOOST
From plugin
vexjoy-agent
425198 skills198 agents12 commands78 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

Load this file before you write or edit any Python code. Each rule fixes a failure measured in generated code that passed its tests but failed `ruff`, `mypy --strict`, packaging, or review. Examples use made-up domains (weather stations, parcels, telescopes, library loans,

Agent definition

python-modern-code.md

Writing Modern Python (3.12–3.14)

Load this file before you write or edit any Python code. Each rule fixes a failure measured in generated code that passed its tests but failed `ruff`, `mypy --strict`, packaging, or review. Examples use made-up domains (weather stations, parcels, telescopes, library loans, recipes, greenhouse sensors). Copy the pattern, never the names or wording.

Build everything the task asks for and nothing it doesn't: no extra endpoints, flags, dependencies, or files.

When you have no tools and must return files as text, every line inside a file block is code, comments, or data. Checks you would run become reading checks (section 14); you never narrate them. Measured: in 19 of 28 guided outputs across two rounds, a sentence about running checks ended up as the last line of a `.py` file, which is a syntax error.

1. Pick the Python version first

Read the version floor before writing code, and use only features at or below it.

grep -n 'requires-python' pyproject.toml   # the floor, e.g. ">=3.12"
python3 --version                          # the interpreter tests run on
  • Existing project: its `requires-python` is the floor.
  • New project with a stated runtime ("runs on 3.12"): set `requires-python = ">=3.12"` and stay at or below 3.12.
  • New project with no stated runtime: use the local `python3 --version` as the floor. Current stable is 3.14 (3.15 is due October 2026).

| Feature | Version | Use | |---|---|---| | `asyncio.TaskGroup`, `asyncio.timeout()`, `except*`, `ExceptionGroup`, `typing.Self`, `datetime.UTC`, `tomllib`, `enum.StrEnum` | 3.11 | Default for new code | | `class Box[T]:`, `def f[T](x: T) -> T:`, `type Alias = ...` (PEP 695), `typing.override`, `itertools.batched`, `Path.walk()`, tarfile `filter=` | 3.12 | Default when the floor is 3.12+ | | `warnings.deprecated`, `copy.replace()`, `typing.TypeIs`, `typing.ReadOnly`, type-parameter defaults, `asyncio.Queue.shutdown()` | 3.13 | Only when the floor is 3.13+ | | Deferred annotations (PEP 649), t-strings `t"..."`, `except A, B:` without brackets, `compression.zstd`, `Path.copy()`/`move()`, `uuid.uuid7()`, tarfile default filter `"data"` | 3.14 | Only when the floor is 3.14 |

Removed modules: `distutils`, `imp`, `asyncore`, `asynchat` (3.12); `cgi`, `telnetlib`, `crypt`, `pipes`, `imghdr` and the rest of PEP 594 (3.13). Deprecated: `datetime.utcnow()` and `utcfromtimestamp()` (3.12).

2. `pyproject.toml` that builds

Measured: 10 of 14 unguided projects declared a build backend that does not exist (`setuptools.backends._legacy:_Backend`, `hatchling.backends`), so `pip install .` failed. Copy one of these two backends exactly:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# or: requires = ["setuptools>=77"] with build-backend = "setuptools.build_meta"

The rest of a new project's file (flat layout: package folder next to `pyproject.toml`, tests in `tests/`):

[project]
name = "shelfscan"
version = "0.1.0"
description = "Scan library shelves for overdue loans."
requires-python = ">=3.12"
dependencies = []

[project.optional-dependencies]
dev = ["pytest>=8", "pytest-asyncio>=1.0", "mypy>=1.10", "ruff>=0.6"]

[tool.ruff]
line-length = 120
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "S", "ASYNC", "PTH", "PT", "C4", "DTZ", "RET", "FURB", "ANN"]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101", "ANN"]

[tool.mypy]
strict = true
python_version = "3.12"

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"                          # only when pytest-asyncio is a dependency
asyncio_default_fixture_loop_scope = "function"

Keep `target-version` and `python_version` equal to the `requires-python` floor.

Write code the way `ruff format` would, or the format check fails. Measured: 6 of 14 round-1 outputs failed the format check, on line wrapping.

  • A call, signature, or literal that fits on one line within `line-length` (120) stays on one line. Don't hand-wrap it at 80 or 88 columns; the formatter joins it back.
  • When it doesn't fit, put one item per line and end with a trailing comma. The trailing comma tells the formatter to keep it exploded.
# BAD at line-length 120: fits on one line, so ruff format joins it
rows = self._conn.execute(
    "SELECT id, book FROM loans WHERE id = ?", (loan_id,)
).fetchall()

# GOOD
rows = self._conn.execute("SELECT id, book FROM loans WHERE id = ?", (loan_id,)).fetchall()

# GOOD: too long for one line, so one item per line with a trailing comma
summary = summarize_loans(
    loans=active_loans,
    overdue_after=timedelta(days=21),
    include_labels=("reserve", "interlibrary"),
)

3. Typing

Measured: 6 of 14 unguided outputs imported `List`, `Optional`, `Sequence`, or `Callable` from `typing`; 7 of 14 failed `mypy --strict`, mostly on bare generics, `Any` returns, and unannotated special methods.

| Write | Never write | |---|---| | `list[int]`, `dict[str, float]`, `tuple[str, ...]` | `List[int]`, `Dict`, `Tuple` from `typing` | | `X \| None`, `A \| B` | `Optional[X]`, `Union[A, B]` | | `from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence, Awaitable` | the same names from `typing` | | `class Shelf[T]:` and `def first[T](...)` (3.12+) | `T = TypeVar("T")` + `Generic[T]` | | `type StationReadings = dict[str, list[float]]` (3.12+) | `StationReadings: TypeAlias = ...` | | `dict[int, Recipe]`, `Shelf[str]` | bare `dict`, bare `Shelf` in any annotation | | `datetime.UTC`, builtin `TimeoutError` | `timezone.utc`, `asyncio.TimeoutError` |

Rules:

  • Annotate every parameter and return, including `-> None` on `__init__` and full `__exit__`/`__aexit__` signatures (section 6).
  • `json.loads`, `response.json()`, `os.environ`, and untyped libraries return `Any`. Narrow with `isinstance` before returning a typed value; `mypy --strict` rejects `return data["x"]` from a function declared `-> float` (`n
Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. VexJoy Agent connects plain-English requests to specialist agents, skills, and workflows. /do selects the knowledge and tools needed for your task.

Get the whole plugin

Other agents on vexjoy-agent.