Skip to main content

Python Advanced Generic Patterns Practice Problems & Exercises

Practice: Advanced Generic Patterns

11 problems3 Easy4 Medium4 Hard75–95 min
← Back to lesson

Easy

#1Generic Wrapper with pass-through MethodsEasy
GenericTypeVarwrapperdelegation

Build a Box[T] generic container with map and apply (applicative functor-style) methods.

Python
from typing import TypeVar, Generic, Callable

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


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

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

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

    def apply(self, f_box: "Box[Callable[[T], U]]") -> "Box[U]":
        return Box(f_box.get()(self._value))

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


b_int: Box[int] = Box(42)
b_str: Box[str] = Box("hello")

print(b_int)
print(b_str)
print(f"map result: {b_int.map(lambda x: x * 2)}")

f_box: Box[Callable[[int], int]] = Box(lambda x: x + 1)
print(f"apply result: {b_int.apply(f_box).get()}")
Expected Output
Wrapped: 42
Wrapped: hello
map result: Wrapped: 84
apply result: 11
Hints

Hint 1: Define class Box(Generic[T]) with a value: T attribute.

Hint 2: map(f) applies f to the wrapped value and returns a new Box. apply(f_box) applies a boxed function to the boxed value.


#2Self Type for Fluent BuilderEasy
SelfTypeVarbuilder-patternfluent-interface

Build a QueryBuilder that uses Self-typed returns so that subclass builders also return the correct type in method chains.

Python
from typing import TypeVar, List, Optional

T = TypeVar("T", bound="QueryBuilder")


class QueryBuilder:
    def __init__(self) -> None:
        self._table: str = ""
        self._conditions: List[str] = []
        self._limit: Optional[int] = None

    def from_table(self: T, table: str) -> T:
        self._table = table
        return self

    def where(self: T, condition: str) -> T:
        self._conditions.append(condition)
        return self

    def limit(self: T, n: int) -> T:
        self._limit = n
        return self

    def build(self) -> str:
        sql = f"SELECT * FROM {self._table}"
        if self._conditions:
            sql += " WHERE " + " AND ".join(self._conditions)
        if self._limit is not None:
            sql += f" LIMIT {self._limit}"
        return sql

    def __repr__(self) -> str:
        return (
            f"QueryBuilder(table={self._table!r}, "
            f"conditions={self._conditions!r}, "
            f"limit={self._limit!r})"
        )


qb = (
    QueryBuilder()
    .from_table("users")
    .where("age > 18")
    .where("active = True")
    .limit(10)
)

print(qb)
print(f"SQL: {qb.build()}")
Expected Output
QueryBuilder(table='users', conditions=['age > 18', 'active = True'], limit=10)
SQL: SELECT * FROM users WHERE age > 18 AND active = True LIMIT 10
Hints

Hint 1: Use T = TypeVar("T", bound="QueryBuilder") and return T from each method. This enables subclass chaining to return the correct type.

Hint 2: Alternatively, use typing_extensions.Self (Python 3.11+) for cleaner syntax.


#3Generic Pair with Type-Preserving swap()Easy
GenericTypeVarswaptype-preservation

Build Pair[A, B] where swap() returns Pair[B, A], correctly modeling the type flip in the return annotation.

Python
from typing import TypeVar, Generic, Tuple

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


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

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

    def to_tuple(self) -> Tuple[A, B]:
        return (self.first, self.second)

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


p: Pair[int, str] = Pair(1, "hello")
swapped: Pair[str, int] = p.swap()

print(f"original: {p}")
print(f"swapped: {swapped}")
print(f"type check first: {type(swapped.first)}")
print(f"type check second: {type(swapped.second)}")
Expected Output
original: Pair(first=1, second='hello')
swapped: Pair(first='hello', second=1)
type check first: <class 'str'>
type check second: <class 'int'>
Hints

Hint 1: Use two TypeVars A and B. swap() returns Pair[B, A] — the types are flipped in the return type.

Hint 2: At runtime everything is dynamic, but the type annotation correctly models the transformation.


Medium

#4Recursive Generic — Linked ListMedium
GenericTypeVarrecursivelinked-list

Implement a generic LinkedList[T] with append, to_list, reverse, and map methods using a recursive node structure.

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

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


class Node(Generic[T]):
    def __init__(self, value: T, next_node: Optional["Node[T]"] = None) -> None:
        self.value = value
        self.next = next_node


