| 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 
 | 
| 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 
 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 typeint_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: bananaT is a type variable representing the type of the arguments and return valuethe 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: helloworldconstrained type variables - can constrain a number of typesdef mult[T:(int, float)](a: T, b: T) -> T:
    return a * b
print(mult(10, 24.5)) # Output: 245.0generic inheritance - can create classes which inherit from generic classesclass 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
 |