Generic Classes with class Stack[T]
Write containers that stay type-safe for any element type using Python 3.12+ generic syntax.
A generic class works with any type while keeping the type checker informed of exactly which one you chose. A Stack of int and a Stack of str share one implementation but never get confused for each other.
The modern syntax (Python 3.12+)
Since Python 3.12, you declare a type parameter right in the class header — no more importing TypeVar and inheriting from Generic:
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
numbers: Stack[int] = Stack()
numbers.push(1) # a type checker now knows pop() returns intWhy it matters
At runtime the type parameter is essentially erased — your code runs the same. The payoff is at authoring time: your editor autocompletes correctly, and a checker like mypy flags numbers.push('oops') before you ever run the program. Generics are how you get the flexibility of duck typing with the safety net of static types.
Example
# Python 3.12+ inline generics — no typing imports needed
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
if not self._items:
raise IndexError('pop from empty stack')
return self._items.pop()
def peek(self) -> T:
return self._items[-1]
def __len__(self) -> int:
return len(self._items)
# A generic FUNCTION too, with a bound: only orderable types
def largest[T: (int, float, str)](items: list[T]) -> T:
winner = items[0]
for x in items[1:]:
if x > winner:
winner = x
return winner
s: Stack[str] = Stack()
s.push('alpha'); s.push('beta')
print('peek :', s.peek()) # beta
print('pop :', s.pop()) # beta
print('size :', len(s)) # 1
print('max int :', largest([3, 9, 2, 7])) # 9
print('max str :', largest(['pear', 'fig', 'kiwi'])) # pear
# A checker would reject: s.push(123) and largest([1, 'x'])
# Output:
# peek : beta
# pop : beta
# size : 1
# max int : 9
# max str : pearWhen to use it
- A typed Stack[int] ensures only integers are pushed, catching misuse at static analysis time.
- A generic Result[T] class wraps a success value or an error message while preserving the value type.
- A typed Queue[Task] makes it clear what objects can be enqueued and dequeued in a worker pool.
More examples
Generic Stack with class[T]
Defines a generic Stack using Python 3.12+ class[T] syntax that preserves the element type.
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 __len__(self) -> int: return len(self._items)
s: Stack[int] = Stack()
s.push(1)
s.push(2)
print(s.pop()) # 2Generic Pair class
Uses two type parameters A and B to create a strongly-typed pair with a type-aware swap method.
class Pair[A, B]:
def __init__(self, first: A, second: B):
self.first = first
self.second = second
def swap(self) -> 'Pair[B, A]':
return Pair(self.second, self.first)
p = Pair('hello', 42)
print(p.first, p.second) # hello 42
swapped = p.swap()
print(swapped.first) # 42Generic class with TypeVar (3.11 compat)
Provides the pre-3.12 approach using TypeVar and Generic[T] for backwards-compatible generic classes.
from typing import Generic, TypeVar
T = TypeVar('T')
class Box(Generic[T]):
def __init__(self, value: T): self.value = value
def unwrap(self) -> T: return self.value
box: Box[str] = Box('treasure')
print(box.unwrap()) # treasure
Discussion