Skip to the solution
MediumDSA12 min read

What is Python 3.12's new type parameter syntax (type, generic classes)?

288 views

The problem

Question presented to candidate: "Python 3.12 lets you write class Box[T]: and type Pair[T] = tuple[T, T]. What does that replace, what actually changes at runtime, and why is it more than just shorter syntax?"

What a strong answer should cover:

  • PEP 695 (Python 3.12) adds type parameter lists: def first[T](xs: list[T]) -> T, class Box[T]:, with no TypeVar import and no Generic[T] base (the class still gets Generic in its MRO)
  • Bounds and constraints are written inline: [N: float], [S: (str, bytes)]; variance is inferred by the checker instead of declared with covariant=True
  • The type statement creates a TypeAliasType whose value is evaluated lazily, so recursive and forward-referencing aliases need no quotes; type is a soft keyword
  • Parameters live in their own annotation scope: they do not leak into the module, and bounds are evaluated only when accessed (__type_params__, __bound__, __value__)
  • Python 3.13 adds defaults (class Response[Body = str], PEP 696); the old and new styles cannot be mixed in one class, and an alias is not a class (isinstance rejects it)

Code / implementation expected: Optional β€” rewrite an old TypeVar / Generic class and a TypeAlias in the new syntax, and say which Python version each needs.

  • typing
  • generics
  • python3.12

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

12 min read Β· 11 sections

Run the code

Edit the code and run it here. Each console line with an expected value becomes a test.

PythonThe pre-3.12 spelling it replaces, and its problems (runs in the page on 3.11)
PythonPEP 695 on Python 3.12: generic classes and functions, bounds, the type statement
# Python 3.12+: PEP 695 type parameter syntax.
from typing import get_args

# 1. Generic class and function: no TypeVar, no Generic base.
class Box[T]:
    def __init__(self, item: T) -> None:
        self.item = item

    def get(self) -> T:
        return self.item

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

print(Box.__type_params__, first.__type_params__)
print(Box.__mro__)
print(Box[int], first([4, 5]))

# The parameter is scoped to its class or function.
print("T" in globals())

# 2. Bounds and constraints go after a colon.
def biggest[N: float](a: N, b: N) -> N:
    return a if a >= b else b

def double[S: (str, bytes)](x: S) -> S:
    return x + x

(n,) = biggest.__type_params__
(s,) = double.__type_params__
print(n.__bound__, s.__constraints__)

# 3. Variance is inferred by the checker, not declared.
print(n.__infer_variance__, n.__covariant__)

# 4. The type statement makes a lazy TypeAliasType.
type Pair[T] = tuple[T, T]
type Json = dict[str, Json] | list[Json] | str | int | float | bool | None
type Later = list[NotYetDefined]

print(type(Pair).__name__, Pair.__name__, Pair.__type_params__)
print(Pair[int], get_args(Pair[int]))
print(Json.__value__)
try:
    Later.__value__
except NameError as err:
    print("NameError on first use:", err)

class NotYetDefined: ...
print(Later.__value__)

# 5. An alias is not a class: no isinstance, no calling.
try:
    isinstance([], Json)
except TypeError as err:
    print("TypeError:", err)

# 6. type is a soft keyword: still fine as a variable name.
type = "still a name"
print(type)
PythonPython 3.13 type parameter defaults, and two rules of the new syntax
# Python 3.13+: type parameter defaults (PEP 696), plus two rules.
from typing import Generic, NoDefault, TypeVar

# 1. A default fills in a parameter the user leaves out.
class Response[Body = str, Status: int = int]:
    def __init__(self, body: Body, status: Status) -> None:
        self.body = body
        self.status = status

body, status = Response.__type_params__
print(body.__default__, status.__default__, status.__bound__)
print(body.has_default())

def plain[T](x: T) -> T:
    return x

(t,) = plain.__type_params__
print(t.has_default(), t.__default__ is NoDefault)

# The old-style TypeVar gained the same keyword.
U = TypeVar("U", default=bytes)
print(U.__default__)

# 2. Old and new styles do not mix on one class.
T = TypeVar("T")
try:
    class Mixed[V](Generic[T]):
        pass
except TypeError as err:
    print("TypeError:", err)

# 3. A parameter without a default after one with a default is an error.
try:
    compile("class Bad[A = int, B]: pass", "<bad>", "exec")
except SyntaxError as err:
    print("SyntaxError:", err.msg)

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 101 of 139 in Python

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Python questions
Up nextMedium

When should you still use multiprocessing vs free-threaded threads in Python 3.12+?

Continue