
The strategy pattern hands one changeable step to a separate object. The code around it keeps the steps that never change, and calls one shared method, such as send, for the step that does. Each way of doing that step is a strategy: email, SMS or no message. Swap the object and the behaviour changes, with no if chain.
In this post we start with wedding invitations. Then we rewind the shop app from the last post to before it had its notifier classes, when the code that sends each message sat inside the order service in an if chain, and pull each way of sending out into its own small class. We test the service with a fake, see that a strategy in Python can be a plain function (sorted already works this way), compare it with the factory pattern, and finish with when an if is the better choice.
A few words first. A class is like a recipe, and an object is one thing made from it. A method is a function that belongs to a class, such as send. A notifier here is an object that sends a message to a user. The interface of an object is the set of methods other code can call on it; all our notifiers share one, send(to, message).
Key takeaways
- A strategy is one way of doing a step, wrapped in its own object (or function). The code that uses it, the context, calls one shared method and doesn't know which way it got.
- It replaces an
ifchain that picks how to do something. Each branch becomes a small class, and the wholeifchain shrinks to one line:notifier.send(...). - Adding a new way is a new class, with no edits to the context. Tests hand the context a fake strategy that writes down what it was asked to do.
- In Python, a strategy can be a plain function.
sorted(orders, key=by_total)is the strategy pattern:sorteddoes the sorting,keydecides what to sort by. - The factory from the last post picks a strategy; the strategy does the work. They often appear together.
- Don't use it for one way that never changes, or for an
ifwith two short branches. And never let the context check which strategy it got: that brings theifchain back.
What is the strategy pattern in simple words?
Think of a family sending wedding invitations. The invitation says the same thing to everyone: whose wedding, where, and when. How it reaches each person is different: a printed card by post for the grandparents, a WhatsApp message for the cousins, a phone call for the uncle who never reads messages. Nobody rewrites the invitation for each way. The job is always the same two steps: write the invitation, then deliver it. Only "deliver it" changes from person to person, and the person writing doesn't decide it: a cousin keeps the list of who gets a card, a message or a call. And when a new friend asks for email, the family adds one more way of delivering. The invitation doesn't change at all.
Code is the same. The part that never changes, here "write the invitation and hand it over to be delivered", is called the context. Each way of doing the step that changes, by post, by WhatsApp or by phone, is a strategy. The context doesn't know or care which strategy it has. It just says "deliver this", and the strategy does it its own way.
What problem does the strategy pattern solve?
Back to the small shop app from the factory pattern post. When an order ships, the app marks it as shipped and messages the user, by the channel they picked: email, SMS, or "off" for no messages. Let's rewind to before the shop had its notifiers.py file. Here's the order service the way most of us write it first, with the sending code itself inside it:
# orders_before.py
class OrderService:
def __init__(self):
self.shipped = []
def ship(self, order_id, contact, channel):
self.shipped.append(order_id)
message = f"Order {order_id} has shipped"
if channel == "email":
print(f"Email from orders@letsbug.in to {contact}: {message}")
elif channel == "sms":
print(f"SMS from LTSBUG to {contact}: {message}")
elif channel == "off":
pass
else:
raise ValueError(f"unknown channel {channel!r}")
self.shipped is a list of the orders this service has shipped; a real app would save that to its database. The print calls stand in for the real sending, so the example runs anywhere. It works, and with three short branches it even looks fine. Then the app grows:
- The shipping class knows every channel's details. In a real app, the email branch connects to an email service, and the SMS branch calls an SMS gateway (a company that delivers text messages), each with its own settings and error handling. That's dozens of lines about email and SMS inside a class whose job is shipping orders.
- Every new channel is an edit to
OrderService. Adding WhatsApp means opening the shipping code, adding a branch, and checking that the email and SMS branches still work. The class changes for reasons that have nothing to do with shipping. - It's hard to test. A test of
shipruns the real branch: in a real app, it would send a real email. The usual ways around it are to catch what the code prints, or to add anelif channel == "test"branch to the app's real code just for tests.
The if chain is choosing how to do one step. The strategy pattern moves each "how" out into its own object.
How does the strategy pattern work?
Each branch of the if becomes a small class with the same method, send(to, message). The context, OrderService, is handed one of them and calls send. It never asks which one it has. That's the whole pattern: one shared method, many ways to do it, and the choice made outside the code that uses it.

