Back to Posts
Person typing on a laptop, learning Python 3.12 generics syntax for improved type-hinting and cleaner code. Nearby items include a smartphone, camera lens, and wallet on a geometric wooden table.

Python 3.12 Generics: Cleaner, Easier Type-Hinting

By Alyce Osbourne

Type-hinting generics in Python got a whole lot easier with the release of Python 3.12. No longer do we need to define TypeVars and ParamSpecs. Let me show you how the new generic syntax can make your code cleaner and easier to read!

What is a generic?

A generic is a type of object whose behavior doesn’t depend on the type it is handling and instead can be used in the same way for many types. An example of a generic is the built-in list[T] object, where T is the generic type. This means we can type hint the object as being a list of strings, integers, or any other object we desire, as the functionality of a list does not depend on its contents.

Generics are essential for creating reusable components. They allow us to write functions, classes, and data structures that can work with any data type while maintaining type consistency. For instance, consider a stack data structure. Using generics, we can implement a stack that works uniformly with integers, strings, or any other type.

Why the change?

Before Python 3.12, type hinting required importing several objects from the typing module, which often felt bolted-on and unintuitive. Issues frequently arose, such as metaclass conflicts when combining generic types with other types, leading to verbose and complex code. The new generics syntax in Python 3.12 simplifies this significantly, making type hints more intuitive and less verbose.

The change aims to streamline type hinting, reduce the cognitive load on developers, and make type-annotated code easier to read and write. By minimizing the boilerplate code required for generics, Python 3.12 enhances the developer experience and aligns Python’s generics more closely with those found in other modern programming languages.

Out with the old!

Previously, to type hint generics, you needed to use the Generic, TypeVar, TypeVarTuple, and ParamSpec types to accurately and properly type hint these objects. Here’s an example of common patterns before Python 3.12:

from typing import Generic, TypeVar, ParamSpec, Callable, Iterable, TypeVarTuple

T = TypeVar("T")
Ts = TypeVarTuple("Ts")
P = ParamSpec("P")

TaggedTuple: type = tuple[str, *Ts]

def decorator(func: Callable[P, T]) -> Callable[P, T]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        return func(*args, **kwargs)
    return wrapper

class Container(Generic[T, *Ts]):
    def __init__(self, items: Iterable[T, *Ts] = ()):
        self.items = list(items)

    def __getitem__(self, item: int) -> T:
        return self.items[item]

    def __setitem__(self, key: int, value: T) -> None:
        self.items[key] = value

    def __delitem__(self, key: int) -> None:
        del self.items[key]

    def __iter__(self) -> Iterable[T]:
        return iter(self.items)

    def __len__(self) -> int:
        return len(self.items)

    def __repr__(self) -> str:
        return f"{type(self).__name__}({self.items})"

This approach, while functional, was cumbersome and required numerous type definitions. Managing these types often became a hassle, particularly in larger codebases.

In with the new!

Python 3.12 introduces a more streamlined syntax for generics, reducing verbosity and improving readability. Here’s how you can rewrite the above code using the new syntax:

from typing import Callable, Iterable

type TaggedTuple[*Ts] = tuple[str, *Ts]

def decorator[**P, T](func: Callable[P, T]) -> Callable[P, T]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
        return func(*args, **kwargs)
    return wrapper

class Container[T]:
    def __init__(self, items: Iterable[T] = ()):
        self.items = list(items)

    def __getitem__(self, item: int) -> T:
        return self.items[item]

    def __setitem__(self, key: int, value: T) -> None:
        self.items[key] = value

    def __delitem__(self, key: int) -> None:
        del self.items[key]

    def __iter__(self) -> Iterable[T]:
        return iter(self.items)

    def __len__(self) -> int:
        return len(self.items)

    def __repr__(self) -> str:
        return f"{type(self).__name__}({self.items})"

With the new syntax, T, *Ts, and **P replace TypeVar, TypeVarTuple, and ParamSpec, respectively, and any class that has generic annotations is considered a Generic type. This approach uses a more familiar and less cluttered syntax, akin to other programming languages that support generics, making the code more maintainable and understandable.

Bounded and constrained types

Python 3.12 also simplifies bounding and constraining generic types using a syntax that mirrors type hinting arguments.

Bound types:

class Container[T: Mapping]:
    ...

Constrained types:

class Container[T: (int, float)]:
    ...

These enhancements make it easier to enforce constraints on generic types, ensuring that they conform to specific interfaces or sets of types. This will aid your IDE in providing better clues into any potential bugs and issues while simultaneously providing correct suggestions and autocompletions.

Final thoughts

The new generics syntax in Python 3.12 is a significant improvement, making type hints cleaner, simpler, and less verbose, which enhances code readability and maintainability. These changes reflect the Python development team’s ongoing efforts to refine and improve the language.

By adopting the new generic syntax, developers can write more expressive and maintainable code. The simplified syntax reduces the overhead of type hinting and makes Python an even more powerful and flexible language for both beginners and experienced developers alike.

Improve your code with my 3-part code diagnosis framework

Watch my free 30 minutes code diagnosis workshop on how to quickly detect problems in your code and review your code more effectively.

When you sign up, you'll get an email from me regularly with additional free content. You can unsubscribe at any time.

Recent posts