Python · Python 3.12 to 3.14

Python 3.12-3.13: Type Parameters, f-strings and Removals

Inline generics and type aliases, the f-string grammar change that sets a minimum version, @override, batched, copy.replace and @deprecated - and the standard-library modules that 3.12 and 3.13 removed.

  • 5 min read
  • 12 practice questions
On this page
  1. Description
  2. Type parameter syntax (3.12)
  3. f-strings, @override and batched (3.12)
  4. Python 3.13
  5. What's gone
  6. Which tool for which job?
  7. Common mistakes
  8. Check yourself
  9. Key takeaways
  10. Quick revise
  11. Practice

Description

Python 3.12 and 3.13 tidied up a lot of everyday code: generics without TypeVar boilerplate, f-strings that behave like normal code, @override, itertools.batched, copy.replace and @deprecated. They also removed a set of old standard-library modules, and laid the groundwork for running without the GIL. Some of these are new syntax, and that matters as much as what they do: a file using new syntax can't even be read by an older Python.

Here's the kind of bug we mean. A library adds a convenience in a minor release:

log.info(f"loaded {", ".join(names)}")

It works on the maintainer's Python 3.12 - and every user on 3.11 gets a SyntaxError when the module is imported, before a single function runs. Python 3.12 relaxed f-string grammar (PEP 701) so the expression can reuse the enclosing quotes; 3.11's parser can't read the file at all.

We'll go from simple to more technical:

  1. Type parameter syntax: def first[T], class Box[T] and type aliases.
  2. f-strings, @override and batched in 3.12.
  3. What 3.13 added: a new REPL, copy.replace, @deprecated and more.
  4. What 3.12 and 3.13 removed, and how to upgrade safely.

Type parameter syntax (3.12)

Before 3.12, a generic function needed a module-level type variable:

from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T: ...

PEP 695 puts the type parameters in the definition:

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

class Box[T]:
    def __init__(self, item: T) -> None:
        self.item = item

def largest[T: (int, float)](xs: list[T]) -> T: ...      # constrained
def save[T: Model](obj: T) -> T: ...                     # bounded

type Pair = tuple[int, int]
type Tree = dict[str, Tree] | None                        # refers to itself - no quotes needed
  • The parameters are scoped to the function or class (first.__type_params__ is (T,)), so there's no shared global T and no naming ceremony.
  • type statements create a TypeAliasType whose value is evaluated lazily - on first access to Pair.__value__ - which is why recursive aliases and forward references work without quotes.
  • Like all annotations, these are for type checkers; nothing is enforced at run time.

f-strings, @override and batched (3.12)

  • f-strings are parsed like normal code (PEP 701): expressions may reuse the enclosing quote character (f"{", ".join(names)}" printed Ada, Bo), nest f-strings, contain backslashes and comments, and span lines. Convenient - but it's new syntax, so a file using it doesn't parse on 3.11 or older.
  • @typing.override marks a method as overriding a base-class method. A type checker reports an error when the base method doesn't exist (say, after it was renamed), catching silent "overrides" of nothing. At run time it only sets __override__ = True.
  • itertools.batched(iterable, n) yields tuples of up to n items: batched(range(7), 3) gives (0, 1, 2), (3, 4, 5), (6,). In 3.13, strict=True raises ValueError: batched(): incomplete batch if the last one is short.
  • 3.12 also gave each subinterpreter its own GIL (PEP 684) and made None, small ints and other singletons immortal (PEP 683) - groundwork for free-threading.

Python 3.13

  • A new interactive REPL with colors, multi-line editing and history, and paste mode.
  • Experimental free-threaded build (python3.13t, PEP 703) and an experimental JIT (PEP 744), both off by default - see Free-Threaded Python and Subinterpreters.
  • copy.replace(obj, **changes) copies an immutable object with some fields changed - frozen dataclasses, named tuples, datetime - copy.replace(config, retries=5) leaves the original untouched.
  • @warnings.deprecated("use fetch_v2") (PEP 702) emits a DeprecationWarning when the function is called and makes type checkers flag every call.
  • locals() semantics (PEP 667) are defined precisely in functions: it returns a snapshot, so writing to it never changes local variables.

What's gone

PEP 594 retired old, unmaintained standard-library modules ("dead batteries"), after deprecation in 3.11:

Removed inModules
3.12distutils (use setuptools), imp (use importlib), asyncore, asynchat, smtpd
3.13cgi, cgitb, telnetlib, crypt, nntplib, pipes, sndhdr, imghdr, audioop, mailcap, msilib, nis, ossaudiodev, spwd, sunau, uu, xdrlib, aifc, chunk

Code (or an old dependency) that imports one fails with ModuleNotFoundError: No module named 'cgi' on upgrade - every one in the table failed that way on 3.14. Run your test suite with -W error::DeprecationWarning on the release before an upgrade: removals are always announced as deprecation warnings first.

