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 byT
.
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.