This move has a name in Martin Fowler's refactoring catalog (a list of small, safe ways to improve code): Replace Conditional with Polymorphism. His example starts with a switch (another language's version of an if chain) on bird.type, and ends with one class per kind of bird, each with its own version of the same method. Polymorphism means "many forms": the same call, notifier.send(...), does different things depending on which object receives it.
How do you write the strategy pattern in Python?
First, the strategies. If you read the last post, this file is the same, word for word: our three notifiers were strategies all along.
# notifiers.py
class EmailNotifier:
def __init__(self, sender):
self.sender = sender
def send(self, to, message):
print(f"Email from {self.sender} to {to}: {message}")
class SmsNotifier:
def __init__(self, sender_id):
self.sender_id = sender_id
def send(self, to, message):
print(f"SMS from {self.sender_id} to {to}: {message}")
class NullNotifier:
"""For users who turned messages off: sends nothing."""
def send(self, to, message):
pass
Each class keeps its own details. The email address it sends from lives in EmailNotifier, and the sender ID (the short name a phone shows as who sent the text) lives in SmsNotifier. NullNotifier has the same send method and does nothing, so "off" is just one more strategy, not a special case the service has to check for.
Now the context. The if chain is gone, and ship takes the notifier to use as an argument:
# orders.py
class OrderService:
def __init__(self):
self.shipped = []
def ship(self, order_id, contact, notifier):
self.shipped.append(order_id) # the same for every order
notifier.send(contact, f"Order {order_id} has shipped") # the step that changes
orders.py doesn't import a single notifier. It works with any object that has a send(to, message) method, including ones that don't exist yet. In the last post, the service got its notifier from a factory; here it's handed one with each order, so we can look at the strategy on its own. Both are the strategy pattern: what matters is that the service calls send and never decides how sending works. (Note that ship now takes order_id, contact, notifier, not the last post's user, order_id, so the last post's tests won't fit this version.)
Something still has to choose which strategy each order gets. For now, main.py chooses by hand:
# main.py
from notifiers import EmailNotifier, NullNotifier, SmsNotifier
from orders import OrderService
email = EmailNotifier(sender="orders@letsbug.in")
sms = SmsNotifier(sender_id="LTSBUG")
off = NullNotifier()
service = OrderService()
service.ship(101, "asha@example.com", email)
service.ship(102, "+91 90000 00001", sms)
service.ship(103, "meera@example.com", off)
print("Shipped:", service.shipped)
Put the three files in one folder and run python main.py:
Email from orders@letsbug.in to asha@example.com: Order 101 has shipped
SMS from LTSBUG to +91 90000 00001: Order 102 has shipped
Shipped: [101, 102, 103]
Asha got an email, and the customer with the phone number got an SMS. Meera turned messages off, so her NullNotifier sent nothing, and her order was still marked as shipped, because that step lives in the context and runs for every order.
Now the shop adds WhatsApp. That's one new class in notifiers.py with a send(to, message) method (the last post's "Try it" built exactly that class). orders.py doesn't change, and neither do its tests.
How do you test code that uses a strategy?
Hand it a strategy made for testing: a fake notifier that sends nothing and writes down what it was asked to send. The service can't tell the difference, because the fake has the same send method.
# test_orders.py
import unittest
from orders import OrderService
class FakeNotifier:
"""A strategy for tests: sends nothing, writes down what it was asked to send."""
def __init__(self):
self.sent = []
def send(self, to, message):
self.sent.append((to, message))
class OrderTest(unittest.TestCase):
def test_ship_hands_the_message_to_the_notifier(self):
fake = FakeNotifier()
service = OrderService()
service.ship(7, "+91 90000 00001", fake)
self.assertEqual(fake.sent, [("+91 90000 00001", "Order 7 has shipped")])
self.assertEqual(service.shipped, [7])
def test_each_order_uses_the_notifier_it_was_given(self):
email, sms = FakeNotifier(), FakeNotifier()
service = OrderService()
service.ship(8, "asha@example.com", email)
service.ship(9, "+91 90000 00001", sms)
self.assertEqual(email.sent, [("asha@example.com", "Order 8 has shipped")])
self.assertEqual(sms.sent, [("+91 90000 00001", "Order 9 has shipped")])
self.assertEqual(service.shipped, [8, 9])
Run python -m unittest in the same folder. unittest is Python's built-in test tool:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Each dot is one test that passed. Nothing was printed, nothing was sent, and there's no "test" branch anywhere in the app's real code. (The time on your machine may say 0.001s.)
Can a strategy be a function in Python?
Yes. In Python, functions are objects too: you can store one in a variable and pass it to other code, like any other value. So when a strategy has one job and no settings, a plain function is enough, and you've already used one. The Python sorting guide says: "The list.sort() method and the functions sorted(), min(), max(), heapq.nsmallest(), and heapq.nlargest() have a key parameter to specify a function (or other callable) to be called on each list element prior to making comparisons." A callable is anything you can call with brackets, like a function. Here the shop sorts its orders two ways:
# sort_orders.py
orders = [
{"id": 101, "total": 499, "items": 1},
{"id": 102, "total": 1299, "items": 3},
{"id": 103, "total": 250, "items": 5},
]
def by_total(order):
return order["total"]
def by_items(order):
return order["items"]
print([order["id"] for order in sorted(orders, key=by_total)])
print([order["id"] for order in sorted(orders, key=by_items)])
Run python sort_orders.py:
[103, 101, 102]
[101, 102, 103]
The first line is cheapest order first (₹250, ₹499, ₹1,299); the second is fewest items first (1, 3, 5). [order["id"] for order in ...] is a list comprehension: a short way to build a list, here the ID of each order in sorted order.