class LinkedList(Generic[T]):
    def __init__(self) -> None:
        self._head: Optional[Node[T]] = None
        self._size = 0

    def append(self, value: T) -> None:
        new_node = Node(value)
        if self._head is None:
            self._head = new_node
        else:
            current = self._head
            while current.next:
                current = current.next
            current.next = new_node
        self._size += 1

    def to_list(self) -> List[T]:
        result = []
        current = self._head
        while current:
            result.append(current.value)
            current = current.next
        return result

    def reverse(self) -> "LinkedList[T]":
        new_list: LinkedList[T] = LinkedList()
        stack: List[T] = self.to_list()
        for val in reversed(stack):
            new_list.append(val)
        return new_list

    def map(self, f: Callable[[T], U]) -> "LinkedList[U]":
        new_list: LinkedList[U] = LinkedList()
        for val in self.to_list():
            new_list.append(f(val))
        return new_list

    def __len__(self) -> int:
        return self._size


ll: LinkedList[int] = LinkedList()
for v in [1, 2, 3, 4, 5]:
    ll.append(v)

print(f"LinkedList: {ll.to_list()}")
print(f"Reversed: {ll.reverse().to_list()}")
print(f"Length: {len(ll)}")
print(f"Map (*2): {ll.map(lambda x: x * 2).to_list()}")
Expected Output
LinkedList: [1, 2, 3, 4, 5]
Reversed: [5, 4, 3, 2, 1]
Length: 5
Map (*2): [2, 4, 6, 8, 10]
Hints

Hint 1: Node[T] has value: T and next: Optional[Node[T]]. LinkedList[T] holds a head pointer.

Hint 2: append, to_list, and map methods work recursively or iteratively on the chain.


#5Generic Mixin with Bounded TypeVarMedium
GenericmixinTypeVarboundSelf

Build a CopyableMixin[T] that provides copy() and with_field() methods using a bounded TypeVar so the return type matches the concrete subclass.

Python
from typing import TypeVar, Generic
import copy

T = TypeVar("T", bound="CopyableMixin")


class CopyableMixin(Generic[T]):
    def copy(self: T) -> T:
        return copy.copy(self)

    def with_field(self: T, **kwargs) -> T:
        obj = copy.copy(self)
        for key, value in kwargs.items():
            setattr(obj, key, value)
        return obj


class User(CopyableMixin["User"]):
    def __init__(self, name: str, score: int) -> None:
        self.name = name
        self.score = score

    def __repr__(self) -> str:
        return f"User(name={self.name!r}, score={self.score})"


u = User("Alice", 100)
u2 = u.copy()
u3 = u.with_field(score=200)

print(u)
print(f"Copied: {u2}")
print(f"Same object: {u is u2}")
print(f"With score=200: {u3}")
Expected Output
User(name='Alice', score=100)
Copied: User(name='Alice', score=100)
Same object: False
With score=200: User(name='Alice', score=200)
Hints

Hint 1: Define CopyableMixin[T] where T = TypeVar("T", bound="CopyableMixin"). copy() returns T.

Hint 2: with_field returns a copy of self with one field replaced — uses dataclasses.replace style logic.


#6Generic Observer with Typed EventsMedium
GenericTypeVarobservereventcallback

Build a generic Observable[T] with subscribe and notify. Use it with a temperature sensor where the payload type is float.

Python
from typing import TypeVar, Generic, Callable, List

T = TypeVar("T")


class Observable(Generic[T]):
    def __init__(self) -> None:
        self._listeners: List[Callable[[T], None]] = []

    def subscribe(self, callback: Callable[[T], None]) -> None:
        self._listeners.append(callback)

    def notify(self, value: T) -> None:
        for listener in self._listeners:
            listener(value)


class TemperatureSensor:
    def __init__(self) -> None:
        self.changed: Observable[float] = Observable()
        self._temp = 0.0

    @property
    def temperature(self) -> float:
        return self._temp

    @temperature.setter
    def temperature(self, value: float) -> None:
        self._temp = value
        self.changed.notify(value)


sensor = TemperatureSensor()

sensor.changed.subscribe(
    lambda t: print(f"Subscriber 1: temperature changed to {t}")
)
sensor.changed.subscribe(
    lambda t: print(f"Subscriber 2: temperature changed to {t}")
)

# High temp alert — only registered for second reading
def high_temp_alert(t: float) -> None:
    if t > 38.0:
        print(f"High temp alert: {t}")

sensor.temperature = 37.5
sensor.changed.subscribe(high_temp_alert)
sensor.temperature = 39.0
Expected Output
Subscriber 1: temperature changed to 37.5
Subscriber 2: temperature changed to 37.5
Subscriber 1: temperature changed to 39.0
High temp alert: 39.0
Hints

Hint 1: Observable[T] maintains a list of callbacks typed as Callable[[T], None]. T is the event payload type.

Hint 2: subscribe(callback) adds a listener. notify(value) calls all listeners with the typed value.


#7Generic Tree with FoldMedium
GenericTypeVartreefoldrecursion

Build a generic Tree[T] with a fold operation that reduces all node values to a single result using a combining function.

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

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


class TreeNode(Generic[T]):
    def __init__(self, value: T, children: Optional[List["TreeNode[T]"]] = None) -> None:
        self.value = value
        self.children: List[TreeNode[T]] = children or []

    def fold(self, f: Callable[[U, T], U], initial: U) -> U:
        acc = f(initial, self.value)
        for child in self.children:
            acc = child.fold(f, acc)
        return acc

    def map(self, g: Callable[[T], U]) -> "TreeNode[U]":
        return TreeNode(
            g(self.value),
            [child.map(g) for child in self.children],
        )

    def to_list(self) -> List[T]:
        return self.fold(lambda acc, v: acc + [v], [])


#        1
#      / | \
#     2  3  4
#            \
#             5
root: TreeNode[int] = TreeNode(1, [
    TreeNode(2),
    TreeNode(3),
    TreeNode(4, [TreeNode(5)]),
])

print(f"Tree sum: {root.fold(lambda acc, v: acc + v, 0)}")
print(f"Tree product: {root.fold(lambda acc, v: acc * v, 1)}")
print(f"Max value: {root.fold(lambda acc, v: max(acc, v), 0)}")
str_tree = root.map(str)
print(f"Stringify: {str_tree.to_list()}")
Expected Output
Tree sum: 15
Tree product: 120
Max value: 5
Stringify: ['1', '2', '3', '4', '5']
Hints

Hint 1: Node[T] has value: T and children: List[Node[T]]. fold(f, initial) traverses the tree and combines all values.

Hint 2: fold is essentially reduce over all nodes in DFS order — f(accumulator, node_value) at each step.


Hard

#8Generic State MachineHard
GenericTypeVarstate-machinetransitions

Build a generic StateMachine[S] where S is an enum type. Define states and allowed transitions, then drive the machine through events.

Python
from typing import TypeVar, Generic, Dict, Tuple, Optional
from enum import Enum, auto

S = TypeVar("S")


class StateMachine(Generic[S]):
    def __init__(self, initial: S) -> None:
        self._state = initial
        self._transitions: Dict[Tuple, S] = {}

    def add_transition(self, from_state: S, event: str, to_state: S) -> None:
        self._transitions[(from_state, event)] = to_state

    def transition(self, event: str) -> S:
        key = (self._state, event)
        if key not in self._transitions:
            raise ValueError(
                f"Cannot transition from {self._state} via '{event}'"
            )
        self._state = self._transitions[key]
        return self._state

    @property
    def state(self) -> S:
        return self._state


class JobState(Enum):
    IDLE = auto()
    RUNNING = auto()
    PAUSED = auto()
    STOPPED = auto()


machine: StateMachine[JobState] = StateMachine(JobState.IDLE)

# Define allowed transitions
machine.add_transition(JobState.IDLE, "start", JobState.RUNNING)
machine.add_transition(JobState.RUNNING, "pause", JobState.PAUSED)
machine.add_transition(JobState.RUNNING, "stop", JobState.STOPPED)
machine.add_transition(JobState.PAUSED, "resume", JobState.RUNNING)
machine.add_transition(JobState.PAUSED, "stop", JobState.STOPPED)

print(f"Initial state: {machine.state.name}")
print(f"After start: {machine.transition('start').name}")
print(f"After pause: {machine.transition('pause').name}")
print(f"After resume: {machine.transition('resume').name}")
print(f"After stop: {machine.transition('stop').name}")

try:
    machine.transition("start")
