Python Advanced Generic Patterns Practice Problems & Exercises
Practice: Advanced Generic Patterns
← Back to lessonEasy
Build a Box[T] generic container with map and apply (applicative functor-style) methods.
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: 11Hints
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.
Build a QueryBuilder that uses Self-typed returns so that subclass builders also return the correct type in method chains.
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 10Hints
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.
Build Pair[A, B] where swap() returns Pair[B, A], correctly modeling the type flip in the return annotation.
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
Implement a generic LinkedList[T] with append, to_list, reverse, and map methods using a recursive node structure.
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.
Build a CopyableMixin[T] that provides copy() and with_field() methods using a bounded TypeVar so the return type matches the concrete subclass.
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.
Build a generic Observable[T] with subscribe and notify. Use it with a temperature sensor where the payload type is float.
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.0Expected 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.0Hints
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.
Build a generic Tree[T] with a fold operation that reduces all node values to a single result using a combining function.
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
Build a generic StateMachine[S] where S is an enum type. Define states and allowed transitions, then drive the machine through events.
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.
Implement Either[L, R] with Left and Right variants. Add chain (flatMap on Right), map, and fold methods for total transformation.
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: -1Hints
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.
Build a generic Trie[T] where the key type T is hashable. Implement insert, contains, and words_with_prefix using the generic node structure.
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': FalseHints
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.
Build a ConfigBuilder[T] that accumulates named fields with validators and produces a typed config object. Collect all validation errors before raising.
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 0Hints
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.
