Modern Python Idioms skill for Codex

Pythonic rewrites your minimum version can run. Codex reads the project's minimum Python version first, lets Ruff's pyupgrade and simplify rules make the mechanical edits, then hand-rewrites only where an idiom makes code shorter or safer: dict records into slotted dataclasses, shape checks into match statements, os.path into pathlib, try/finally into context managers. Tests run after each step and any hunk that changes behavior is reverted.

  • Languages
  • $python-modern-idioms
  • 188 lines
  • 2 files
  • ~1,900 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- python-modern-idioms

Installs to ~/.agents/skills/python-modern-idioms/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $python-modern-idioms, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Rewrites Python into current idioms for 3.10 to 3.14 - dataclasses with slots and kw_only, structural pattern matching, pathlib instead of os.path, context managers and ExitStack, enum.StrEnum, itertools.batched, zoneinfo, tomllib and f-string debugging - and automates the safe parts with Ruff pyupgrade rules. Use when a user asks to modernize or clean up old Python, replace dict-based records with typed classes, convert if/elif ladders over shapes of data, or review code for Pythonic style.

Use it when

  • A codebase still carries Python 2 or 3.6-era patterns
  • You raised the minimum Python version and want to use what it allows
  • Records are passed around as loosely shaped dicts or tuples
  • A style review asks for more Pythonic code

Use something else when

  • You want to configure Ruff rules and formatting for the repo: use ruff-setup
  • The change is a larger structural refactor beyond idioms: use safe-refactoring

How to check the result

Ruff reports no remaining pyupgrade fixes, the test suite passes unchanged, and the code compiles on the minimum supported interpreter.

The mistake it prevents

Using a feature newer than requires-python because the developer's own interpreter accepts it, which breaks users on the oldest supported version.

~/.agents/skills/python-modern-idioms/SKILL.md

--- name: python-modern-idioms description: Rewrites Python into current idioms for 3.10 to 3.14 - dataclasses with slots and kw_only, structural pattern matching, pathlib instead of os.path,… license: MIT ---

Done well means: each rewrite uses a feature the project’s minimum Python actually has, behavior is unchanged (tests prove it), mechanical upgrades are done by a tool, and the hand-written changes make the code shorter or safer, never just different.

Before changing anything

  1. Read requires-python in pyproject.toml (or python_requires, CI matrix, Dockerfile base image). The lowest supported version caps every choice below.
  2. Read AGENTS.md for style rules; respect existing conventions over personal taste.
  3. Confirm tests exist for the code you will touch. If not, add characterization tests first or limit the change to tool-driven rewrites.
  4. Let the tool do the mechanical part before any manual edit:
ruff check --select UP,SIM,PTH,C4,RUF --target-version py311 --diff src/   # preview
ruff check --select UP,SIM,C4 --fix src/                                     # apply safe fixes

UP (pyupgrade) handles typing.List to list, Optional[X] to X | None, old string formatting, super() arguments and more. PTH flags os.path calls but has no autofix; those are manual. Commit tool output separately from hand edits.

Version map

IdiomMinReplaces
match statements3.10isinstance / key-probing ladders
dataclass(slots=True, kw_only=True)3.10hand-written __slots__, positional-arg confusion
zip(a, b, strict=True)3.10silent truncation on length mismatch
contextlib.chdir, tomllib, enum.StrEnum, datetime.UTC, ExceptionGroup3.11os.chdir try/finally, third-party toml, str, Enum mixins
itertools.batched, PEP 695 generics, Path.walk3.12manual chunking loops, os.walk
copy.replace() for dataclasses and namedtuples3.13dataclasses.replace only
Template strings (t"..."), lazy annotations3.14from __future__ import annotations

Never introduce a feature above the minimum, even if CI’s newest interpreter accepts it.

Records: choose the right container

DataUse
Internal value object, methods, defaults@dataclass(slots=True, frozen=True)
Many small immutable tuples, unpacked positionallytyping.NamedTuple
Shape of a JSON dict you do not own, stays a dictTypedDict
Untrusted input needing validation and coercionPydantic model (see $pydantic-v2)
A bag of config with 2 keys used onceA plain dict is fine
from dataclasses import dataclass, field
from datetime import datetime, UTC

@dataclass(slots=True, kw_only=True)
class Job:
    name: str
    retries: int = 3
    tags: list[str] = field(default_factory=list)
    created: datetime = field(default_factory=lambda: datetime.now(UTC))

    def __post_init__(self) -> None:
        if self.retries < 0:
            raise ValueError("retries must be >= 0")