Which tool for which job?

You want toUse
A generic function or classdef first[T](...) / class Box[T]: (3.12+)
The same on older PythonsT = TypeVar("T"), or typing_extensions
A named type, possibly recursivetype Tree = dict[str, Tree] | None
Catch "overrides" of a renamed base method@typing.override + a type checker
Process items in chunksitertools.batched(items, n)
Copy an immutable object with a changecopy.replace(obj, field=value) (3.13)
Retire a function politely@warnings.deprecated("use ...") (3.13)
Find removals before an upgraderun tests with -W error::DeprecationWarning

Common mistakes

MistakeWhat happensDo this instead
PEP 701 f-strings or def f[T] in a library that supports 3.11SyntaxError on import for those userswait until you drop 3.11
Expecting @override to check at run timenothing happensrun a type checker in CI
Mixing an old TypeVar and new syntax for one parameterconfusing errors from the checkerpick one style per definition
Upgrading with deprecation warnings ignoredModuleNotFoundError for removed modulesfix warnings on the release before
batched without strict=True where batches must be fulla short last batch slips throughstrict=True (3.13)

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Why can't a library that supports Python 3.11 use f"{", ".join(names)}"?

Show answer

Reusing the enclosing quotes inside an f-string is 3.12 syntax (PEP 701). On 3.11 the whole module fails to parse with a SyntaxError at import.

2. Why does type Tree = dict[str, Tree] | None work without quotes?

Show answer

A type statement's value is evaluated lazily, when the alias is first used, by which time Tree exists.

3. What does @override do at run time?

Show answer

Only sets __override__ = True on the method. Its value is in type checkers, which flag an @override method whose base method doesn't exist.

Key takeaways

  • def f[T] and type X = ... replace TypeVar boilerplate; aliases are lazy.
  • PEP 701 f-strings and type-parameter syntax raise your minimum Python to 3.12.
  • @override, batched, copy.replace, @deprecated - small, useful, checked by tools.
  • 3.12 and 3.13 removed the dead batteries; heed deprecation warnings before upgrading.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You want toUse
A generic function or classdef first[T](...), class Box[T]: (3.12)
A bounded or constrained parameter[T: Model], [T: (int, float)]
A named, possibly recursive typetype Tree = dict[str, Tree] | None
Flag a method that overrides nothing@typing.override + a checker
Chunks of nitertools.batched(items, n) (strict=True in 3.13)
Copy an immutable object with changescopy.replace(obj, ...) (3.13)
Deprecate a function or class@warnings.deprecated("...") (3.13)
Catch removals early-W error::DeprecationWarning

Snippets worth knowing by heart

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

class Box[T]:
    def __init__(self, item: T) -> None:
        self.item = item

type Pair = tuple[int, int]

for chunk in itertools.batched(rows, 500):
    insert_many(chunk)

safer = copy.replace(config, retries=5)

@warnings.deprecated("use fetch_v2")
def fetch(url): ...

The traps

  • New syntax raises your minimum Python - PEP 701 f-strings and def f[T] fail to import on 3.11.
  • @override and type parameters do nothing at run time - checkers enforce them.
  • Removed modules (distutils, imp, cgi, telnetlib, ...) - ModuleNotFoundError on upgrade.
  • DeprecationWarning is hidden by default outside __main__ and test runners.

Staff-level points, one line each

  • Type parameters live in annotation scopes: T isn't a module global; variance is inferred.
  • type aliases are lazy - a typo fails only when __value__ is read; aliases aren't classes (isinstance raises).
  • PEP 701: f-strings are tokenized (FSTRING_START, FSTRING_MIDDLE, ...) and parsed as code - quotes, backslashes, nesting.
  • Immortal objects (PEP 683): sys.getrefcount(None) is a constant 3221225472; no counter contention.
  • A GIL per subinterpreter (PEP 684) in 3.12; free-threaded build and copy-and-patch JIT experimental in 3.13.
  • PEP 667: locals() in a function is a snapshot; frame.f_locals is a write-through FrameLocalsProxy.
  • copy.replace calls __replace__; plain classes raise TypeError; dataclasses re-run __post_init__.
  • @deprecated warns on call (on instantiation for a class) and sets __deprecated__.
  • PEP 387: at least two releases of warnings before a removal.

Say it in an interview

  • "We can't use the new f-string syntax until we drop 3.11 - it's a parse error there."
  • "Type parameters are scoped to the definition, so there's no shared global T."
  • "Before upgrading, we run the suite with deprecation warnings as errors."
  • "copy.replace gives us an updated frozen config without mutating the shared one."

Practice

This skill comes with 12 multiple-choice questions that tell you which misconception a wrong answer reveals. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Python 3.12-3.13: Type Parameters, f-strings and Removals free

Practice Python 3.12-3.13: Type Parameters, f-strings and Removals free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime