Python Topics : Generics
What Are Generics?
generics allow functions and classes to be written which can operate on any data type while maintaining clear type expectations
generics define a placeholder for a type which is specified when the generic class is instantiated or call the generic function
useful for creating reusable data structures and algorithms
Python does not enforce type safety at runtime
type hints with generics can greatly assist an IDE in providing better code completion, error checking, and documentation
Why Use Generics?
advantages of using generics
  • type awareness - type hints can help IDE alert programmer to potential errors enhancing the development experience
    using generics allows preserving the type information
    denies type erasure
  • helpful IDE suggestions - generic hints improve the suggestions from the IDE
    provides IDE with extra context about what types being used
  • code reusability - can write more general and reusable code without sacrificing type awareness
  • readability - well-defined generics can make the code more readable and self-documenting
A Generic Stack
to use generics in Python need to import the necessary components from the typing module
the typing module is included in the standard library

a stack is a common data structure that follows the Last In, First Out (LIFO) principle
below

  • 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
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)
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
A Generic Function
generics are not limited to classes
can also create generic functions
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
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 - 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 - can 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 - can create classes which 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
index