Skip to main content

Python Generics and TypeVar Practice Problems & Exercises

Practice: Generics and TypeVar

11 problems3 Easy4 Medium4 Hard70–90 min
← Back to lesson

Easy

#1Generic Identity FunctionEasy
TypeVargeneric-functiontype-annotation

Write a generic identity function using TypeVar that returns its argument unchanged and preserves the exact type through the signature.

Python
from typing import TypeVar

T = TypeVar("T")


def identity(x: T) -> T:
    return x


# Each call preserves its specific type
print(identity(42))
print(identity("hello"))
print(identity([1, 2, 3]))
Expected Output
42
hello
[1, 2, 3]
Hints

Hint 1: Declare T = TypeVar("T") and annotate the function as def identity(x: T) -> T.

Hint 2: TypeVar ensures the return type matches the input type — the static checker enforces this even though runtime is identical.


#2Generic Stack ClassEasy
GenericTypeVarstackdata-structure

Implement a Stack[T] generic class with push, pop, and peek operations. Demonstrate usage with integers.

Python
from typing import TypeVar, Generic, List

T = TypeVar("T")


class Stack(Generic[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:
        if not self._items:
            raise IndexError("peek at empty stack")
        return self._items[-1]

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


s: Stack[int] = Stack()
s.push(1)
s.push(2)
s.push(3)

print(f"Popped: {s.pop()}")
print(f"Remaining: {s._items}")
print(f"Top: {s.peek()}")
Expected Output
Popped: 3
Remaining: [1, 2]
Top: 2
Hints

Hint 1: Declare T = TypeVar("T") and class Stack(Generic[T]). Methods push, pop, and peek use T in their annotations.

Hint 2: The internal storage is a plain list; Generic[T] only affects static type checking.


#3Generic first() and last() UtilitiesEasy
TypeVarSequencegeneric-functionOptional

Write generic first() and last() functions that work on any Sequence[T] and return Optional[T].

Python
from typing import TypeVar, Sequence, Optional

T = TypeVar("T")


def first(seq: Sequence[T]) -> Optional[T]:
    return seq[0] if seq else None


def last(seq: Sequence[T]) -> Optional[T]:
    return seq[-1] if seq else None


nums = [10, 20, 30]
print(f"first([10,20,30]) = {first(nums)}")
print(f"last([10,20,30]) = {last(nums)}")
print(f"first([]) = {first([])}")
print(f"last([]) = {last([])}")
Expected Output
first([10,20,30]) = 10
last([10,20,30]) = 30
first([]) = None
last([]) = None
Hints

Hint 1: Use TypeVar T and Sequence[T] as the parameter type. Return Optional[T].

Hint 2: Return None (typed as T | None) when the sequence is empty.


Medium

#4Bounded TypeVar — Numeric OperationsMedium
TypeVarboundNumericconstraint

Use a bounded TypeVar to write sum_all and clamp functions that work on any numeric type (int or float) but not on strings or other types.

Python
from typing import TypeVar, List

N = TypeVar("N", int, float)


def sum_all(values: List[N]) -> N:
    result = values[0]
    for v in values[1:]:
        result = result + v
    return result


def clamp(value: N, lo: N, hi: N) -> N:
    if value < lo:
        return lo
    if value > hi:
        return hi
    return value


print(f"sum_all([1, 2, 3, 4]) = {sum_all([1, 2, 3, 4])}")
print(f"sum_all([1.5, 2.5, 3.0]) = {sum_all([1.5, 2.5, 3.0])}")
print(f"clamp(15, lo=0, hi=10) = {clamp(15, 0, 10)}")
print(f"clamp(-5, lo=0, hi=10) = {clamp(-5, 0, 10)}")
Expected Output
sum_all([1, 2, 3, 4]) = 10
sum_all([1.5, 2.5, 3.0]) = 7.0
clamp(15, lo=0, hi=10) = 10
clamp(-5, lo=0, hi=10) = 0
Hints

Hint 1: Declare N = TypeVar("N", int, float) or N = TypeVar("N", bound=numbers.Real) to constrain to numeric types.

Hint 2: Bounded TypeVars restrict which concrete types are allowed at call sites — operations like + and > are safe within the bound.


#5Generic Pair and BimapMedium
GenericTypeVarPairbimaptwo-type-parameters

Build a Pair[A, B] generic class with bimap (transform both sides independently) and swap (flip first and second).

Python
from typing import TypeVar, Generic, Callable, Tuple

A = TypeVar("A")
B = TypeVar("B")
C = TypeVar("C")
D = TypeVar("D")


class Pair(Generic[A, B]):
    def __init__(self, first: A, second: B) -> None:
        self.first = first
        self.second = second

    def bimap(self, f: Callable[[A], C], g: Callable[[B], D]) -> "Pair[C, D]":
        return Pair(f(self.first), g(self.second))

    def swap(self) -> "Pair[B, A]":
        return Pair(self.second, self.first)

    def __repr__(self) -> str:
        return f"Pair({self.first!r}, {self.second!r})"


p: Pair[int, str] = Pair(1, "hello")
print(p)

transformed = p.bimap(lambda x: x * 2, str.upper)
print(f"After bimap: {transformed}")

swapped = p.swap()
print(f"Swapped: {swapped}")
Expected Output
Pair(1, 'hello')
After bimap: Pair(2, 'HELLO')
Swapped: Pair('hello', 1)
Hints

Hint 1: Use two TypeVars: A and B. Declare class Pair(Generic[A, B]) with a first: A and second: B attribute.

Hint 2: bimap takes two callables and returns a new Pair with both sides transformed.


#6Covariant Generic ContainerMedium
TypeVarcovariancecovariantGeneric

Create a ReadOnlyBox[T_co] covariant container. Demonstrate that a ReadOnlyBox[str] can be used where a ReadOnlyBox[object] is expected.

Python
from typing import TypeVar, Generic

T_co = TypeVar("T_co", covariant=True)


class ReadOnlyBox(Generic[T_co]):
    def __init__(self, value: T_co) -> None:
        self._value = value

    def get(self) -> T_co:
        return self._value

    def __repr__(self) -> str:
        return f"ReadOnlyBox({self._value!r})"


def print_any_box(box: "ReadOnlyBox[object]") -> None:
    print(f"Contents: {box.get()}")


str_box: ReadOnlyBox[str] = ReadOnlyBox("hello")
print(f"ReadOnly[str] can hold a str: {str_box.get()}")

# Covariant: ReadOnlyBox[str] is assignable to ReadOnlyBox[object]
obj_box: ReadOnlyBox[object] = str_box   # valid due to covariance
print(f"Covariant: ReadOnly[str] is assignable to ReadOnly[object]: {isinstance(obj_box, ReadOnlyBox)}")

print_any_box(str_box)
Expected Output
ReadOnly[str] can hold a str: hello
Covariant: ReadOnly[str] is assignable to ReadOnly[object]: True
Contents: hello
Hints

Hint 1: Declare T_co = TypeVar("T_co", covariant=True) and use it in a read-only container (no __setitem__ or append).

Hint 2: A covariant container of str is a subtype of a container of object — this is safe because you can only read from it.


#7Generic Result Type — Ok and ErrMedium
GenericTypeVarResultalgebraic-type

Implement a Result[T, E] algebraic type with Ok and Err variants. Add map, unwrap, and unwrap_or methods.

Python
from typing import TypeVar, Generic, Callable, Union

T = TypeVar("T")
E = TypeVar("E")
U = TypeVar("U")


class Result(Generic[T, E]):
    pass


class Ok(Result[T, E]):
    def __init__(self, value: T) -> None:
        self._value = value

    def map(self, f: Callable[[T], U]) -> "Ok[U, E]":
        return Ok(f(self._value))

    def unwrap(self) -> T:
        return self._value

    def unwrap_or(self, default: T) -> T:
        return self._value

    def __repr__(self) -> str:
        return f"Ok({self._value!r})"


class Err(Result[T, E]):
    def __init__(self, error: E) -> None:
        self._error = error

    def map(self, f: Callable[[T], U]) -> "Err[U, E]":
        return self  # type: ignore

    def unwrap(self) -> T:
        raise ValueError(f"Called unwrap on Err: {self._error}")

    def unwrap_or(self, default: T) -> T:
        return default

    def __repr__(self) -> str:
        return f"Err({self._error!r})"


def safe_divide(a: int, b: int) -> Result[int, str]:
    if b == 0:
        return Err("division by zero")
    return Ok(a // b)


ok_result = safe_divide(42, 1)
err_result = safe_divide(10, 0)

print(ok_result)
print(err_result)
print(f"map Ok: {ok_result.map(lambda x: x * 2)}")
print(f"map Err: {err_result.map(lambda x: x * 2)}")
print(f"unwrap_or: {err_result.unwrap_or(0)}")
Expected Output
Ok(42)
Err('division by zero')
map Ok: Ok(84)
map Err: Err('division by zero')
unwrap_or: 0
Hints

Hint 1: Create Ok[T] and Err[E], both subclasses of Result[T, E]. Use two TypeVars T and E.

Hint 2: Implement map(f) on Ok to transform the value; on Err it returns self unchanged. unwrap_or returns the value or the default.


Hard

#8Generic Binary Tree with In-Order TraversalHard
GenericTypeVarbinary-treetraversalrecursive

Build a generic BST[T] (Binary Search Tree) with insert, contains, and in_order_traversal. The element type must be orderable.

Python
from typing import TypeVar, Generic, Optional, List

T = TypeVar("T")


class BSTNode(Generic[T]):
    def __init__(self, value: T) -> None:
        self.value = value
        self.left: Optional["BSTNode[T]"] = None
        self.right: Optional["BSTNode[T]"] = None


class BST(Generic[T]):
    def __init__(self) -> None:
        self._root: Optional[BSTNode[T]] = None

    def insert(self, value: T) -> None:
        def _insert(node: Optional[BSTNode[T]], val: T) -> BSTNode[T]:
            if node is None:
                return BSTNode(val)
            if val < node.value:   # type: ignore[operator]
                node.left = _insert(node.left, val)
            elif val > node.value:  # type: ignore[operator]
                node.right = _insert(node.right, val)
            return node

        self._root = _insert(self._root, value)

    def contains(self, value: T) -> bool:
        node = self._root
        while node:
            if value == node.value:
                return True
            elif value < node.value:  # type: ignore[operator]
                node = node.left
            else:
                node = node.right
        return False

    def in_order(self) -> List[T]:
        result: List[T] = []

        def _walk(node: Optional[BSTNode[T]]) -> None:
            if node:
                _walk(node.left)
                result.append(node.value)
                _walk(node.right)

        _walk(self._root)
        return result

    def min_val(self) -> Optional[T]:
        node = self._root
        while node and node.left:
            node = node.left
        return node.value if node else None

    def max_val(self) -> Optional[T]:
        node = self._root
        while node and node.right:
            node = node.right
        return node.value if node else None


tree: BST[int] = BST()
for v in [5, 3, 7, 1, 9]:
    tree.insert(v)

print(f"In-order: {tree.in_order()}")
print(f"Min: {tree.min_val()}")
print(f"Max: {tree.max_val()}")
print(f"Contains 5: {tree.contains(5)}")
print(f"Contains 6: {tree.contains(6)}")
Expected Output
In-order: [1, 3, 5, 7, 9]
Min: 1
Max: 9
Contains 5: True
Contains 6: False
Hints

Hint 1: Declare T = TypeVar("T") with bound="Comparable" or just trust that elements are orderable. Use Optional[BSTNode[T]] for left/right.

Hint 2: insert and contains use comparison operators (<, >) which are available on the bounded type.


#9Generic Pipeline BuilderHard
GenericTypeVarpipelinefunction-composition

Build a type-safe Pipeline[A, B] that chains transformations. Composing pipe(f) on a Pipeline[A, B] with f: B -> C produces a Pipeline[A, C].

Python
from typing import TypeVar, Generic, Callable, List, Any

A = TypeVar("A")
B = TypeVar("B")
C = TypeVar("C")


class Pipeline(Generic[A, B]):
    def __init__(self, steps: List[Callable[..., Any]]) -> None:
        self._steps = steps

    @classmethod
    def start(cls) -> "Pipeline[A, A]":
        return cls([])

    def pipe(self, f: Callable[[B], C]) -> "Pipeline[A, C]":
        return Pipeline(self._steps + [f])

    def run(self, value: A) -> B:
        result: Any = value
        for step in self._steps:
            result = step(result)
        return result  # type: ignore[return-value]


pipeline = (
    Pipeline.start()
    .pipe(str.strip)
    .pipe(str.upper)
    .pipe(lambda s: s + "!")
    .pipe(len)
)

text = "  hello world  "
result = pipeline.run(text)

print(f"Pipeline: {text.strip()!r} -> {text.strip().upper()!r} -> {text.strip().upper() + '!'!r} -> {result}")
print(f"Result: {result}")
Expected Output
Pipeline: 'hello world' -> 'HELLO WORLD' -> 'HELLO WORLD!' -> 13
Result: 13
Hints

Hint 1: Use two TypeVars A and B. Pipeline[A, B] represents a transformation from A to B. Composing Pipeline[A, B] with Pipeline[B, C] gives Pipeline[A, C].

Hint 2: Store the steps as a list of callables. pipe() appends a step and returns a new Pipeline with updated output type.


#10Contravariant Sink TypeHard
TypeVarcontravariancecontravariantconsumer

Create a Sink[T_contra] contravariant consumer. Demonstrate that a Sink[Animal] can be used where a Sink[Dog] is expected (contravariance).

Python
from typing import TypeVar, Generic, Callable

T_contra = TypeVar("T_contra", contravariant=True)


class Sink(Generic[T_contra]):
    def __init__(self, handler: Callable[[T_contra], None]) -> None:
        self._handler = handler

    def accept(self, value: T_contra) -> None:
        self._handler(value)


class Animal:
    def __init__(self, name: str) -> None:
        self.name = name

    def speak(self) -> str:
        return f"{self.name} makes a sound"


class Dog(Animal):
    def speak(self) -> str:
        return f"{self.name} barks"


def handle_animal(a: Animal) -> None:
    print(f"  Handling: {a.speak()}")


# A Sink[Animal] can handle any Animal — including Dog
animal_sink: Sink[Animal] = Sink(handle_animal)

dog: Dog = Dog("Rex")
animal: Animal = Animal("Generic")

print(f"AnimalSink accepts Animal: True")
animal_sink.accept(animal)

print(f"AnimalSink accepts Dog (subtype): True")
animal_sink.accept(dog)

# Contravariance: Sink[Animal] is assignable to Sink[Dog]
dog_sink: Sink[Dog] = animal_sink  # valid due to contravariance
print(f"Contravariant: Sink[Animal] usable as Sink[Dog]: {isinstance(dog_sink, Sink)}")
Expected Output
AnimalSink accepts Animal: True
AnimalSink accepts Dog (subtype): True
Contravariant: Sink[Animal] usable as Sink[Dog]: True
Hints

Hint 1: Declare T_contra = TypeVar("T_contra", contravariant=True). A Sink[Animal] is a supertype of Sink[Dog] — it is safe to use a more general consumer where a specific one is expected.

Hint 2: Implement accept(value: T_contra) -> None. A consumer that handles Animal can certainly handle Dog.


#11Generic Monad — Maybe[T]Hard
GenericTypeVarmonadMaybeflatmap

Implement a Maybe[T] monad with Just and Nothing variants. Support map, flatMap, and method chaining that short-circuits on Nothing.

Python
from typing import TypeVar, Generic, Callable, Optional

T = TypeVar("T")
U = TypeVar("U")


class Maybe(Generic[T]):
    def map(self, f: Callable[[T], U]) -> "Maybe[U]":
        raise NotImplementedError

    def flat_map(self, f: Callable[[T], "Maybe[U]"]) -> "Maybe[U]":
        raise NotImplementedError

    def get_or(self, default: T) -> T:
        raise NotImplementedError


class Just(Maybe[T]):
    def __init__(self, value: T) -> None:
        self._value = value

    def map(self, f: Callable[[T], U]) -> Maybe[U]:
        return Just(f(self._value))

    def flat_map(self, f: Callable[[T], Maybe[U]]) -> Maybe[U]:
        return f(self._value)

    def get_or(self, default: T) -> T:
        return self._value

    def __repr__(self) -> str:
        return f"Just({self._value!r})"


class _Nothing(Maybe):
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def map(self, f):
        return self

    def flat_map(self, f):
        return self

    def get_or(self, default):
        return default

    def __repr__(self) -> str:
        return "Nothing"


Nothing = _Nothing()


def safe_sqrt(x: float) -> Maybe[float]:
    import math
    if x < 0:
        return Nothing
    return Just(math.sqrt(x))


def safe_halve(x: float) -> Maybe[float]:
    if x == 0:
        return Nothing
    return Just(x / 2)


j: Maybe[int] = Just(42)
print(j)
print(Nothing)
print(f"flatMap Just: {j.flat_map(lambda x: Just(x * 2))}")
print(f"flatMap Nothing: {Nothing.flat_map(lambda x: Just(x * 2))}")

chained = Just(42.0).flat_map(safe_sqrt).flat_map(safe_halve)
print(f"chain: {chained}")

broken = Just(-1.0).flat_map(safe_sqrt).flat_map(safe_halve)
print(f"chain with None-returning step: {broken}")
Expected Output
Just(42)
Nothing
flatMap Just: Just(84)
flatMap Nothing: Nothing
chain: Just(21)
chain with None-returning step: Nothing
Hints

Hint 1: Create Just[T] and Nothing as subclasses of Maybe[T]. Nothing is a singleton.

Hint 2: flatMap (or bind) on Just calls f(value) and returns the result. On Nothing it returns Nothing. map wraps the result in Just automatically.

© 2026 EngineersOfAI. All rights reserved.