kw_only=True prevents Job("x", 5) call sites that break when fields are reordered. slots=True cuts memory and blocks typo attributes; it breaks code that sets ad-hoc attributes or relies on __dict__, so search for that first.

Structural pattern matching

Use match when branching on the shape of data (tuples, dicts from JSON, AST-like objects). Do not use it as a switch over plain constants where a dict lookup is clearer.

def handle(event: dict) -> str:
    match event:
        case {"type": "click", "pos": [x, y]}:
            return f"click at {x},{y}"
        case {"type": "key", "key": str(k)} if k.isprintable():
            return f"key {k}"
        case {"type": "resize", "size": {"w": int(w), "h": int(h)}}:
            return f"resize {w}x{h}"
        case _:
            return "ignored"

Traps:

  • A bare name in a case (case status:) captures and always matches. Compare constants through a dotted name (case Status.OK:) or a literal.
  • Mapping patterns match if the listed keys exist; extra keys are allowed.
  • Class patterns with positional args need __match_args__ (dataclasses generate it).

Paths and files

from pathlib import Path

root = Path(__file__).resolve().parent
config = root / "config" / "app.toml"
for csv_path in sorted(root.glob("data/**/*.csv")):
    text = csv_path.read_text(encoding="utf-8")
out = root / "build"
out.mkdir(parents=True, exist_ok=True)
  • Always pass encoding="utf-8" for text I/O; the platform default differs on Windows until UTF-8 mode is default (3.15).
  • Path.read_text loads the whole file; stream large files with with path.open() as f:.
  • Use shutil for copy/move trees; Path.rename fails across filesystems.

Context managers

from contextlib import ExitStack, contextmanager, suppress

@contextmanager
def timed(label: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        log.info("%s took %.3fs", label, time.perf_counter() - start)

with ExitStack() as stack:                    # variable number of resources
    files = [stack.enter_context(open(p, encoding="utf-8")) for p in paths]

with suppress(FileNotFoundError):
    tmp.unlink()                              # or tmp.unlink(missing_ok=True)

Every open, lock, connection and temp dir gets a with. A try/finally that only releases one resource is a context manager waiting to be written.

Smaller idioms worth applying

OldNew
for i in range(len(xs)): x = xs[i]for i, x in enumerate(xs)
manual chunk loopfor chunk in itertools.batched(items, 500) (3.12)
zip(a, b) that must alignzip(a, b, strict=True)
d.has_key, k in d.keys()k in d
x = d[k] if k in d else defaultd.get(k, default)
print("x=", x) debuggingprint(f"{x=}")
pytz.timezone("Europe/Paris")zoneinfo.ZoneInfo("Europe/Paris")
datetime.utcnow() (deprecated 3.12)datetime.now(UTC)
class Color(str, Enum)class Color(StrEnum)
@lru_cache(maxsize=None)@functools.cache
s[len(prefix):] if s.startswith(prefix)s.removeprefix(prefix)
while True: line = f.readline(); if not line: breakwhile line := f.readline():

Walrus (:=) is for avoiding a duplicated call in a condition; do not use it to compress readable code into one line.

Gotchas

  • datetime.now(UTC) returns an aware datetime; comparing it with existing naive values raises TypeError. Migrate a whole module’s datetimes together.
  • StrEnum members format as their value (f"{Color.RED}" gives "red"), unlike mixin enums on some versions. Check string formatting in logs and URLs after converting.
  • slots=True makes the decorator return a new class, so zero-argument super() and __class__ inside its methods can fail on older interpreters. Test such methods on the minimum version, or pass super(Cls, self) explicitly.
  • functools.cache on methods keeps self alive forever; use cached_property or a module function instead.
  • Ruff UP fixes respect target-version; a wrong target rewrites code into syntax older interpreters cannot parse.

Verification

ruff check --select UP,SIM,C4 src/          # nothing left the tool would change
python -m pytest -q                         # behavior unchanged
python3.11 -m compileall -q src/            # parses on the minimum version (use the real minimum)

If a test fails after a rewrite, revert that hunk rather than adjusting the test; an idiom change is not allowed to change behavior.

Output

Summarize in the PR: tool-applied rules (with counts), hand rewrites grouped by idiom, and any rewrite skipped because the minimum Python version does not support it.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Modern Python Idioms"
  short_description: "Pythonic rewrites your minimum version can run"
  default_prompt: "$python-modern-idioms Modernize this Python 2-era module for Python 3.12"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc