Imagine three different students in your class are asked to write a program for the school canteen's ordering counter. Priya writes one giant function with fifteen nested if statements. Aman copies and pastes the same "create an order, print a receipt" code seven times for seven different snacks. Riya writes something clean that a new programmer can read in thirty seconds and extend without breaking anything. All three programs might produce correct output today. But six months later, when the canteen adds five new snacks, Priya's function becomes unreadable, Aman's file balloons with duplicated bugs, and Riya's code takes ten minutes to update. Riya didn't get lucky — she used a design pattern, a tested shape for organizing code that experienced programmers reach for again and again because it survives change well.
This chapter is about learning to recognize and build three of the most useful patterns: Singleton, Factory, and Observer. You will write real, runnable Python for each one, trace exactly what happens line by line, and see why the "obvious" way of writing the code quietly breaks as a program grows.
What a Design Pattern Actually Is
A design pattern is not a piece of code you download and paste into your project. It is a named, reusable strategy for solving a problem that keeps showing up in different programs — the way "carry the 1" is a named strategy for addition that works whether you're adding 47+38 or 999+1. The pattern tells you the shape of the solution; you still write the actual code yourself, adapted to your problem.
The idea was made famous in 1994 by four computer scientists — Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, often nicknamed the "Gang of Four" — in a book called Design Patterns: Elements of Reusable Object-Oriented Software. They catalogued 23 recurring patterns and sorted them into three families based on what problem they solve:
- Creational patterns (5 of the 23, including Singleton and Factory Method) — control how objects get created.
- Structural patterns (7 of the 23) — control how objects are combined into larger structures.
- Behavioral patterns (11 of the 23, including Observer) — control how objects communicate with each other.
A common misconception is that design patterns are a checklist to tick off — "a good program must use five patterns." That's backwards. A pattern is worth using only when it solves a real problem you actually have; sprinkling patterns into simple code just to look advanced makes the code harder to read, not better. Keep that caution in mind as you go through the three patterns below — each one solves one specific, recognizable problem, and each one is a bad idea when that problem isn't present.
A 30-Second Refresher: Classes and Objects
Since every pattern below is built from classes, here's the one-line refresher you need. A class is a blueprint — like the design for "Car" — and an object is one actual thing built from that blueprint, like your uncle's specific white Maruti Swift. In Python:
class Car:
def __init__(self, colour):
self.colour = colour # data that belongs to THIS object
my_car = Car("white") # my_car is an object (an "instance") of class Car
print(my_car.colour) # white
Every time you write Car("white"), Python normally builds a brand-new, separate object in memory. That "normally" is exactly the assumption our first pattern breaks on purpose.
Pattern 1 — Singleton: When There Must Be Exactly One
Think about your school's official attendance register. There is exactly one — not a photocopy per teacher, because if five teachers kept five separate registers, they'd disagree about who was present. The whole point of a register is that everyone reads and writes the same one.
Now suppose we model a live vote counter for the school election using an ordinary class:
class VoteCounter:
def __init__(self):
self.total = 0
booth1 = VoteCounter()
booth2 = VoteCounter()
booth1.total = 120
print(booth1.total) # 120
print(booth2.total) # 0
print(id(booth1) == id(booth2)) # False
Trace it: booth1 = VoteCounter() builds one fresh object in memory with total = 0. booth2 = VoteCounter() builds a second, independent object, also starting at total = 0 — Python has no idea these two are supposed to represent the same election. When we set booth1.total = 120, only booth1's copy changes. booth2.total is still 0, and id(booth1) == id(booth2) — comparing their actual memory addresses — prints False, confirming they are two separate objects. If real vote-counting code accidentally created a second counter like this, half the votes would silently vanish from the "wrong" object.
The Singleton pattern fixes this by making the class itself refuse to create more than one object. We override Python's __new__ method — the method that actually allocates a new object, called just before __init__ fills it in:
class SingleVoteCounter:
_instance = None # will hold the one-and-only object once created
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.total = 0
return cls._instance
booth1 = SingleVoteCounter()
booth2 = SingleVoteCounter()
booth1.total = 120
print(booth2.total) # 120
print(id(booth1) == id(booth2)) # True
Trace it carefully. booth1 = SingleVoteCounter() calls __new__. Since cls._instance is still None, we build a real object with super().__new__(cls), store it in the class-level variable _instance, set its total to 0, and return it — booth1 now points to this object. Next, booth2 = SingleVoteCounter() calls __new__ again, but this time cls._instance is no longer None — it's the object created a moment ago — so the if block is skipped entirely, and the same object is returned. booth2 is not a new object; it's just another name pointing at booth1's object. So when we set booth1.total = 120, we're editing the only object that exists, and booth2.total reads 120 too, because there was never a second object to read from. id(booth1) == id(booth2) is True because they are, literally, the same object at the same memory address.
Notice what changed structurally: with the plain class, two variable names produced two boxes. With the Singleton, two variable names produced one box with two arrows pointing at it. That single-box guarantee is the entire pattern — useful whenever a program must have exactly one shared source of truth, such as one active database connection, one game's score tracker, or one configuration object read by every part of an app.
Pattern 2 — Factory: Let One Place Decide What to Build
Picture the canteen counter again. A customer says "dosa" and food appears — the customer never needs to know that a dosa requires spreading batter thin on a hot tawa, while a samosa requires deep-frying a stuffed pastry. The ordering logic (what was asked for) is separate from the preparation logic (how each item is actually made).
Now think about how a program without this separation tends to get written. Every time the program needs a snack object, it repeats the same decision:
order_name = "dosa"
if order_name == "samosa":
item = Samosa()
elif order_name == "dosa":
item = Dosa()
elif order_name == "sandwich":
item = Sandwich()
This block looks harmless once. The problem appears when this exact if/elif chain gets copy-pasted in twelve different places in the program — the billing screen, the kitchen display, the daily report — because each of those places also needs to turn an order name into the right object. Add a new snack, and you must remember to update all twelve copies. Miss one, and that screen silently breaks for the new snack.
The Factory pattern fixes this by putting the decision in exactly one function that everyone else calls:
class Snack:
def prepare(self):
raise NotImplementedError
class Samosa(Snack):
def prepare(self):
return "Deep-frying a triangular pastry stuffed with spiced potato"
class Dosa(Snack):
def prepare(self):
return "Spreading rice-lentil batter thin on a hot tawa"
class Sandwich(Snack):
def prepare(self):
return "Layering vegetables between grilled bread slices"
def snack_factory(order_name):
order_name = order_name.lower()
if order_name == "samosa":
return Samosa()
elif order_name == "dosa":
return Dosa()
elif order_name == "sandwich":
return Sandwich()
else:
raise ValueError(f"Canteen does not serve {order_name}")
order = snack_factory("dosa")
print(order.prepare())
Trace it: snack_factory("dosa") runs, lower-cases the input (still "dosa"), matches the second branch, and returns a fresh Dosa() object. That object is stored in order. Calling order.prepare() runs Dosa.prepare, which returns the string "Spreading rice-lentil batter thin on a hot tawa". print displays exactly that. Now, every screen in the program calls snack_factory(name) instead of repeating the if/elif chain — add a new snack, and you edit one function, once.
The version above is often called a Simple Factory — a single function that branches on a name. The Gang of Four's book actually describes a slightly stronger version called Factory Method, where instead of one function with a branch, different subclasses each override a method to decide what they build. Here's the same idea reshaped that way — imagine the canteen has a South Indian counter and a North Indian counter, and you simply ask whichever counter you're standing at to make you "the snack it makes":
class CanteenCounter:
def make_snack(self):
raise NotImplementedError # each counter overrides this
class SouthIndianCounter(CanteenCounter):
def make_snack(self):
return Dosa()
class NorthIndianCounter(CanteenCounter):
def make_snack(self):
return Samosa()
counter = SouthIndianCounter()
snack = counter.make_snack()
print(snack.prepare())
Trace it: counter is a SouthIndianCounter object. counter.make_snack() runs that class's version of make_snack (not the parent's, since it's overridden), which returns a Dosa() object. snack.prepare() then runs exactly as before, printing "Spreading rice-lentil batter thin on a hot tawa". The difference from Simple Factory: instead of one function checking a string, the choice of what to build is baked into which subclass you're using — useful when "what gets built" is tied to a whole family of behaviour, not just a name lookup.
Pattern 3 — Observer: Automatic Notifications Without Asking
During a cricket match, thousands of people want live score updates. They don't each walk up and ask the scorer "what's the score now?" every ten seconds — instead, they subscribe once, and updates get pushed to them automatically the moment the score changes. That is precisely the shape of the Observer pattern: one object (the subject) keeps a list of interested objects (the observers) and calls a fixed method on each of them whenever its own state changes.
class ScoreBoard:
def __init__(self):
self._subscribers = []
self._score = "0/0"
def subscribe(self, observer):
self._subscribers.append(observer)
def update_score(self, new_score):
self._score = new_score
for observer in self._subscribers:
observer.notify(self._score)
class SMSAlert:
def __init__(self, name):
self.name = name
def notify(self, score):
print(f"SMS to {self.name}: Score updated to {score}")
class AppAlert:
def __init__(self, name):
self.name = name
def notify(self, score):
print(f"App push to {self.name}: Score is now {score}")
board = ScoreBoard()
board.subscribe(SMSAlert("Riya"))
board.subscribe(AppAlert("Karan"))
board.update_score("45/2 in 8 overs")
Trace it line by line. board = ScoreBoard() creates one object with an empty _subscribers list and _score = "0/0". board.subscribe(SMSAlert("Riya")) creates an SMSAlert object with name = "Riya" and appends it to _subscribers — the list now has one item. The next line does the same for an AppAlert named Karan — the list now has two items. Finally, board.update_score("45/2 in 8 overs") sets self._score to that string, then loops over _subscribers in the order they were added: first it calls notify on the SMSAlert object, printing "SMS to Riya: Score updated to 45/2 in 8 overs"; then it calls notify on the AppAlert object, printing "App push to Karan: Score is now 45/2 in 8 overs". The ScoreBoard never needed to know these were an SMS system and an app system specifically — it only ever calls the shared method name notify, which is why any new kind of observer can be added later without touching ScoreBoard's code at all.
Choosing Correctly — and Not Overusing a Pattern
Each pattern solves one specific symptom, and reaching for the wrong one creates new problems instead of fixing old ones. Use Singleton only when a shared, single source of truth genuinely matters — a global game score, a single log file handle, a single settings object. Do not use it for ordinary objects like a Student or a Car, where you obviously want many independent instances; forcing every class into a Singleton is a well-known anti-pattern because it secretly turns local variables into global state that's hard to test and hard to reason about.
Use Factory when object-creation logic is repeated in multiple places or is likely to grow new cases over time — new snack types, new question types in a quiz app, new vehicle types in a transport simulator. Don't use it for a class you construct in exactly one place with no variation; that's just adding a layer of indirection with no payoff.
Use Observer whenever one change must ripple out to a variable, possibly-growing number of interested parties without the source of the change needing to know who they are — live scores, stock price tickers, a train's live running-status page pushing updates to every passenger who's tracking that PNR. Don't use it for a simple one-to-one relationship where a plain function call would do the same job with less code.
Why This Matters for Your Exams
CBSE Computer Science and Informatics Practices questions on object-oriented programming increasingly ask you to read a class hierarchy and explain why it's structured a certain way, not just trace output. Being able to say "this is a Factory because object creation is centralized in one method, which makes the design open to new snack types without modifying existing classes" demonstrates exactly the kind of design reasoning examiners reward in long-answer OOP questions, and it is the same reasoning that competitive programming and software-engineering interviews test years later — recognizing a recurring shape in someone else's code is a skill, and it starts with having built these three shapes yourself.
Check Your Understanding
- In the naive
VoteCounterexample,id(booth1) == id(booth2)printedFalse. Explain precisely why, referring to what__new__did each timeVoteCounter()was called. - A school attendance app currently has one
if/elifblock, repeated in four files, that turns a subject name ("Maths", "Science", "Hindi") into the rightSubjectobject. Which pattern would you apply, and what is the exact first step? - In the
ScoreBoardexample, if you calledboard.update_score(...)a second time before subscribing any observers, what would print? Why? - Explain, in one sentence each, the difference between Simple Factory and Factory Method as shown in this chapter.
- A classmate says "I'll just make every class in my project a Singleton, it seems safer." Explain what's wrong with this plan using the
Carexample from the refresher section.
Answers: (1) Each call to VoteCounter() ran the default __new__, which always allocates a fresh block of memory — nothing in that class stopped a second object from being built, so booth1 and booth2 point to two different addresses. (2) Factory pattern; the first step is writing one subject_factory(name) function containing the if/elif logic, then replacing all four repeated blocks with a call to it. (3) Nothing would print, because the for observer in self._subscribers: loop would run zero times over an empty list — update_score would still correctly update self._score, just silently. (4) Simple Factory uses one function with a branch (like if/elif) to decide what to build; Factory Method uses different subclasses, each overriding the same method name, so the choice of what gets built depends on which subclass you're using. (5) Singleton removes the ability to have more than one object — but you clearly want many independent Car objects (your uncle's car, your neighbour's car, a rental car), so forcing Car into a Singleton would make it impossible to represent more than one car in the whole program at once, which breaks the very reason the class exists.
Summary
A design pattern is a named, reusable strategy for a recurring code-design problem — not a code snippet to copy, but a shape to adapt. Singleton guarantees exactly one shared object exists, by overriding __new__ to return the same stored instance every time instead of building a new one. Factory centralizes the decision of what object to build into one place — either a single branching function (Simple Factory) or a set of subclasses each overriding a creation method (Factory Method) — so that adding new cases means editing one spot, not hunting through the whole program. Observer lets one subject object automatically notify a changing list of observer objects through a shared method name, without the subject ever needing to know the specific type of each observer. All three come from the Gang of Four's 1994 catalogue of 23 patterns, grouped into Creational, Structural, and Behavioral families — and all three are only worth using when the specific problem they solve is actually present in your code, not as decoration.