Skip to content
v0.1.0latest release21 skills2 categories3 sub-categories22 tested scripts68 reference docsContact us

Modern Python by Version

Reference for python-idiomatic-code-reviewer · 383 lines

Programming / Pythonv1.0.0Updated OCT 20 20265 references1 script

Features from Python 3.8 to 3.13 that change what "idiomatic" means. Before suggesting a rewrite, check the project's minimum version (requires-python in pyproject.toml, python_requires in setup.cfg, the Docker base image, or CI matrix). A suggestion that does not run on the floor version is a bug, not an improvement.

Contents

  1. Version gate quick table
  2. 3.8
  3. 3.9
  4. 3.10
  5. 3.11
  6. 3.12
  7. 3.13
  8. Deprecations and removals to flag
  9. Upgrade playbook

Version gate quick table

FeatureMin versionReplaces
Walrus :=3.8assign-then-test
Positional-only params /3.8*args hacks
f"{x=}" debug specifier3.8f"x={x}"
functools.cached_property3.8hand-rolled lazy attributes
typing.Protocol, Literal, TypedDict, Final3.8typing_extensions
Builtin generics list[int]3.9typing.List[int]
Dict union a | b, a |= b3.9{**a, **b}, a.update(b)
str.removeprefix/removesuffix3.9slicing by len(prefix)
functools.cache3.9lru_cache(maxsize=None)
zoneinfo3.9pytz
match statement3.10if/elif chains on structure
X | Y unions at runtime, isinstance(x, int | str)3.10Union[X, Y], tuples
Parenthesised context managers3.10backslashes / nested with
zip(strict=True)3.10manual length checks
itertools.pairwise3.10zip(xs, xs[1:])
dataclass(slots=True, kw_only=True)3.10manual __slots__
asyncio.TaskGroup, asyncio.timeout3.11gather + manual cancel, wait_for
ExceptionGroup, except*3.11ad-hoc error aggregation
tomllib3.11toml package (read-only)
enum.StrEnum3.11class E(str, Enum)
typing.Self, LiteralString, Required/NotRequired3.11TypeVar bound to class
datetime.UTC3.11timezone.utc
Exception notes err.add_note()3.11re-raising with longer messages
type statement, def f[T](x: T) generics3.12TypeVar declarations
itertools.batched3.12slicing helpers
f-string grammar relaxed (nested same quotes)3.12quote juggling
@typing.override3.12comments
pathlib.Path.walk3.12os.walk
copy.replace, free-threaded build (experimental), warnings.deprecated3.13dataclasses.replace for more types, custom decorators

Python 3.8

Walrus operator

python
while (batch := queue.get_batch(64)):
    process(batch)

if (n := len(tokens)) > MAX_TOKENS:
    raise ValueError(f"prompt has {n} tokens (max {MAX_TOKENS})")

clean = [y for x in raw if (y := normalize(x))]

Positional-only parameters

python
def cosine_similarity(a, b, /, *, eps: float = 1e-8) -> float: ...

Self-documenting f-strings

python
logger.debug(f"{batch_size=} {lr=:.1e}")      # "batch_size=32 lr=3.0e-04"

(Use this for quick debugging only; for log calls prefer %-style arguments.)

functools.cached_property

python
from functools import cached_property

class Corpus:
    def __init__(self, docs: list[str]):
        self.docs = docs

    @cached_property
    def vocabulary(self) -> set[str]:
        return {tok for doc in self.docs for tok in doc.split()}

Python 3.9

Builtin and collections generics in annotations

python
from collections.abc import Iterable, Iterator, Mapping, Sequence

def batch(items: Sequence[str], size: int) -> Iterator[list[str]]: ...
def merge(a: Mapping[str, float], b: Mapping[str, float]) -> dict[str, float]: ...

Import abstract types from collections.abc, not typing (the typing aliases are deprecated).

Dict union

python
params = DEFAULT_GEN_PARAMS | {"temperature": 0.2}

removeprefix / removesuffix

python
deployment = name.removeprefix("azure/")
stem = filename.removesuffix(".jsonl")

functools.cache — unbounded memoisation, simpler than lru_cache(maxsize=None).

zoneinfo

python
from zoneinfo import ZoneInfo
from datetime import datetime
local = datetime.now(ZoneInfo("Europe/Berlin"))

X | None in annotations on 3.9 works only with from __future__ import annotations (annotations become strings). It fails at runtime anywhere annotations are evaluated (pydantic v1 on 3.9, typing.get_type_hints). Use Optional[X] if pydantic or FastAPI must evaluate them on 3.9.

Python 3.10

Structural pattern matching

python
def handle(event: dict) -> None:
    match event:
        case {"type": "content_block_delta", "delta": {"text": str(text)}}:
            sink.write(text)
        case {"type": "message_stop"}:
            sink.close()
        case {"type": "error", "error": {"message": msg}}:
            raise ProviderError(msg)
        case _:
            pass

Class patterns with dataclasses:

python
from dataclasses import dataclass

@dataclass
class ToolCall:
    name: str
    args: dict

match call:
    case ToolCall(name="search", args={"query": str(q)}):
        return search(q)
    case ToolCall(name="calculator", args={"expr": str(e)}):
        return calculate(e)
    case ToolCall(name=other):
        raise UnknownTool(other)

When not to use match: plain value → function maps (use a dict), range checks (use if/elif), or code that must run on 3.9.

Union types at runtime

python
def to_list(x: str | list[str]) -> list[str]:
    return [x] if isinstance(x, str) else x
isinstance(value, int | float)

Parenthesised context managers

python
with (
    open("prompts.jsonl", encoding="utf-8") as src,
    open("completions.jsonl", "w", encoding="utf-8") as dst,
):
    ...

zip(strict=True) — raises ValueError when lengths differ; use it whenever ids and vectors, or predictions and labels, must line up.

itertools.pairwise

python
from itertools import pairwise
deltas = [b - a for a, b in pairwise(timestamps)]

Dataclass slots and kw_only

python
@dataclass(slots=True, kw_only=True, frozen=True)
class SamplingParams:
    temperature: float = 0.0
    top_p: float = 1.0
    max_tokens: int = 512

Better error messages — no code change, but suggests reviewers can trust tracebacks more.

encoding="locale" and EncodingWarning (python -X warn_default_encoding) help find missing encoding= arguments.

Python 3.11

TaskGroup (structured concurrency)

python
async def retrieve_all(queries: list[str]) -> list[list[Doc]]:
    async with asyncio.TaskGroup() as tg:
        tasks = [tg.create_task(retrieve(q)) for q in queries]
    return [t.result() for t in tasks]

If any task fails, the others are cancelled and an ExceptionGroup is raised.

asyncio.timeout

python
async with asyncio.timeout(15):
    reply = await llm.chat.completions.create(model=model, messages=messages)

ExceptionGroup and except*

python
try:
    results = await retrieve_all(queries)
except* TimeoutError as eg:
    logger.warning("%d retrievals timed out", len(eg.exceptions))
    raise
except* ValueError as eg:
    for err in eg.exceptions:
        logger.error("bad query: %s", err)
    raise

Exception notes

python
try:
    record = Example.model_validate(row)
except ValidationError as err:
    err.add_note(f"row {i} in {path}")
    raise

tomllib

python
import tomllib
with open("pyproject.toml", "rb") as f:     # binary mode is required
    config = tomllib.load(f)

StrEnum

python
from enum import StrEnum

class Role(StrEnum):
    SYSTEM = "system"
    USER = "user"
    ASSISTANT = "assistant"

{"role": Role.USER, "content": "hi"}   # serialises as "user"

typing.Self

python
from typing import Self

class PromptBuilder:
    def __init__(self) -> None:
        self.parts: list[str] = []

    def add(self, text: str) -> Self:
        self.parts.append(text)
        return self

datetime.UTC — datetime.now(UTC) (alias of timezone.utc).

Performance — 3.11 is 10–60% faster for pure-Python code; an upgrade can beat micro-optimisation.

Python 3.12

Type parameter syntax and type aliases

python
def first[T](items: list[T]) -> T:
    return items[0]

class Cache[K, V]:
    def __init__(self) -> None:
        self._data: dict[K, V] = {}

type Embedding = list[float]
type Retriever = Callable[[str, int], list[Doc]]

Only on 3.12+; on older versions keep TypeVar and TypeAlias.

itertools.batched

python
from itertools import batched
for batch in batched(ids, 100):            # tuples of up to 100
    index.upsert(list(batch))

Relaxed f-strings (PEP 701)

python
f"{', '.join(f'{k}={v}' for k, v in params.items())}"
f"{payload["model"]}"                      # same quotes inside: 3.12+ only

Avoid same-quote nesting if the code must run on 3.11 or older.

typing.override

python
from typing import override

class OpenAIEmbedder(BaseEmbedder):
    @override
    def embed(self, texts: list[str]) -> list[list[float]]: ...

Path.walk

python
for dirpath, dirnames, filenames in Path("corpus").walk():
    dirnames[:] = [d for d in dirnames if not d.startswith(".")]

Deprecated: datetime.utcnow() and utcfromtimestamp() (PY027), asyncio.get_event_loop() without a running loop warns.

Python 3.13

  • copy.replace() works on dataclasses, namedtuples and other types implementing __replace__: cfg2 = copy.replace(cfg, temperature=0.7).
  • warnings.deprecated decorator (PEP 702) for API deprecations recognised by type checkers.
  • Free-threaded build (python3.13t, experimental): the GIL can be disabled. Thread-safety of shared state matters more; do not rely on the GIL for atomicity.
  • Experimental JIT — no idiom change.
  • Improved REPL with multi-line editing and colour.
  • Removed modules (PEP 594): cgi, cgitb, crypt, imghdr, mailcap, nntplib, pipes, sndhdr, telnetlib, uu, xdrlib, audioop, chunk, msilib, nis, ossaudiodev, spwd, sunau, and lib2to3. Flag imports of them.
  • typing.TypeIs for precise narrowing functions; ReadOnly for TypedDict items.

Deprecations and removals to flag

PatternStatusReplacement
typing.List, Dict, Tuple, SetDeprecated aliases since 3.9list, dict, tuple, set
typing.Iterable etc.Deprecated aliasescollections.abc.Iterable
datetime.utcnow()Deprecated 3.12datetime.now(timezone.utc)
asyncio.get_event_loop() outside a loopDeprecated 3.10–3.12asyncio.run(main()), asyncio.get_running_loop()
@asyncio.coroutine, yield from coroutinesRemoved 3.11async def / await
distutilsRemoved 3.12setuptools, sysconfig, shutil
impRemoved 3.12importlib
asynchat, asyncore, smtpdRemoved 3.12asyncio, aiosmtpd
pkg_resourcesDeprecated (setuptools)importlib.metadata, importlib.resources
ast.Num, ast.Str, ast.BytesRemoved 3.14ast.Constant
locale.getdefaultlocale()Deprecated 3.11locale.getlocale()
unittest.makeSuite, findTestCasesRemoved 3.13unittest.TestLoader

Upgrade playbook

  1. Raise the floor in pyproject.toml (requires-python = ">=3.11") and CI.
  2. Run pyupgrade --py311-plus (or ruff check --select UP --fix) for mechanical rewrites: typing aliases, f-strings, super(), set literals.
  3. Run python scripts/review_idioms.py src/ and handle what remains: match candidates (PY029), utcnow (PY027), typing generics (PY031).
  4. Adopt structural features deliberately, one PR each: TaskGroup for fan-out, asyncio.timeout for deadlines, StrEnum for roles/states, batched for batching helpers.
  5. Re-run tests under -W error::DeprecationWarning to surface remaining deprecated calls.
More in Python

python-beginner-concept-tutor

Teach Python concepts to beginners heading into AI engineering (data scientists, analysts, students, career switchers): plain-language definitions, analogies, minimal examples, line-by-line walkthroughs, common mistakes, check-your-understanding questions and mini exercises; Socratic hints before solutions; explaining tracebacks; mutable vs immutable, references and aliasing, is vs ==, integer division, floating point, slicing and off-by-one, indentation errors; a staged learning path from variables to calling an LLM API, embeddings and a tiny RAG. Ships a script that lists the concepts a snippet uses in learning order and traces execution step by step like Python Tutor.

python-comprehension-refactorer

Refactor Python loops into list, dict and set comprehensions and generator expressions, and unroll comprehensions that are too clever: filtering, mapping, nested-loop flattening, conditional expressions, walrus in comprehensions, dict inversion, grouping with defaultdict/Counter, lazy generators for sum/any/all/max, streaming large files, itertools (chain, islice, groupby, batched, pairwise, accumulate, product), scoping rules, side-effect anti-patterns and performance (when map/filter or numpy/pandas vectorization win). AI-engineering focus: building prompt lists, batching texts for embeddings, filtering retrieved chunks by score, id->embedding dicts, flattening tool-call results, lazy dataset streaming and token counting.

python-context-manager-writer

Write correct Python context managers and find resource-handling bugs — __enter__/__exit__ semantics and exception suppression, contextlib (contextmanager, asynccontextmanager, ExitStack, AsyncExitStack, closing, aclosing, suppress, redirect_stdout, nullcontext, chdir), async with, re-entrancy, leaked files/connections/clients/pools/locks, and AI-engineering recipes: torch.no_grad/inference_mode/autocast, eval()/train() switches, GPU cache cleanup, token and latency meters, tracing spans, temp env vars, rate-limit semaphores, httpx/aiohttp client lifecycle, FastAPI lifespan, DB/vector-store transactions, atomic checkpoint writes, seeds and agent sandboxes.

python-dataclass-converter

Convert boilerplate Python classes, dicts, tuples and JSON payloads into dataclasses, or into pydantic v2 BaseModel, TypedDict, NamedTuple, attrs or msgspec when that fits better. Covers every @dataclass option (frozen, slots, kw_only, order, unsafe_hash, match_args, weakref_slot), field() (default_factory, init=False, repr, compare, hash, metadata), __post_init__, InitVar, KW_ONLY, inheritance and default ordering, replace(), asdict deep-copy pitfalls, hashing/equality, serialization and validation. AI focus: LLM request/response and chat Message types, RAG chunks with embeddings (numpy eq gotcha), eval records, configs, typed OpenAI/Anthropic JSON.

python-exception-handling-designer

Design and review Python exception handling: custom exception hierarchies with retryable flags, what to catch where, EAFP vs LBYL, chaining with raise ... from, else/finally, logger.exception, add_note, ExceptionGroup and except* with asyncio.TaskGroup, warnings, asserts vs validation, FastAPI error translation, retries with backoff and jitter, timeouts, cancellation, pytest.raises, plus LLM-app failures: 429 rate limits with retry-after, 5xx overloaded, context-length exceeded, content filters, broken streams, malformed JSON repair loops, tool-call errors fed back to the model, partial embedding batch failures, CUDA OOM batch halving, vector DB outages, circuit breakers and model fallbacks.

python-type-hints-annotator

Add accurate, modern type hints to Python and fix mypy / pyright (pyrefly, ty) errors: builtin generics, X | None, TypeAlias and the 3.12 type statement, TypeVar and PEP 695 generics, Protocol, Callable / ParamSpec / Concatenate, TypedDict (Required, NotRequired, ReadOnly), Literal, Final, ClassVar, overload, Annotated, TypeGuard / TypeIs, Self, Never, TypeVarTuple, async types, Iterable vs Sequence vs Mapping, TYPE_CHECKING, stubs, py.typed, gradual strictness. AI-engineering focus: typed LLM message dicts and tool schemas, pydantic vs TypedDict, numpy NDArray, torch / jaxtyping shapes, streaming generators, async clients, callbacks, generic VectorStore protocols, typed config. Ships an AST audit: coverage gate, legacy-typing rewrites, inferred types.