A design pattern is a tested, reusable solution to a problem programmers face again and again. Instead of reinventing the wheel, you reach for a known recipe â and other developers instantly recognize it.
Patterns aren't code you copy-paste; they're ideas for how to structure your classes. Let's meet three friendly ones.
đ Pattern 1: Factory
A factory is a function (or method) whose job is to create objects for you, so the rest of your code doesn't worry about the details.
def make_animal(kind):
if kind == "dog":
return Dog()
if kind == "cat":
return Cat()
pet = make_animal("dog") # factory decides which class
đ¯ Pattern 2: Strategy
The strategy pattern lets you swap behavior by passing in different functions or objects. Same code, different "strategy."
def add(a, b): return a + b
def multiply(a, b): return a * b
def calculate(a, b, strategy):
return strategy(a, b)
print(calculate(3, 4, add)) # 7
print(calculate(3, 4, multiply)) # 12
đĄ You've used this!
Passing key= to Python's sorted() is the strategy pattern â you give it a strategy for how to compare items.
đŖ Pattern 3: Observer
The observer pattern lets many objects "subscribe" to an event. When something happens, everyone gets notified â like followers getting a post update.
class Channel:
def __init__(self):
self.subscribers = []
def subscribe(self, name):
self.subscribers.append(name)
def upload(self):
return [name + " notified" for name in self.subscribers]
â What you learned
A pattern is a reusable design idea, not copy-paste code.
Factory â a helper that creates objects for you.
Strategy â swap behavior by passing in functions/objects.
Observer â notify many subscribers when an event happens.
đŽ Time to Practice!
Wire up some classic patterns. đ¨
đ
Task 1: Animal Factory
Medium
Write Dog and Cat classes with a speak() method, then a make_animal(kind) factory that returns the right object for "dog" or "cat". The caller never needs to know which class it got.
class Dog:
def speak(self):
return "Woof"
class Cat:
def speak(self):
return "Meow"
def make_animal(kind):
if kind == "dog":
return Dog()
if kind == "cat":
return Cat()
print(make_animal("dog").speak())
print(make_animal("cat").speak())
đ¯
Task 2: Pick a Strategy
Medium
Write two tiny functions, add(a, b) and multiply(a, b), then calculate(a, b, strategy) which runs whichever one it is handed. Call it with 3 and 4 both ways â 7 then 12.
def add(a, b):
return a + b
def multiply(a, b):
return a * b
def calculate(a, b, strategy):
return strategy(a, b)
print(calculate(3, 4, add))
print(calculate(3, 4, multiply))
đŖ
Task 3: Observer Notifications
Challenge
Write a VideoChannel that keeps a list of subscribers, with subscribe(name) and upload() which returns a list like ['Mia notified', 'Leo notified']. Subscribe Mia and Leo, then upload.
class VideoChannel:
def __init__(self):
self.subscribers = []
def subscribe(self, name):
self.subscribers.append(name)
def upload(self):
return [name + " notified" for name in self.subscribers]
channel = VideoChannel()
channel.subscribe("Mia")
channel.subscribe("Leo")
print(channel.upload())
đī¸ Extra Practice â Engineering Reps
Design patterns are recipes other engineers already tested. Cook each one at least once and you will recognize it forever.
1ī¸âŖ
Task 4: Singleton: Only One Allowed
Tricky
Some things should exist exactly once â a game's settings, for example. Write Settings whose __new__ always returns the same instance. Create it twice, change a value through the first, and print the value through the second plus a is b.
9
True
class Settings:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.volume = 5
return cls._instance
a = Settings()
b = Settings()
a.volume = 9
print(b.volume)
print(a is b)
đ
Task 5: Decorator: Wrap a Function
Tricky
A decorator adds behavior around a function without touching its code. Write @shout that takes any function returning text and makes the result UPPERCASE with an exclamation mark. Decorate greet(name) and print greet("ana").
Your app expects .area() but an old library only offers compute_surface(). Write the old class, then an Adapter that wraps it and provides area(). Print the area through the adapter.
42
class OldShape:
def compute_surface(self):
return 42
class ShapeAdapter:
def __init__(self, old_shape):
self.old_shape = old_shape
def area(self):
return self.old_shape.compute_surface()
print(ShapeAdapter(OldShape()).area())
âŠī¸
Task 7: Command: Actions You Can Undo
Tricky
Store actions as objects so you can undo them. Write AddText with run(doc) and undo(doc) (both returning the new text), keep a history list, run two commands, then undo one. Print the document after every step.
Hello
Hello World
Hello
class AddText:
def __init__(self, text):
self.text = text
def run(self, doc):
return doc + self.text
def undo(self, doc):
return doc[:-len(self.text)]
doc = ""
history = []
for command in [AddText("Hello "), AddText("World")]:
doc = command.run(doc)
history.append(command)
print(doc)
doc = history.pop().undo(doc)
print(doc)
đ§ą
Task 8: Template Method: Same Steps, Different Details
Medium
Every game has the same skeleton: start, play, finish. Write a Game base class with a play_game() method that calls start(), turn() and end(), then two subclasses (Chess, Tag) that fill in their own turn(). Play both.
Chess starting
Move a knight
Chess over
Tag starting
Run away!
Tag over
class Game:
def start(self):
print(f"{type(self).__name__} starting")
def turn(self):
raise NotImplementedError
def end(self):
print(f"{type(self).__name__} over\n")
def play_game(self):
self.start()
self.turn()
self.end()
class Chess(Game):
def turn(self):
print("Move a knight")
class Tag(Game):
def turn(self):
print("Run away!")
Chess().play_game()
Tag().play_game()
đ°
Task 9: Boss Level: Factory + Strategy Together
Boss
Build a monster spawner: a make_monster(kind) factory returning Goblin or Dragon objects, where each has an attack_style strategy object with attack(). Spawn one of each and print their attacks. Two patterns, one small program.
Goblin swings a club
Dragon breathes fire
class Melee:
def attack(self):
return "swings a club"
class FireBreath:
def attack(self):
return "breathes fire"
class Monster:
def __init__(self, name, style):
self.name = name
self.style = style
def act(self):
return f"{self.name} {self.style.attack()}"
def make_monster(kind):
if kind == "goblin":
return Monster("Goblin", Melee())
if kind == "dragon":
return Monster("Dragon", FireBreath())
raise ValueError("unknown monster")
for kind in ["goblin", "dragon"]:
print(make_monster(kind).act())