except ValueError as e:
    print(f"Invalid transition: {e}")
Expected Output
Initial state: IDLE
After start: RUNNING
After pause: PAUSED
After resume: RUNNING
After stop: STOPPED
Invalid transition: Cannot transition from STOPPED via 'start'
Hints

Hint 1: StateMachine[S] is generic over the state enum type S. Transitions are stored as a dict of (current_state, event) -> new_state.

Hint 2: transition(event) looks up the pair and raises an error if no valid transition exists.


#9Generic Either Type with ChainHard
GenericTypeVarEitherLeftRightchain

Implement Either[L, R] with Left and Right variants. Add chain (flatMap on Right), map, and fold methods for total transformation.

Python
from typing import TypeVar, Generic, Callable, Union

L = TypeVar("L")
R = TypeVar("R")
L2 = TypeVar("L2")
R2 = TypeVar("R2")


class Either(Generic[L, R]):
    pass


class Right(Either[L, R]):
    def __init__(self, value: R) -> None:
        self._value = value

    def map(self, f: Callable[[R], R2]) -> "Right[L, R2]":
        return Right(f(self._value))

    def chain(self, f: Callable[[R], Either[L, R2]]) -> Either[L, R2]:
        return f(self._value)

    def fold(self, left_f: Callable[[L], R2], right_f: Callable[[R], R2]) -> R2:
        return right_f(self._value)

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


class Left(Either[L, R]):
    def __init__(self, error: L) -> None:
        self._error = error

    def map(self, f: Callable[[R], R2]) -> "Left[L, R2]":
        return Left(self._error)  # type: ignore[return-value]

    def chain(self, f: Callable[[R], Either[L, R2]]) -> "Left[L, R2]":
        return Left(self._error)  # type: ignore[return-value]

    def fold(self, left_f: Callable[[L], R2], right_f: Callable[[R], R2]) -> R2:
        return left_f(self._error)

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


def parse_int(s: str) -> Either[str, int]:
    try:
        return Right(int(s))
    except ValueError:
        return Left(f"not a number: {s}")


r = parse_int("42")
l = parse_int("abc")

print(r)
print(l)
print(f"chain Right: {r.chain(lambda x: Right(x + 1))}")
print(f"chain Left: {l.chain(lambda x: Right(x + 1))}")
print(f"fold Right: {r.fold(lambda e: -1, lambda v: v)}")
print(f"fold Left: {l.fold(lambda e: -1, lambda v: v)}")
Expected Output
Right(42)
Left('not a number: abc')
chain Right: Right(43)
chain Left: Left('not a number: abc')
fold Right: 42
fold Left: -1
Hints

Hint 1: Either[L, R] has Left[L, R] and Right[L, R]. chain(f) on Right calls f(value) returning a new Either; on Left it returns self.

Hint 2: fold(left_f, right_f) applies left_f to a Left value and right_f to a Right value.


#10Generic Trie with Type-Safe Prefix SearchHard
GenericTypeVarTrieprefix-searchdata-structure

Build a generic Trie[T] where the key type T is hashable. Implement insert, contains, and words_with_prefix using the generic node structure.

Python
from typing import TypeVar, Generic, Dict, Optional, List, Iterator
from typing import Hashable

T = TypeVar("T", bound=Hashable)


class TrieNode(Generic[T]):
    def __init__(self) -> None:
        self.children: Dict[T, "TrieNode[T]"] = {}
        self.is_end = False


class Trie(Generic[T]):
    def __init__(self) -> None:
        self._root: TrieNode[T] = TrieNode()

    def insert(self, sequence: List[T]) -> None:
        node = self._root
        for item in sequence:
            if item not in node.children:
                node.children[item] = TrieNode()
            node = node.children[item]
        node.is_end = True

    def contains(self, sequence: List[T]) -> bool:
        node = self._root
        for item in sequence:
            if item not in node.children:
                return False
            node = node.children[item]
        return node.is_end

    def _collect(self, node: TrieNode[T], prefix: List[T]) -> Iterator[List[T]]:
        if node.is_end:
            yield list(prefix)
        for item, child in node.children.items():
            prefix.append(item)
            yield from self._collect(child, prefix)
            prefix.pop()

    def words_with_prefix(self, prefix: List[T]) -> List[List[T]]:
        node = self._root
        for item in prefix:
            if item not in node.children:
                return []
            node = node.children[item]
        return list(self._collect(node, list(prefix)))


