Design Patterns in Python

Expert · 17 min read · ▶ live playground · ✦ checkpoint

A design pattern can make you sound senior and still make your code worse. The trick is not memorizing pattern names. The trick is spotting when a small, ordinary Python shape solves the problem cleanly.

Patterns are shared names

A design pattern is a reusable name for a common code shape. It is not a law, a package, or a badge of seriousness. Think of a pattern like a sketch pinned to the workshop wall: "when the problem looks like this, this shape often helps."

Python changes the feel of many classic patterns. Because functions can be passed around as values, modules can hold state, and objects can share method names without sharing a parent class, Pythonic code, meaning clear Python that uses expected Python habits, is often smaller than the version you would write in Java or C++.

The question is always: does the pattern make the next change easier to make and easier to read?

Factory: choose what to build

A Factory is code that chooses and creates the right object for a request. It acts like a front-desk clerk: you say what you need, and it hands you the right tool without making you walk through the storage room.

Here, the rest of the program asks for a reward by name. It does not need to know which class builds that reward.

class StickerReward:
    def __init__(self, label):
        self.label = label
 
    def message_for(self, player):
        return f"{player} earned sticker: {self.label}"
 
 
class BadgeReward:
    def __init__(self, title):
        self.title = title
 
    def message_for(self, player):
        return f"{player} earned badge: {self.title}"
 
 
def make_reward(kind):
    if kind == "focus":
        return StickerReward("deep focus")
    if kind == "helper":
        return BadgeReward("team helper")
    raise ValueError(f"unknown reward kind: {kind}")
 
 
reward = make_reward("helper")
print(reward.message_for("Maya"))

Maya earned badge: team helper

The useful part is not the word "Factory." The useful part is the boundary. The choosing logic lives in one place. If tomorrow you add a TrophyReward, the calling code can keep asking make_reward("trophy").

Factory adds noise when it hides nothing. If your program has one class and no decision to make, calling the class directly is clearer.

Strategy: swap the rule

A Strategy is an interchangeable behavior you pass in or store so code can use one rule today and another rule tomorrow. In Python, a strategy is often just a function.

This score report can use a generous rule or a strict rule without rewriting the report itself.

def generous_points(minutes):
    return minutes * 3 + 10
 
 
def strict_points(minutes):
    return minutes * 2
 
 
def print_score(player, minutes, scoring_rule):
    points = scoring_rule(minutes)
    print(f"{player}: {points} points")
 
 
print_score("Noor", 12, generous_points)
print_score("Noor", 12, strict_points)

Noor: 46 points
Noor: 24 points

You already know the pieces: functions can be values, and parameters can receive values. Strategy just gives that shape a name. It shines when you have several real rules and want the caller to choose one.

Try it - factory plus strategy

Run this, then change reward_kind to "focus" or scoring_rule to strict_points. Notice which code changes and which code stays still.

python — playgroundlive
⌘/Ctrl + Enter to run

Observer: announce what happened

An Observer is code that lets interested functions or objects subscribe to an event and get notified when it happens. An event is a small value that describes something that happened, like "Maya scored 5 points." A listener is a function or object that gets called when that event happens.

This shape keeps the scoreboard focused on scores. Other code can react without being baked into the scoreboard class.

class ScoreBoard:
    def __init__(self):
        self.scores = {}
        self.listeners = []
 
    def subscribe(self, listener):
        self.listeners.append(listener)
 
    def add_points(self, player, points):
        old_score = self.scores.get(player, 0)
        new_score = old_score + points
        self.scores[player] = new_score
 
        event = {"player": player, "points": points, "total": new_score}
        for listener in self.listeners:
            listener(event)
 
 
def print_congrats(event):
    print(f"{event['player']} scored {event['points']} points")
 
 
def print_total(event):
    print(f"Total now: {event['total']}")
 
 
board = ScoreBoard()
board.subscribe(print_congrats)
board.subscribe(print_total)
board.add_points("Nia", 5)

Nia scored 5 points
Total now: 5

Observer helps when several things may react to the same event: logging, notifications, UI updates, audit trails, or test probes. It adds noise when there is only one obvious next step. A direct function call is allowed to stay direct.

Singleton-shaped choices

A Singleton is a design choice where a program tries to keep exactly one shared instance of something. Python can do that with advanced class machinery, but you usually do not need to start there.

The Pythonic version is often a module-level object, meaning a value created directly in a .py file outside any function or class. You create one settings object, import it where needed, and treat it as shared program state.

class Settings:
    def __init__(self, theme, max_attempts):
        self.theme = theme
        self.max_attempts = max_attempts
 
 
SETTINGS = Settings("dark", 3)
 
 
def show_game_header(player):
    print(f"{player} plays in {SETTINGS.theme} mode")
    print(f"Attempts: {SETTINGS.max_attempts}")
 
 
show_game_header("Iris")

Iris plays in dark mode
Attempts: 3

This is useful for stable setup values. It is risky for changing state, because hidden shared data makes tests and debugging harder. If each game, user, or request needs its own settings, pass a settings object in instead of reaching for one shared instance.

A practical pattern test

Before naming a pattern, ask four questions:

  • What change does this make easier?
  • What code becomes less tangled?
  • Can a beginner on the team still follow the flow?
  • Would a plain function, dictionary, or module-level object say the same thing with less ceremony?

Patterns are tools, not trophies. You use Factory when creation choices are spreading. You use Strategy when rules need to swap. You use Observer when several listeners react to one event. You use Singleton-shaped state only when one shared object is honest.

Checkpoint

Answer all three to mark this lesson complete

Design patterns give you names for useful shapes. Next, you zoom out from named shapes to design principles: the habits that keep whole systems changeable, testable, and calm.

+50 XP on completion