sorted is the context: the sorting never changes. The key function is the strategy: it decides what to sort by.sorted never changes; it knows how to sort anything. The key function is the one step that changes: what to compare. That's the strategy pattern, built into Python. A context is written for one shape of strategy, though. sorted calls key(order), so it takes a function. Our OrderService calls notifier.send(...), so it needs an object with a send method; hand it a plain function and Python stops with an AttributeError. A version written for functions would call notify(contact, message) directly. So why are our notifiers classes and not functions? Because each one carries a setting, the sender, and a class keeps a setting together with the method that uses it. A good rule: one job and no settings, use a function; settings or several methods, use a class.
Strategy vs factory pattern: what's the difference?
The last post ended with this, and now we can see both halves:
- The factory answers "which object do I make?" Each call to
make_notifier("sms")builds oneSmsNotifier, and that call's job is done. - The strategy answers "how do I do this step?" It's the object the factory built, and it does the work every time
sendis called.
In the real shop, each user's channel is a setting saved with their account, so the two work together. The factory turns the setting into a strategy, and the service uses it: service.ship(order_id, user["contact"], make_notifier(user["channel"])). You can have strategies without a factory, as in our main.py, and a factory that builds things that aren't strategies. But when the choice comes from a setting, you'll usually see both.
When should you not use the strategy pattern?
Each strategy is a new class or function, and a reader has to jump to it to see what happens. Skip the pattern when:
- There's only one way, and it isn't changing. If the shop only ever sends email, call
EmailNotifierdirectly. Don't build asendinterface for channels nobody has asked for. Fowler's note on "Yagni" covers this: "It's a statement that some capability we presume our software needs in the future should not be built now because "you aren't gonna need it"." Pull out a strategy on the day the second way arrives. - The choice is an
ifwith two short branches. Free delivery on orders of ₹499 or more isfee = 0 if total >= 499 else 40(Python's one-lineif/else). That's one line anyone can read. Two classes and a shared method would hide a simple rule in three places. - The "ways" differ only by a value. Two email notifiers with different sender addresses aren't two strategies: they're one class with two settings, which is what
senderis for. Strategies are for different behaviour, not different numbers or names.
What mistake do beginners make with the strategy pattern?
The context checks which strategy it got. Someone notices that for users with messages off, there's "no point" calling send, and adds a shortcut:
from notifiers import NullNotifier
class OrderService:
def __init__(self):
self.shipped = []
def ship(self, order_id, contact, notifier):
if isinstance(notifier, NullNotifier): # "messages are off, so skip it"
return
self.shipped.append(order_id)
notifier.send(contact, f"Order {order_id} has shipped")
isinstance(notifier, NullNotifier) asks "was this object made from the NullNotifier class?" Now run main.py again, and the last line says Shipped: [101, 102]. Meera's order 103 was never marked as shipped, because the shortcut skipped the step that every order needs. That's the first problem. The second is bigger: the if chain is back. orders.py imports a notifier class again, and every new "special" strategy will want its own check.
Python has a name for the right habit: duck typing, from the saying "if it looks like a duck and quacks like a duck, it's a duck". The Python glossary describes it as "A programming style which does not look at an object's type to determine if it has the right interface; instead, the method or attribute is simply called or used", and adds: "Duck-typing avoids tests using type() or isinstance()." (An object's type is the class it was made from.) In plain words: if it has send, call send. The fix is to delete the shortcut. NullNotifier already does nothing; that's its whole job. If a strategy needs to behave differently, put that difference inside the strategy, never in the context.
1) Swap the mistake version of OrderService into the folder and run python -m unittest. Both tests still pass. Why? Write a third test that fails on the mistake and passes on the real orders.py.
2) Using sort_orders.py, list the order IDs with the most expensive order first. What do you change, and what does it print?
3) The shop adds WhatsApp. Which of notifiers.py, orders.py, main.py and test_orders.py change?
Show the answers
1) Both tests hand the service a FakeNotifier, so the isinstance(notifier, NullNotifier) check never matches, and the shortcut never runs. A test has to pass the real "off" strategy. Add from notifiers import NullNotifier to the imports at the top of test_orders.py, and this method to OrderTest:
def test_off_still_marks_the_order_shipped(self):
service = OrderService()
service.ship(10, "meera@example.com", NullNotifier())
self.assertEqual(service.shipped, [10])
With the mistake, service.shipped is [] and the test fails. With the real orders.py, it passes.
2) Keep the same strategy and ask sorted to reverse the order: sorted(orders, key=by_total, reverse=True). It prints [102, 101, 103]: ₹1,299, then ₹499, then ₹250. The sorting guide: "Both list.sort() and sorted() accept a reverse parameter with a boolean value. This is used to flag descending sorts."
3) notifiers.py gets a WhatsAppNotifier class with a send(to, message) method. main.py builds one and passes it to ship for the users who chose WhatsApp (in the real app, the factory's menu gets one new line instead). orders.py and test_orders.py don't change.
Questions people ask
What is the strategy pattern in simple words?
It's a way to make one step of your code swappable. Each way of doing that step goes in its own object, all with the same method, and the code that uses them just calls that method. Like a family sending the same wedding invitation by post to some people and by WhatsApp to others: the invitation stays the same, only the delivery changes.
What is the difference between the strategy and factory patterns?
A factory decides which object to make, then its job is done. A strategy is the object that does the work, through one shared method like send. They often work together: the factory turns a setting such as "sms" into an SmsNotifier, and the rest of the code uses it without knowing which one it got.
Do I need an interface or abstract class for a strategy in Python?
No. An abstract class is a class you don't make objects from directly; its job is to list the methods the classes based on it must have. Python doesn't need one here, because it uses duck typing: any object with the right method fits. If your project uses type hints (notes in the code saying what type a value should be), you can write the shared method down with typing.Protocol, whose docs say "Such classes are primarily used with static type checkers that recognize structural subtyping (static duck-typing)". A static type checker is a tool that checks types before the code runs, and "structural subtyping" means an object fits if it has the right methods, whatever class it came from. It's optional; Python itself doesn't need it.
Is the strategy pattern the same as polymorphism?
It's built on polymorphism, which means the same call does different things depending on the object that receives it. The strategy pattern is one way to use that: take the step that varies, give each way of doing it its own object, and let the code that uses it call one method without checking which object it has.
Keep going
- Previous in this System Design series: Factory pattern, which turns a user's setting into the right notifier. The factory picks the strategy; this post was about the strategy doing the work.
- Next: "Observer pattern", where one event, such as an order shipping, tells every part of the app that's listening, without the order code knowing who they are.
- Where the strategy gets handed in: Dependency injection explained with a real Python example, which plugs a mailer into a sign-up service the same way.
Checked on 1 November 2026 against the 5 sources below.
Sources
- Python documentation: Sorting Techniques (key functions and
reverse) - Python documentation: Glossary (duck-typing, callable)
- Python documentation: typing (
Protocol) - Martin Fowler: Replace Conditional with Polymorphism (Refactoring catalog)
- Martin Fowler: Yagni (2015)
Every Python sample in this post was run in Python 3.13 before publishing, and the tests were run against the fake notifier.