# String trie — characters as T=str
char_trie: Trie[str] = Trie()

words = ["app", "apple", "application", "banana", "band", "cat"]
for word in words:
    char_trie.insert(list(word))

print(f"Insert {len(words)} words")

app_words = ["".join(w) for w in char_trie.words_with_prefix(list("app"))]
print(f"Words with prefix 'app': {sorted(app_words)}")

ban_words = ["".join(w) for w in char_trie.words_with_prefix(list("ban"))]
print(f"Words with prefix 'ban': {sorted(ban_words)}")

print(f"Contains 'apple': {char_trie.contains(list('apple'))}")
print(f"Contains 'appl': {char_trie.contains(list('appl'))}")
Expected Output
Insert 5 words
Words with prefix 'app': ['app', 'apple', 'application']
Words with prefix 'ban': ['banana', 'band']
Contains 'apple': True
Contains 'appl': False
Hints

Hint 1: Trie[T] stores children as Dict[T, TrieNode[T]]. T must be hashable — use TypeVar("T", bound=Hashable).

Hint 2: insert(sequence) adds each element as a child node. search_prefix(prefix) returns all complete words that start with prefix.


#11Generic Validated Config BuilderHard
GenericTypeVarbuildervalidationconfig

Build a ConfigBuilder[T] that accumulates named fields with validators and produces a typed config object. Collect all validation errors before raising.

Python
from typing import TypeVar, Generic, Dict, Any, List, Callable, Tuple, Optional

T = TypeVar("T")


class ValidationError(Exception):
    pass


class ConfigBuilder(Generic[T]):
    def __init__(self, factory: Callable[..., T]) -> None:
        self._factory = factory
        self._fields: Dict[str, Any] = {}
        self._validators: List[Tuple[str, Callable[[Any], Optional[str]]]] = []

    def set(self, name: str, value: Any) -> "ConfigBuilder[T]":
        self._fields[name] = value
        return self

    def validate(
        self,
        name: str,
        rule: Callable[[Any], Optional[str]],
    ) -> "ConfigBuilder[T]":
        self._validators.append((name, rule))
        return self

    def build(self) -> T:
        errors: List[str] = []
        for name, rule in self._validators:
            value = self._fields.get(name)
            error = rule(value)
            if error:
                errors.append(error)
        if errors:
            raise ValidationError("; ".join(errors))
        return self._factory(**self._fields)


class ServerConfig:
    def __init__(
        self,
        host: str,
        port: int,
        workers: int,
        debug: bool = False,
    ) -> None:
        self.host = host
        self.port = port
        self.workers = workers
        self.debug = debug

    def __repr__(self) -> str:
        return (
            f"ServerConfig(host={self.host!r}, port={self.port}, "
            f"workers={self.workers}, debug={self.debug})"
        )


def build_server_config(**kwargs) -> "ConfigBuilder[ServerConfig]":
    builder: ConfigBuilder[ServerConfig] = ConfigBuilder(ServerConfig)
    for k, v in kwargs.items():
        builder.set(k, v)
    return (
        builder
        .validate("port", lambda p: f"port must be between 1 and 65535, got {p}"
                  if not (1 <= p <= 65535) else None)
        .validate("workers", lambda w: f"workers must be between 1 and 32, got {w}"
                  if not (1 <= w <= 32) else None)
    )


cfg = build_server_config(host="localhost", port=8080, workers=4, debug=True).build()
print(f"Config built: {cfg}")

try:
    build_server_config(host="localhost", port=99999, workers=4).build()
except ValidationError as e:
    print(f"ValidationError: {e}")

try:
    build_server_config(host="localhost", port=8080, workers=0).build()
except ValidationError as e:
    print(f"ValidationError: {e}")
Expected Output
Config built: ServerConfig(host='localhost', port=8080, workers=4, debug=True)
ValidationError: port must be between 1 and 65535, got 99999
ValidationError: workers must be between 1 and 32, got 0
Hints

Hint 1: Use a TypeVar T bound to a dataclass-like structure. ConfigBuilder[T] accumulates fields and validators.

Hint 2: build() runs all validators and raises a combined error listing all failures, or returns the constructed object.

© 2026 EngineersOfAI. All rights reserved.