Skip to main content

Python Design Patterns in Python —: Practice Problems & Exercises

Practice: Design Patterns in Python — Idiomatic Implementations for Production Code

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1Implement a SingletonEasy
singletoncreational

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.

Python
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\nTrue
Hints

Hint 1: In __new__, if cls._instance is None, call super().__new__(cls) and store it.

Hint 2: Always return cls._instance.


#2Simple Factory FunctionEasy
factorycreational

Implement a factory function that creates Circle or Rectangle objects based on a string parameter. Use a dispatch dict, not an if/elif chain.

Python
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.0
Hints

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.


#3Strategy PatternEasy
strategybehaviouraltyping.Protocol

Run the Strategy pattern implementation. Observe how swapping the strategy changes behaviour without modifying FileArchiver. Then add a NoCompressor strategy as an extension.

Python
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.


#4Observer Pattern — Event SubscriptionEasy
observerbehavioural

Implement EventEmitter.subscribe() and notify() to complete the Observer pattern. Multiple observers should be called in subscription order.

Python
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

#5Decorator Pattern — Stacking WrappersMedium
decoratorstructuralcomposition

Compose EncryptedDataSource and CompressedDataSource to produce a source that compresses then encrypts. Observe how nesting wrappers stacks transformations.

Python
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.


#6Registry PatternMedium
registrycreational__init_subclass__

Run the Registry pattern. Observe that new validators self-register via __init_subclass__. Then add a MinLengthValidator as an extension.

Python
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\nTrue
Hints

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.


#7Builder Pattern — Fluent InterfaceMedium
buildercreationalfluent-interface

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.

Python
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 10
Hints

Hint 1: Each method should set the relevant attribute and return self.

Hint 2: Returning self enables the fluent (chaining) interface.


#8Abstract Factory — Cross-Platform UIMedium
abstract-factorycreational

Run the Abstract Factory implementation and observe how render_ui works with both factories without modification. Then add a LinuxFactory as an extension.

Python
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 Checkbox
Hints

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

#9Singleton Thread Safety with metaclassHard
singletonmetaclassthreading

Implement a thread-safe Singleton using a metaclass and double-checked locking. Ten threads should all receive the exact same instance.

Python
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 instance
Expected Output
True
Hints

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.


#10Composite Pattern — File System TreeHard
compositestructuralrecursion

Implement Directory.size() and Directory.display() for the Composite pattern. Both methods should work recursively for arbitrary nesting depth.

Python
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: 1792B
Hints

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.


#11Command Pattern — Undo/Redo StackHard
commandbehaviouralundo-redo

Implement CommandHistory.execute() and CommandHistory.undo() to build a working undo/redo stack. Each command encapsulates an action and its inverse.

Python
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\n
Hints

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.

© 2026 EngineersOfAI. All rights reserved.