Back to Posts
Person coding on a laptop with colorful code editor display in a modern workspace, emphasizing a Python Generics Tutorial and showcasing practical programming tools.

How to Use Python Generics Effectively

Writing code that is both reusable and type-aware can often be a challenging endeavor. Much of the code we write is reusable, but ensuring it works with different data types and catching potential issues early can be difficult without the right tools. This is where generics in Python come into play.

What are generics?

Generics allow you to write functions and classes that can operate on any data type while maintaining clear type expectations. They enable you to define a placeholder for a type that you specify when you instantiate the generic class or call the generic function. This is particularly useful for creating reusable data structures and algorithms. While Python does not enforce type safety at runtime, type hints with generics can greatly assist your IDE in providing better code completion, error checking, and documentation.

Why use generics?

Generics offer several advantages:

  • Type awareness: While Python doesn’t provide true type safety, type hints can help your IDE alert you to potential errors, enhancing your development experience. Using generics allows us to preserve the type information, unlike any, which results in type erasure.
  • Helpful IDE suggestions: Just like regular type hinting, generic hints improve the suggestions you get from your IDE by providing it with extra context about what types you are using.
  • Code reusability: You can write more general and reusable code without sacrificing type awareness.
  • Readability: Well-defined generics can make your code more readable and self-documenting.

Getting started with generics in Python

To use generics in Python, you need to import the necessary components from the typing module. Let’s start with a simple example using a generic class.

Example: A generic stack

A stack is a common data structure that follows the Last In, First Out (LIFO) principle. Here’s how you can define a generic stack class in Python:

class Stack[T]:
    def __init__(self):
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T:
        return self._items.pop()

    def peek(self) -> T:
        return self._items[-1]

    def is_empty(self) -> bool:
        return len(self._items) == 0

    def size(self) -> int:
        return len(self._items)

In this example:

  • T is a type variable that can be any type.
  • Stack is a generic class that can store items of any type specified by T.

You can now create a stack of integers, strings, or any other type:

int_stack: Stack[int] = Stack()
int_stack.push(1)
int_stack.push(2)
print(int_stack.pop())  # Output: 2

str_stack: Stack[str] = Stack()
str_stack.push("hello")
str_stack.push("world")
print(str_stack.pop())  # Output: world

Example: A generic function

Generics are not limited to classes; you can also create generic functions. Here’s an example of a simple generic function that returns the maximum of two values:

def get_max[T](a: T, b: T) -> T:
    return a if a > b else b

print(get_max(10, 20))  # Output: 20
print(get_max("apple", "banana"))  # Output: banana

In this function:

  • T is a type variable representing the type of the arguments and return value.
  • The function get_max works with any type that supports the > operator.

Advanced usage

  • Bounded type variables: You can restrict a type variable to a certain subset of types.
def concatenate[T:str](a: T, b: T) -> T:
    return a + b

print(concatenate("hello", "world"))  # Output: helloworld
  • Constrained type variables: Much like bound variables, you can also constrain a number of types.
def mult[T:(int, float)](a: T, b: T) -> T:
    return a * b

print(mult(10, 24.5)) # Output: 245.0
  • Generic inheritance: You can create classes that inherit from generic classes.
class Container[T]:
    def __init__(self, value: T):
        self.value = value

class IntContainer(Container[int]):
    pass

int_container = IntContainer(42)
print(int_container.value)  # Output: 42

Final thoughts

Generics are a powerful feature in Python that can help you write more reusable, type-aware code. By using type variables, you can define functions and classes that work with any data type while maintaining clear type expectations.

As you delve deeper into Python’s type hints and generics, you’ll find that they can greatly enhance the robustness and readability of your code.

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