Python Design Patterns in Python —: Practice Problems & Exercises
Practice: Design Patterns in Python — Idiomatic Implementations for Production Code
← Back to lessonEasy
Implement the Singleton pattern using __new__. Verify that cfg1 and cfg2 are the same object and that a mutation via cfg1 is visible through cfg2.
class AppConfig:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not hasattr(self, "_initialised"):
self.debug = False
self._initialised = True
cfg1 = AppConfig()
cfg2 = AppConfig()
cfg1.debug = True
print(cfg1 is cfg2)
print(cfg2.debug)Solution
class AppConfig:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self) -> None:
# Guard prevents re-initialisation on subsequent calls
if not hasattr(self, "_initialised"):
self.debug = False
self.log_level = "INFO"
self._initialised = True
cfg1 = AppConfig()
cfg2 = AppConfig()
print(cfg1 is cfg2) # True — same object
cfg1.debug = True
print(cfg2.debug) # True — same object, mutation visible
print(id(cfg1) == id(cfg2)) # True
# Reset for re-use in tests (show how to break singleton in tests):
AppConfig._instance = None
cfg3 = AppConfig()
print(cfg3 is cfg1) # False — fresh instance after reset
Explanation: __new__ creates the object before __init__ initialises it. By storing the first instance in _instance and returning it on subsequent calls, we ensure only one instance ever exists. The _initialised guard in __init__ prevents fields from being reset every time AppConfig() is called.
class AppConfig:
_instance = None
def __new__(cls):
# TODO: return the existing instance or create one
pass
def __init__(self):
if not hasattr(self, "_initialised"):
self.debug = False
self._initialised = True
cfg1 = AppConfig()
cfg2 = AppConfig()
cfg1.debug = True
print(cfg1 is cfg2)
print(cfg2.debug)Expected Output
True\nTrueHints
Hint 1: In __new__, if cls._instance is None, call super().__new__(cls) and store it.
Hint 2: Always return cls._instance.
Implement a factory function that creates Circle or Rectangle objects based on a string parameter. Use a dispatch dict, not an if/elif chain.
import math
class Circle:
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
_SHAPES = {
"circle": Circle,
"rectangle": Rectangle,
}
def shape_factory(shape_type: str, **kwargs):
if shape_type not in _SHAPES:
raise ValueError(f"Unknown shape: {shape_type}")
return _SHAPES[shape_type](**kwargs)
s1 = shape_factory("circle", radius=5)
s2 = shape_factory("rectangle", width=4, height=6)
print(type(s1).__name__)
print(s2.area())Solution
import math
class Circle:
def __init__(self, radius: float) -> None:
self.radius = radius
def area(self) -> float:
return math.pi * self.radius ** 2
class Triangle:
def __init__(self, base: float, height: float) -> None:
self.base = base
self.height = height
def area(self) -> float:
return 0.5 * self.base * self.height
class Rectangle:
def __init__(self, width: float, height: float) -> None:
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
_SHAPES: dict[str, type] = {
"circle": Circle,
"rectangle": Rectangle,
"triangle": Triangle,
}
def shape_factory(shape_type: str, **kwargs):
if shape_type not in _SHAPES:
raise ValueError(f"Unknown shape '{shape_type}'. Available: {list(_SHAPES)}")
return _SHAPES[shape_type](**kwargs)
s1 = shape_factory("circle", radius=5)
s2 = shape_factory("rectangle", width=4, height=6)
s3 = shape_factory("triangle", base=3, height=8)
print(type(s1).__name__) # Circle
print(s2.area()) # 24.0
print(s3.area()) # 12.0
Explanation: A dispatch dict is the idiomatic Python factory. It is open for extension (add a new entry to _SHAPES) without changing shape_factory. The **kwargs forwarding means the factory works for any constructor signature without needing per-type branching.
class Circle:
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
def shape_factory(shape_type: str, **kwargs):
# TODO: return a Circle or Rectangle based on shape_type
pass
s1 = shape_factory("circle", radius=5)
s2 = shape_factory("rectangle", width=4, height=6)
print(type(s1).__name__)
print(s2.area())Expected Output
Circle\n24.0Hints
Hint 1: Use a dict mapping shape_type to class: {"circle": Circle, "rectangle": Rectangle}.
Hint 2: Look up the class and call it with **kwargs.
Run the Strategy pattern implementation. Observe how swapping the strategy changes behaviour without modifying FileArchiver. Then add a NoCompressor strategy as an extension.
from typing import Protocol
class CompressionStrategy(Protocol):
def compress(self, data: str) -> str: ...
class ZipCompressor:
def compress(self, data: str) -> str:
return f"ZIP({data})"
class GzipCompressor:
def compress(self, data: str) -> str:
return f"GZIP({data})"
class NoCompressor:
def compress(self, data: str) -> str:
return data
class FileArchiver:
def __init__(self, strategy: CompressionStrategy) -> None:
self._strategy = strategy
def archive(self, filename: str) -> str:
return self._strategy.compress(filename)
def set_strategy(self, strategy: CompressionStrategy) -> None:
self._strategy = strategy
archiver = FileArchiver(ZipCompressor())
print(archiver.archive("report.pdf"))
archiver.set_strategy(GzipCompressor())
print(archiver.archive("report.pdf"))Solution
from typing import Protocol
class CompressionStrategy(Protocol):
def compress(self, data: str) -> str: ...
class ZipCompressor:
def compress(self, data: str) -> str:
return f"ZIP({data})"
class GzipCompressor:
def compress(self, data: str) -> str:
return f"GZIP({data})"
class NoCompressor:
"""Null strategy — passes data through unchanged."""
def compress(self, data: str) -> str:
return data
class LZ4Compressor:
"""Extension — FileArchiver needs zero modification."""
def compress(self, data: str) -> str:
return f"LZ4({data})"
class FileArchiver:
def __init__(self, strategy: CompressionStrategy) -> None:
self._strategy = strategy
def archive(self, filename: str) -> str:
return self._strategy.compress(filename)
def set_strategy(self, strategy: CompressionStrategy) -> None:
self._strategy = strategy
archiver = FileArchiver(ZipCompressor())
print(archiver.archive("report.pdf")) # ZIP(report.pdf)
archiver.set_strategy(GzipCompressor())
print(archiver.archive("report.pdf")) # GZIP(report.pdf)
archiver.set_strategy(NoCompressor())
print(archiver.archive("report.pdf")) # report.pdf
archiver.set_strategy(LZ4Compressor())
print(archiver.archive("data.csv")) # LZ4(data.csv)
Explanation: The Strategy pattern encapsulates an interchangeable algorithm behind a Protocol. FileArchiver is closed for modification — you never touch it to add new compression algorithms. The NoCompressor null object is idiomatic: it lets callers opt out of compression without special-casing None checks inside FileArchiver.
from typing import Protocol
class CompressionStrategy(Protocol):
def compress(self, data: str) -> str: ...
class ZipCompressor:
def compress(self, data: str) -> str:
return f"ZIP({data})"
class GzipCompressor:
def compress(self, data: str) -> str:
return f"GZIP({data})"
class FileArchiver:
def __init__(self, strategy: CompressionStrategy):
self._strategy = strategy
def archive(self, filename: str) -> str:
return self._strategy.compress(filename)
def set_strategy(self, strategy: CompressionStrategy) -> None:
self._strategy = strategy
archiver = FileArchiver(ZipCompressor())
print(archiver.archive("report.pdf"))
archiver.set_strategy(GzipCompressor())
print(archiver.archive("report.pdf"))Expected Output
ZIP(report.pdf)\nGZIP(report.pdf)Hints
Hint 1: This is already implemented — run it and trace how the strategy swap works.
Hint 2: set_strategy() replaces the compression algorithm at runtime.
Implement EventEmitter.subscribe() and notify() to complete the Observer pattern. Multiple observers should be called in subscription order.
from typing import Protocol, List
class Observer(Protocol):
def update(self, event: str, data: dict) -> None: ...
class EventEmitter:
def __init__(self) -> None:
self._observers: List = []
def subscribe(self, observer) -> None:
self._observers.append(observer)
def notify(self, event: str, data: dict) -> None:
for obs in self._observers:
obs.update(event, data)
class PrintObserver:
def update(self, event: str, data: dict) -> None:
print(f"Event: {event}, data: {data}")
emitter = EventEmitter()
emitter.subscribe(PrintObserver())
emitter.notify("sale", {"item": "book", "price": 9.99})Solution
from typing import Protocol, List
class Observer(Protocol):
def update(self, event: str, data: dict) -> None: ...
class EventEmitter:
def __init__(self) -> None:
self._observers: List = []
def subscribe(self, observer) -> None:
self._observers.append(observer)
def unsubscribe(self, observer) -> None:
self._observers = [o for o in self._observers if o is not observer]
def notify(self, event: str, data: dict) -> None:
for obs in list(self._observers): # copy to allow unsubscription during notify
obs.update(event, data)
class PrintObserver:
def __init__(self, label: str) -> None:
self.label = label
def update(self, event: str, data: dict) -> None:
print(f"[{self.label}] Event: {event}, data: {data}")
class CountingObserver:
def __init__(self) -> None:
self.count = 0
def update(self, event: str, data: dict) -> None:
self.count += 1
emitter = EventEmitter()
counter = CountingObserver()
emitter.subscribe(PrintObserver("A"))
emitter.subscribe(PrintObserver("B"))
emitter.subscribe(counter)
emitter.notify("sale", {"item": "book", "price": 9.99})
emitter.notify("sale", {"item": "pen", "price": 1.50})
print(f"Total events: {counter.count}") # 2
Explanation: The Observer (Publish/Subscribe) pattern decouples event producers from consumers. EventEmitter knows nothing about what observers do — it just calls update() on each. Adding a new observer type requires zero changes to the emitter. The list() copy in notify() prevents bugs if an observer unsubscribes itself during the notification loop.
from typing import Protocol, List
class Observer(Protocol):
def update(self, event: str, data: dict) -> None: ...
class EventEmitter:
def __init__(self):
self._observers: List = []
def subscribe(self, observer) -> None:
# TODO: add observer to list
pass
def notify(self, event: str, data: dict) -> None:
# TODO: call update on all observers
pass
class PrintObserver:
def update(self, event: str, data: dict) -> None:
print(f"Event: {event}, data: {data}")
emitter = EventEmitter()
emitter.subscribe(PrintObserver())
emitter.notify("sale", {"item": "book", "price": 9.99})Expected Output
Event: sale, data: {'item': 'book', 'price': 9.99}Hints
Hint 1: subscribe() appends observer to self._observers.
Hint 2: notify() iterates self._observers and calls o.update(event, data) on each.
Medium
Compose EncryptedDataSource and CompressedDataSource to produce a source that compresses then encrypts. Observe how nesting wrappers stacks transformations.
class DataSource:
def read(self) -> str:
return "raw data"
class EncryptedDataSource:
def __init__(self, source):
self._source = source
def read(self) -> str:
data = self._source.read()
return f"ENCRYPTED({data})"
class CompressedDataSource:
def __init__(self, source):
self._source = source
def read(self) -> str:
data = self._source.read()
return f"COMPRESSED({data})"
source = CompressedDataSource(DataSource())
encrypted_compressed = EncryptedDataSource(source)
print(encrypted_compressed.read())Solution
from typing import Protocol
class Readable(Protocol):
def read(self) -> str: ...
class DataSource:
def read(self) -> str:
return "raw data"
class EncryptedDataSource:
def __init__(self, source: Readable) -> None:
self._source = source
def read(self) -> str:
return f"ENCRYPTED({self._source.read()})"
class CompressedDataSource:
def __init__(self, source: Readable) -> None:
self._source = source
def read(self) -> str:
return f"COMPRESSED({self._source.read()})"
class LoggedDataSource:
"""Extension — no modification to existing wrappers."""
def __init__(self, source: Readable, label: str = "DS") -> None:
self._source = source
self._label = label
def read(self) -> str:
result = self._source.read()
print(f"[{self._label}] read called")
return result
base = DataSource()
compressed = CompressedDataSource(base)
encrypted = EncryptedDataSource(compressed)
logged = LoggedDataSource(encrypted, label="PIPELINE")
print(logged.read())
# [PIPELINE] read called
# ENCRYPTED(COMPRESSED(raw data))
# Different order:
alt = CompressedDataSource(EncryptedDataSource(base))
print(alt.read()) # COMPRESSED(ENCRYPTED(raw data))
Explanation: The Decorator pattern adds behaviour by wrapping an object, not by subclassing. Each wrapper calls self._source.read() and adds its own transformation. Stacking wrappers in different orders produces different pipelines without touching any existing class. This is the same mechanism Python's built-in @functools.wraps and many I/O stream libraries use.
class DataSource:
def read(self) -> str:
return "raw data"
class EncryptedDataSource:
def __init__(self, source):
self._source = source
def read(self) -> str:
data = self._source.read()
return f"ENCRYPTED({data})"
class CompressedDataSource:
def __init__(self, source):
self._source = source
def read(self) -> str:
data = self._source.read()
return f"COMPRESSED({data})"
# TODO: create a source that is both encrypted AND compressed
# Compress first, then encrypt the compressed result
source = CompressedDataSource(DataSource())
encrypted_compressed = EncryptedDataSource(source)
print(encrypted_compressed.read())Expected Output
ENCRYPTED(COMPRESSED(raw data))Hints
Hint 1: Wrap DataSource in CompressedDataSource, then wrap the result in EncryptedDataSource.
Hint 2: Each wrapper calls self._source.read() to get the inner result, then transforms it.
Run the Registry pattern. Observe that new validators self-register via __init_subclass__. Then add a MinLengthValidator as an extension.
class Validator:
_registry = {}
def __init_subclass__(cls, rule_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if rule_name:
Validator._registry[rule_name] = cls
def validate(self, value) -> bool:
raise NotImplementedError
@classmethod
def get(cls, rule_name: str) -> "Validator":
if rule_name not in cls._registry:
raise KeyError(f"No validator: {rule_name}")
return cls._registry[rule_name]()
class NonEmptyValidator(Validator, rule_name="non_empty"):
def validate(self, value) -> bool:
return bool(value)
class PositiveValidator(Validator, rule_name="positive"):
def validate(self, value) -> bool:
return isinstance(value, (int, float)) and value > 0
v1 = Validator.get("non_empty")
v2 = Validator.get("positive")
print(v1.validate("hello"))
print(v1.validate(""))
print(v2.validate(5))Solution
class Validator:
_registry: dict[str, type] = {}
def __init_subclass__(cls, rule_name: str = "", **kwargs) -> None:
super().__init_subclass__(**kwargs)
if rule_name:
Validator._registry[rule_name] = cls
def validate(self, value) -> bool:
raise NotImplementedError
@classmethod
def get(cls, rule_name: str) -> "Validator":
if rule_name not in cls._registry:
raise KeyError(f"No validator '{rule_name}'. Available: {list(cls._registry)}")
return cls._registry[rule_name]()
@classmethod
def available(cls) -> list:
return list(cls._registry.keys())
class NonEmptyValidator(Validator, rule_name="non_empty"):
def validate(self, value) -> bool:
return bool(value)
class PositiveValidator(Validator, rule_name="positive"):
def validate(self, value) -> bool:
return isinstance(value, (int, float)) and value > 0
class MinLengthValidator(Validator, rule_name="min_length"):
"""Extension — zero changes to Validator."""
def __init__(self, min_len: int = 3) -> None:
self.min_len = min_len
@classmethod # override factory to accept param
def create(cls, min_len: int) -> "MinLengthValidator":
return cls(min_len)
def validate(self, value) -> bool:
return isinstance(value, str) and len(value) >= self.min_len
v1 = Validator.get("non_empty")
v2 = Validator.get("positive")
print(v1.validate("hello")) # True
print(v1.validate("")) # False
print(v2.validate(5)) # True
v3 = MinLengthValidator.create(5)
print(v3.validate("hi")) # False
print(v3.validate("hello")) # True
print(Validator.available())
Explanation: The Registry pattern combined with __init_subclass__ creates a self-populating directory. Each subclass announces itself at class-definition time. Adding a new validator requires only creating a new class — no registration calls, no modification to the base class, no global registry variable to import and mutate manually.
class Validator:
_registry = {}
def __init_subclass__(cls, rule_name: str = "", **kwargs):
super().__init_subclass__(**kwargs)
if rule_name:
Validator._registry[rule_name] = cls
def validate(self, value) -> bool:
raise NotImplementedError
@classmethod
def get(cls, rule_name: str) -> "Validator":
if rule_name not in cls._registry:
raise KeyError(f"No validator: {rule_name}")
return cls._registry[rule_name]()
class NonEmptyValidator(Validator, rule_name="non_empty"):
def validate(self, value) -> bool:
return bool(value)
class PositiveValidator(Validator, rule_name="positive"):
def validate(self, value) -> bool:
return isinstance(value, (int, float)) and value > 0
v1 = Validator.get("non_empty")
v2 = Validator.get("positive")
print(v1.validate("hello"))
print(v1.validate(""))
print(v2.validate(5))Expected Output
True\nFalse\nTrueHints
Hint 1: This is already implemented — run it and add a new LengthValidator(rule_name="min_length") as an extension test.
Hint 2: Observe that adding a validator never touches the Validator base class.
Implement the Builder pattern for a SQL query builder. Each method should return self to enable fluent chaining. The final build() assembles the query string.
class QueryBuilder:
def __init__(self) -> None:
self._table = ""
self._conditions: list[str] = []
self._limit = None
def from_table(self, table: str) -> "QueryBuilder":
self._table = table
return self
def where(self, condition: str) -> "QueryBuilder":
self._conditions.append(condition)
return self
def limit(self, n: int) -> "QueryBuilder":
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
query = (
QueryBuilder()
.from_table("users")
.where("age > 18")
.where("active = true")
.limit(10)
.build()
)
print(query)Solution
class QueryBuilder:
def __init__(self) -> None:
self._table = ""
self._conditions: list[str] = []
self._columns: list[str] = ["*"]
self._order_by: str | None = None
self._limit: int | None = None
def select(self, *columns: str) -> "QueryBuilder":
self._columns = list(columns)
return self
def from_table(self, table: str) -> "QueryBuilder":
self._table = table
return self
def where(self, condition: str) -> "QueryBuilder":
self._conditions.append(condition)
return self
def order_by(self, column: str) -> "QueryBuilder":
self._order_by = column
return self
def limit(self, n: int) -> "QueryBuilder":
self._limit = n
return self
def build(self) -> str:
cols = ", ".join(self._columns)
sql = f"SELECT {cols} FROM {self._table}"
if self._conditions:
sql += " WHERE " + " AND ".join(self._conditions)
if self._order_by:
sql += f" ORDER BY {self._order_by}"
if self._limit is not None:
sql += f" LIMIT {self._limit}"
return sql
query = (
QueryBuilder()
.select("id", "name", "email")
.from_table("users")
.where("age > 18")
.where("active = true")
.order_by("name")
.limit(10)
.build()
)
print(query)
# SELECT id, name, email FROM users WHERE age > 18 AND active = true ORDER BY name LIMIT 10
simple = QueryBuilder().from_table("users").where("age > 18").limit(10).build()
print(simple)
Explanation: The Builder pattern separates complex object construction from its representation. The fluent interface (returning self) makes the API readable and ensures all parts of the query are set before build() assembles the final string. This is the pattern used by SQLAlchemy's select().where().order_by() API.
class QueryBuilder:
def __init__(self):
self._table = ""
self._conditions = []
self._limit = None
def from_table(self, table: str) -> "QueryBuilder":
# TODO: set table and return self
pass
def where(self, condition: str) -> "QueryBuilder":
# TODO: append condition and return self
pass
def limit(self, n: int) -> "QueryBuilder":
# TODO: set limit and return self
pass
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
query = (
QueryBuilder()
.from_table("users")
.where("age > 18")
.where("active = true")
.limit(10)
.build()
)
print(query)Expected Output
SELECT * FROM users WHERE age > 18 AND active = true LIMIT 10Hints
Hint 1: Each method should set the relevant attribute and return self.
Hint 2: Returning self enables the fluent (chaining) interface.
Run the Abstract Factory implementation and observe how render_ui works with both factories without modification. Then add a LinuxFactory as an extension.
from abc import ABC, abstractmethod
class Button(ABC):
@abstractmethod
def render(self) -> str: pass
class Checkbox(ABC):
@abstractmethod
def render(self) -> str: pass
class UIFactory(ABC):
@abstractmethod
def create_button(self) -> Button: pass
@abstractmethod
def create_checkbox(self) -> Checkbox: pass
class MacButton(Button):
def render(self) -> str: return "Mac Button"
class MacCheckbox(Checkbox):
def render(self) -> str: return "Mac Checkbox"
class WinButton(Button):
def render(self) -> str: return "Windows Button"
class WinCheckbox(Checkbox):
def render(self) -> str: return "Windows Checkbox"
class MacFactory(UIFactory):
def create_button(self) -> Button: return MacButton()
def create_checkbox(self) -> Checkbox: return MacCheckbox()
class WindowsFactory(UIFactory):
def create_button(self) -> Button: return WinButton()
def create_checkbox(self) -> Checkbox: return WinCheckbox()
def render_ui(factory: UIFactory) -> None:
btn = factory.create_button()
chk = factory.create_checkbox()
print(btn.render())
print(chk.render())
render_ui(MacFactory())
render_ui(WindowsFactory())Solution
from abc import ABC, abstractmethod
class Button(ABC):
@abstractmethod
def render(self) -> str: pass
class Checkbox(ABC):
@abstractmethod
def render(self) -> str: pass
class UIFactory(ABC):
@abstractmethod
def create_button(self) -> Button: pass
@abstractmethod
def create_checkbox(self) -> Checkbox: pass
class MacButton(Button):
def render(self) -> str: return "Mac Button"
class MacCheckbox(Checkbox):
def render(self) -> str: return "Mac Checkbox"
class WinButton(Button):
def render(self) -> str: return "Windows Button"
class WinCheckbox(Checkbox):
def render(self) -> str: return "Windows Checkbox"
class LinuxButton(Button):
"""Extension: new platform, zero changes to existing code."""
def render(self) -> str: return "Linux Button"
class LinuxCheckbox(Checkbox):
def render(self) -> str: return "Linux Checkbox"
class MacFactory(UIFactory):
def create_button(self) -> Button: return MacButton()
def create_checkbox(self) -> Checkbox: return MacCheckbox()
class WindowsFactory(UIFactory):
def create_button(self) -> Button: return WinButton()
def create_checkbox(self) -> Checkbox: return WinCheckbox()
class LinuxFactory(UIFactory):
def create_button(self) -> Button: return LinuxButton()
def create_checkbox(self) -> Checkbox: return LinuxCheckbox()
def render_ui(factory: UIFactory) -> None:
btn = factory.create_button()
chk = factory.create_checkbox()
print(btn.render())
print(chk.render())
for factory in [MacFactory(), WindowsFactory(), LinuxFactory()]:
render_ui(factory)
print("---")
Explanation: Abstract Factory creates families of related objects. render_ui is fully platform-agnostic — it works with any UIFactory without modification. Adding Linux support required only three new classes. The factory guarantees consistency: a MacFactory will never accidentally return a WinButton.
from abc import ABC, abstractmethod
class Button(ABC):
@abstractmethod
def render(self) -> str: pass
class Checkbox(ABC):
@abstractmethod
def render(self) -> str: pass
class UIFactory(ABC):
@abstractmethod
def create_button(self) -> Button: pass
@abstractmethod
def create_checkbox(self) -> Checkbox: pass
class MacButton(Button):
def render(self) -> str: return "Mac Button"
class MacCheckbox(Checkbox):
def render(self) -> str: return "Mac Checkbox"
class WinButton(Button):
def render(self) -> str: return "Windows Button"
class WinCheckbox(Checkbox):
def render(self) -> str: return "Windows Checkbox"
class MacFactory(UIFactory):
def create_button(self) -> Button: return MacButton()
def create_checkbox(self) -> Checkbox: return MacCheckbox()
class WindowsFactory(UIFactory):
def create_button(self) -> Button: return WinButton()
def create_checkbox(self) -> Checkbox: return WinCheckbox()
def render_ui(factory: UIFactory) -> None:
btn = factory.create_button()
chk = factory.create_checkbox()
print(btn.render())
print(chk.render())
render_ui(MacFactory())
render_ui(WindowsFactory())Expected Output
Mac Button\nMac Checkbox\nWindows Button\nWindows CheckboxHints
Hint 1: This is already complete — run it and add a LinuxFactory as an extension.
Hint 2: The key insight: render_ui() never knows which platform it is building for.
Hard
Implement a thread-safe Singleton using a metaclass and double-checked locking. Ten threads should all receive the exact same instance.
import threading
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class DatabasePool(metaclass=SingletonMeta):
def __init__(self):
if not hasattr(self, "_initialised"):
self.connections = []
self._initialised = True
results = []
def create_instance():
results.append(DatabasePool())
threads = [threading.Thread(target=create_instance) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(len(set(id(r) for r in results)) == 1)Solution
import threading
class SingletonMeta(type):
_instances: dict = {}
_lock: threading.Lock = threading.Lock()
def __call__(cls, *args, **kwargs):
# Fast path: already created, no lock needed
if cls not in cls._instances:
# Slow path: acquire lock and double-check
with cls._lock:
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
class DatabasePool(metaclass=SingletonMeta):
def __init__(self) -> None:
if not hasattr(self, "_initialised"):
self.connections: list = []
self._initialised = True
def add_connection(self, conn: str) -> None:
self.connections.append(conn)
# Thread safety test:
results: list = []
def create_instance() -> None:
results.append(DatabasePool())
threads = [threading.Thread(target=create_instance) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
unique_ids = set(id(r) for r in results)
print(len(unique_ids) == 1) # True — all threads got the same instance
# Metaclass supports multiple singleton classes:
class ConfigManager(metaclass=SingletonMeta):
def __init__(self) -> None:
if not hasattr(self, "_initialised"):
self.settings: dict = {}
self._initialised = True
cfg1 = ConfigManager()
cfg2 = ConfigManager()
print(cfg1 is cfg2) # True
print(DatabasePool() is ConfigManager()) # False — different classes
Explanation: The metaclass approach is cleaner than __new__ because it intercepts __call__ at the class level, applying to every class that uses SingletonMeta without code duplication. Double-checked locking is the standard thread-safe pattern: the outer check avoids locking on every call (performance), while the inner check prevents a race condition between two threads that both passed the outer check simultaneously.
import threading
class SingletonMeta(type):
_instances = {}
_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
# TODO: implement thread-safe singleton creation
# Use double-checked locking
pass
class DatabasePool(metaclass=SingletonMeta):
def __init__(self):
if not hasattr(self, "_initialised"):
self.connections = []
self._initialised = True
results = []
def create_instance():
results.append(DatabasePool())
threads = [threading.Thread(target=create_instance) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(len(set(id(r) for r in results)) == 1) # all same instanceExpected Output
TrueHints
Hint 1: Double-checked locking: first check cls._instances outside the lock (fast path), then check again inside the lock (slow path).
Hint 2: Use with cls._lock: to acquire the lock before creating the instance.
Implement Directory.size() and Directory.display() for the Composite pattern. Both methods should work recursively for arbitrary nesting depth.
from abc import ABC, abstractmethod
from typing import List
class FileSystemItem(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def size(self) -> int: pass
@abstractmethod
def display(self, indent: int = 0) -> None: pass
class File(FileSystemItem):
def __init__(self, name: str, size_bytes: int):
super().__init__(name)
self._size = size_bytes
def size(self) -> int:
return self._size
def display(self, indent: int = 0) -> None:
print(" " * indent + f"- {self.name} ({self._size}B)")
class Directory(FileSystemItem):
def __init__(self, name: str):
super().__init__(name)
self._children: List[FileSystemItem] = []
def add(self, item: FileSystemItem) -> None:
self._children.append(item)
def size(self) -> int:
return sum(child.size() for child in self._children)
def display(self, indent: int = 0) -> None:
print(" " * indent + f"[{self.name}]")
for child in self._children:
child.display(indent + 2)
root = Directory("root")
src = Directory("src")
src.add(File("main.py", 1024))
src.add(File("utils.py", 512))
root.add(src)
root.add(File("README.md", 256))
root.display()
print(f"Total: {root.size()}B")Solution
from abc import ABC, abstractmethod
from typing import List
class FileSystemItem(ABC):
def __init__(self, name: str) -> None:
self.name = name
@abstractmethod
def size(self) -> int: pass
@abstractmethod
def display(self, indent: int = 0) -> None: pass
class File(FileSystemItem):
def __init__(self, name: str, size_bytes: int) -> None:
super().__init__(name)
self._size = size_bytes
def size(self) -> int:
return self._size
def display(self, indent: int = 0) -> None:
print(" " * indent + f"- {self.name} ({self._size}B)")
class Directory(FileSystemItem):
def __init__(self, name: str) -> None:
super().__init__(name)
self._children: List[FileSystemItem] = []
def add(self, item: FileSystemItem) -> None:
self._children.append(item)
def remove(self, item: FileSystemItem) -> None:
self._children.remove(item)
def size(self) -> int:
return sum(child.size() for child in self._children)
def display(self, indent: int = 0) -> None:
print(" " * indent + f"[{self.name}]")
for child in self._children:
child.display(indent + 2)
# Build tree
root = Directory("root")
src = Directory("src")
tests = Directory("tests")
src.add(File("main.py", 1024))
src.add(File("utils.py", 512))
tests.add(File("test_main.py", 768))
root.add(src)
root.add(tests)
root.add(File("README.md", 256))
root.display()
print(f"Total: {root.size()}B") # 1024+512+768+256 = 2560B
Explanation: The Composite pattern lets you treat individual objects and compositions uniformly. size() on a Directory recursively sums its children — each of which may themselves be directories. display() recursively increases the indent level. Client code (root.size(), root.display()) works identically whether the item is a file or a deeply nested directory tree.
from abc import ABC, abstractmethod
from typing import List
class FileSystemItem(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def size(self) -> int: pass
@abstractmethod
def display(self, indent: int = 0) -> None: pass
class File(FileSystemItem):
def __init__(self, name: str, size_bytes: int):
super().__init__(name)
self._size = size_bytes
def size(self) -> int:
return self._size
def display(self, indent: int = 0) -> None:
print(" " * indent + f"- {self.name} ({self._size}B)")
class Directory(FileSystemItem):
def __init__(self, name: str):
super().__init__(name)
self._children: List[FileSystemItem] = []
def add(self, item: FileSystemItem) -> None:
self._children.append(item)
def size(self) -> int:
# TODO: return sum of all children sizes
pass
def display(self, indent: int = 0) -> None:
# TODO: print directory name, then recursively display children
pass
root = Directory("root")
src = Directory("src")
src.add(File("main.py", 1024))
src.add(File("utils.py", 512))
root.add(src)
root.add(File("README.md", 256))
root.display()
print(f"Total: {root.size()}B")Expected Output
[root]\n [src]\n - main.py (1024B)\n - utils.py (512B)\n - README.md (256B)\nTotal: 1792BHints
Hint 1: size() should return sum(child.size() for child in self._children).
Hint 2: display() should print the directory name with indent, then call child.display(indent+2) for each child.
Implement CommandHistory.execute() and CommandHistory.undo() to build a working undo/redo stack. Each command encapsulates an action and its inverse.
from typing import Protocol, List
class Command(Protocol):
def execute(self) -> None: ...
def undo(self) -> None: ...
class TextEditor:
def __init__(self):
self.text = ""
class AppendCommand:
def __init__(self, editor: TextEditor, text: str):
self._editor = editor
self._text = text
def execute(self) -> None:
self._editor.text += self._text
def undo(self) -> None:
self._editor.text = self._editor.text[:-len(self._text)]
class CommandHistory:
def __init__(self) -> None:
self._history: List = []
def execute(self, cmd) -> None:
cmd.execute()
self._history.append(cmd)
def undo(self) -> None:
if self._history:
cmd = self._history.pop()
cmd.undo()
editor = TextEditor()
history = CommandHistory()
history.execute(AppendCommand(editor, "Hello"))
history.execute(AppendCommand(editor, " World"))
print(editor.text)
history.undo()
print(editor.text)
history.undo()
print(editor.text)Solution
from typing import Protocol, List
class Command(Protocol):
def execute(self) -> None: ...
def undo(self) -> None: ...
class TextEditor:
def __init__(self) -> None:
self.text = ""
class AppendCommand:
def __init__(self, editor: TextEditor, text: str) -> None:
self._editor = editor
self._text = text
def execute(self) -> None:
self._editor.text += self._text
def undo(self) -> None:
self._editor.text = self._editor.text[: -len(self._text)]
class ReplaceCommand:
def __init__(self, editor: TextEditor, old: str, new: str) -> None:
self._editor = editor
self._old = old
self._new = new
def execute(self) -> None:
self._editor.text = self._editor.text.replace(self._old, self._new)
def undo(self) -> None:
self._editor.text = self._editor.text.replace(self._new, self._old)
class CommandHistory:
def __init__(self) -> None:
self._history: List = []
self._redo_stack: List = []
def execute(self, cmd) -> None:
cmd.execute()
self._history.append(cmd)
self._redo_stack.clear() # branching: clear redo after new command
def undo(self) -> None:
if self._history:
cmd = self._history.pop()
cmd.undo()
self._redo_stack.append(cmd)
def redo(self) -> None:
if self._redo_stack:
cmd = self._redo_stack.pop()
cmd.execute()
self._history.append(cmd)
editor = TextEditor()
history = CommandHistory()
history.execute(AppendCommand(editor, "Hello"))
history.execute(AppendCommand(editor, " World"))
print(editor.text) # Hello World
history.undo()
print(editor.text) # Hello
history.redo()
print(editor.text) # Hello World
history.execute(ReplaceCommand(editor, "World", "Python"))
print(editor.text) # Hello Python
history.undo()
print(editor.text) # Hello World
history.undo()
print(editor.text) # Hello
history.undo()
print(editor.text) # (empty)
Explanation: The Command pattern encapsulates each action as an object with execute() and undo(). CommandHistory stores a stack of executed commands — undoing pops from the stack and calls undo(), redoing replays from a redo stack. Adding new command types (like ReplaceCommand) never touches CommandHistory. This is the exact pattern behind Ctrl+Z in text editors and transaction logs in databases.
from typing import Protocol, List
class Command(Protocol):
def execute(self) -> None: ...
def undo(self) -> None: ...
class TextEditor:
def __init__(self):
self.text = ""
class AppendCommand:
def __init__(self, editor: TextEditor, text: str):
self._editor = editor
self._text = text
def execute(self) -> None:
self._editor.text += self._text
def undo(self) -> None:
self._editor.text = self._editor.text[:-len(self._text)]
class CommandHistory:
def __init__(self):
self._history: List = []
def execute(self, cmd: Command) -> None:
# TODO: execute command and push to history
pass
def undo(self) -> None:
# TODO: pop last command and undo it (if history is not empty)
pass
editor = TextEditor()
history = CommandHistory()
history.execute(AppendCommand(editor, "Hello"))
history.execute(AppendCommand(editor, " World"))
print(editor.text)
history.undo()
print(editor.text)
history.undo()
print(editor.text)Expected Output
Hello World\nHello\nHints
Hint 1: In execute(), call cmd.execute() then append cmd to self._history.
Hint 2: In undo(), pop the last command and call its undo() method.
