Design Patterns in Python
Reviewed & published by Brayan K
Master the essential design patterns used in professional Python development. Learn Singleton, Factory, and Strategy patterns with real-world examples and best practices.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
1. What Are Design Patterns (Really)?
Design patterns are reusable solutions to common design problems in software.
They're not code you copy 1:1. They're mental templates you adapt:
| Problem You Have | Pattern to Use | Real Example |
|---|---|---|
| "I need only one of this thing globally" | Singleton | Database connection, Logger |
| "I need a flexible way to create objects" | Factory | Payment processors, Notification senders |
| "I need to swap behaviour/algorithm easily" | Strategy | Discount calculators, Sorting algorithms |
In Python, we also care about:
- Not over-engineering (patterns are tools, not religion)
- Using Python features (modules, first-class functions, dataclasses, typing)
- Keeping code readable and testable
In this lesson we'll cover:
- Singleton – one global instance
- Factory – flexible object creation
- Strategy – swappable algorithms/behaviours
2. Singleton Pattern — Intent & When to Use
Intent: Ensure only one instance of a class exists, and provide a global access point to it.
- Configuration manager
- Database connection pool
- Logging manager
- Global cache
3. The Simplest Python Singleton: Just Use a Module
Instead of creating a complex class-based singleton, you can just create a module:
# config.py
SETTINGS = {
"debug": True,
"db_url": "postgres://localhost",
}
def is_debug():
return SETTINGS["debug"]
# Then everywhere:
# anywhere.py
# import config
# print(config.SETTINGS["db_url"])
print(SETTINGS)
print(is_debug())
# ✅ Expected output:
# {'debug': True, 'db_url': 'postgres://localhost'}
# True- config is imported once
- Python caches modules in sys.modules
- Every import config gets the same object
👉 For many apps, this is the most Pythonic "singleton".
Only use heavy OO-singleton patterns if you really need class-based behaviour.
4. Classic OO Singleton With __new__
Let's look at the "canonical" pattern:
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
# create the one and only instance
cls._instance = super().__new__(cls)
return cls._instance
class AppConfig(Singleton):
def __init__(self, debug=False):
# __init__ may be called multiple times, be careful
if not hasattr(self, "_initialised"):
self.debug = debug
self._initialised = True
config1 = AppConfig(debug=True)
config2 = AppConfig(debug=False)
print(config1 is config2) # True
print(config1.debug, config2.debug) # True True
# ✅ Expected output:
# True
# True True- We override __new__ (object creation) not __init__ (initialisation).
- We store the one instance in cls._instance.
- We guard __init__ with a flag (_initialised) to avoid re-running logic.
- Looks like a normal class.
- Supports inheritance.
- Can be overkill for many cases.
- Harder to test (global state).
5. Singleton via Decorator (Reusable & Clean)
We can build a @singleton decorator that turns any class into a singleton:
def singleton(cls):
instances = {}
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class Logger:
def __init__(self):
self.logs = []
def log(self, msg):
self.logs.append(msg)
logger1 = Logger()
logger2 = Logger()
logger1.log("Hello")
logger2.log("World")
print(logger1 is logger2) # True
print(logger1.logs) # ['Hello', 'World']
# ✅ Expected output:
# True
# ['Hello', 'World']- @singleton replaces the class with a function (get_instance)
- Calling Logger() actually calls get_instance()
- The real instance is cached in instances[cls]
- Super simple to apply.
- Easy to reuse on different classes.
- You lose the "type" a bit — Logger is now actually a function returning a Logger instance (can confuse IDEs / type checkers).
- Some tools (like MyPy) might need extra hints.
6. The Borg Pattern (Shared State Singleton)
Alternative: many instances, shared state.
class Borg:
_shared_state = {}
def __init__(self):
self.__dict__ = self._shared_state
class Settings(Borg):
def __init__(self):
super().__init__()
if not hasattr(self, "initialised"):
self.debug = True
self.initialised = True
s1 = Settings()
s2 = Settings()
s1.debug = False
print(s2.debug) # False
print(s1 is s2) # False (different instances)
# ✅ Expected output:
# False
# FalseAll instances share the same __dict__.
Changing an attribute in one changes it for all.
When might this be useful?
- When you want instances that act independent for type/identity, but share config or state.
- Some advanced patterns where identity and shared state are separate concerns.
7. Singleton in Real Projects — When & When NOT to Use
- Application-wide config (with care)
- Database connection pool / engine (like SQLAlchemy's engine)
- Global metrics collector
- Logging facility
- To "hack around" passing objects properly
- To avoid dependency injection
- To hide poor architecture or circular dependencies
- For everything — overuse = messy, untestable code
Example of something better than a singleton: dependency injection:
Part 2: Factory Patterns
Factory patterns control how objects are created:
- You want to be able to easily swap implementations
- You don't want if type == "x" everywhere in the code
- You want code that is open for extension, closed for modification
- Simple Factory (function-based)
- Factory Method (OO inheritance-based)
- Abstract Factory (families of objects)
8. Simple Factory (Function-Based)
This is the most Pythonic and often best starting point.
Example: Notification Sender Factory
We want to create different notification senders: email, SMS, push.
class EmailSender:
def send(self, to, message):
print(f"[EMAIL to {to}] {message}")
class SMSSender:
def send(self, to, message):
print(f"[SMS to {to}] {message}")
class PushSender:
def send(self, to, message):
print(f"[PUSH to {to}] {message}")
def create_notifier(channel: str):
channel = channel.lower()
if channel == "email":
return EmailSender()
elif channel == "sms":
return SMSSender()
elif channel == "push":
return PushSender()
else:
raise ValueError(f"Unknown channel: {channel}")
# Usage
notifier = create_notifier("email")
notifier.send("[email protected]", "Welcome to LearnCodingFast!")
# ✅ Expected output:
# [EMAIL to [email protected]] Welcome to LearnCodingFast!- Your app code only knows about create_notifier("email")
- All the "which class?" logic is inside the factory function
If you add WhatsApp later, only one place changes.
9. Improving the Simple Factory with a Registry
Instead of if/elif, we can use a mapping (much cleaner for many types).
class EmailSender:
def send(self, to, message):
print(f"[EMAIL to {to}] {message}")
class SMSSender:
def send(self, to, message):
print(f"[SMS to {to}] {message}")
class PushSender:
def send(self, to, message):
print(f"[PUSH to {to}] {message}")
NOTIFIER_REGISTRY = {
"email": EmailSender,
"sms": SMSSender,
"push": PushSender,
}
def create_notifier(channel: str):
try:
cls = NOTIFIER_REGISTRY[channel.lower()]
except KeyError:
raise ValueError(f"Unknown channel: {channel}")
return cls()
notifier = create_notifier("sms")
notifier.send("+44...", "Your code is 123456")
# ✅ Expected output:
# [SMS to +44...] Your code is 123456- Easy to extend: just add to NOTIFIER_REGISTRY
- Great for config-driven systems: channel can come from JSON/env
This "simple factory + registry" pattern is used everywhere in real-world Python code (ML frameworks, plugin systems, etc.).
10. Factory Method Pattern (OO Style)
Sometimes you don't want a separate function, you want subclasses to decide what they create.
Factory Method = a method in a parent class that is overridden in subclasses to decide what object to create.
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def create_client(self):
"""Factory method: subclasses return appropriate client."""
pass
def process_payment(self, amount: float):
client = self.create_client()
client.charge(amount)
class StripeClient:
def charge(self, amount):
print(f"[Stripe] Charging £{amount:.2f}")
class PayPalClient:
def charge(self, amount):
print(f"[PayPal] Charging £{amount:.2f}")
class StripeProcessor(PaymentProcessor):
def create_client(self):
return StripeClient()
class PayPalProcessor(PaymentProcessor):
def create_client(self):
return PayPalClient()
# Usage:
processor = StripeProcessor()
processor.process_payment(29.99)
# ✅ Expected output:
# [Stripe] Charging £29.99- process_payment() is defined once in the base class
- create_client() is the factory method
- Each subclass defines what concrete client it wants
This pattern is great when:
- You have a common workflow (template), but some steps vary
- You want subclasses to control those steps via factory methods
11. Abstract Factory — Creating "Families" of Related Objects
You're building a cross-platform GUI toolkit:
- On Windows: WindowsButton, WindowsCheckbox
- On macOS: MacButton, MacCheckbox
- Ensure you never accidentally mix WindowsButton with MacCheckbox
- Make it easy to "switch theme" or "switch platform" by changing one factory
Abstract Factory returns a family of related objects.
from abc import ABC, abstractmethod
# Product interfaces
class Button(ABC):
@abstractmethod
def render(self): ...
class Checkbox(ABC):
@abstractmethod
def render(self): ...
# Concrete products
class LightButton(Button):
def render(self):
print("[Light Button]")
class DarkButton(Button):
def render(self):
print("[Dark Button]")
class LightCheckbox(Checkbox):
def render(self):
print("[Light Checkbox]")
class DarkCheckbox(Checkbox):
def render(self):
print("[Dark Checkbox]")
# Abstract Factory
class UIFactory(ABC):
@abstractmethod
def create_button(self) -> Button: ...
@abstractmethod
def create_checkbox(self) -> Checkbox: ...
# Concrete factories
class LightUIFactory(UIFactory):
def create_button(self) -> Button:
return LightButton()
def create_checkbox(self) -> Checkbox:
return LightCheckbox()
class DarkUIFactory(UIFactory):
def create_button(self) -> Button:
return DarkButton()
def create_checkbox(self) -> Checkbox:
return DarkCheckbox()
# Usage
def render_screen(factory: UIFactory):
btn = factory.create_button()
cb = factory.create_checkbox()
btn.render()
cb.render()
factory = DarkUIFactory()
render_screen(factory)
# ✅ Expected output:
# [Dark Button]
# [Dark Checkbox]- You switch entire families by swapping the factory.
- You never accidentally pair incompatible components.
This is similar to:
- Theme systems
- Cross-platform widgets
- Database driver families (e.g. Postgres/MySQL adapters)
Part 3: Strategy Pattern
The Strategy pattern allows you to change how something works without changing the code that uses it.
Python makes this pattern extremely powerful because functions are first-class objects.
12. Why Strategy Pattern Exists
Without Strategy, developers often write:
- Hard to add new rules (must edit this function)
- Conditionals spread everywhere
- Testing each rule becomes harder
- Breaking Open–Closed Principle (OCP)
Strategy solves all this.
13. Strategy Pattern — Functional (Most Pythonic)
The simplest and BEST way in Python is using functions as strategies.
# Step 1: Define behaviors
def discount_percentage(price):
return price * 0.9
def discount_fixed(price):
return price - 5
def discount_bogo(price):
return price / 2
# Step 2: Strategy dictionary
DISCOUNT_STRATEGIES = {
"percentage": discount_percentage,
"fixed": discount_fixed,
"bogo": discount_bogo
}
# Step 3: Apply strategy
def apply_discount(price, strategy_name):
strategy = DISCOUNT_STRATEGIES[strategy_name]
return strategy(price)
print(apply_discount(100, "percentage"))
print(apply_discount(100, "fixed"))
print(apply_discount(100, "bogo"))
# ✅ Expected output:
# 90.0
# 95
# 50.0- ✔ most Pythonic
- ✔ extremely fast
- ✔ ideal for data pipelines, ML preprocessing, game logic, pricing engines
14. Strategy Using Classes (Classic OOP Approach)
Sometimes you need stateful strategies or polymorphism.
from abc import ABC, abstractmethod
# Step 1: Strategy interface
class DiscountStrategy(ABC):
@abstractmethod
def apply(self, price):
pass
# Step 2: Implement concrete strategies
class PercentageDiscount(DiscountStrategy):
def apply(self, price):
return price * 0.9
class FixedDiscount(DiscountStrategy):
def apply(self, price):
return price - 5
# Step 3: Use strategy
def calculate_total(price, strategy: DiscountStrategy):
return strategy.apply(price)
print(calculate_total(100, PercentageDiscount()))
print(calculate_total(100, FixedDiscount()))
# ✅ Expected output:
# 90.0
# 95- When strategies need state
- When strategies become complex
- When building enterprise-level frameworks
15. Strategy in Real Systems
▶ 1. Machine Learning Preprocessing Pipelines
▶ 2. Payment Processing Logic
Choose pricing or fraud-checking algorithms at runtime.
- follow-player
- Reward calculation strategies
- Exploration strategies (epsilon-greedy, Boltzmann, UCB)
▶ 5. Web Framework Routing
- write-through
- Black Friday discount mode
- Bulk discounts
- VIP customer strategies
📖 Worked Example: All Three Patterns in One Checkout
You have met the three patterns one at a time. In real code they show up together, and each one answers a different question:
- Singleton — "how many of this thing should exist?" (one)
- Factory — "which class do I build?" (decided at run time, in one place)
- Strategy — "which behaviour do I use?" (swapped without touching the caller)
Read the program below before writing anything. Notice that checkout() — the function doing the actual work — never names a concrete discount rule or a concrete payment class. It looks both up by name. That is the whole point of the patterns.
"""One small checkout — Singleton, Factory and Strategy working together."""
# ---------- SINGLETON: one shared settings object for the whole program ----------
class Settings:
_instance = None
def __new__(cls):
# __new__ runs BEFORE __init__ and decides which object you get back.
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.currency = "GBP"
cls._instance.vat = 0.20
return cls._instance # every later call returns that same object
# ---------- STRATEGY: interchangeable pricing rules, as plain functions ----------
def no_discount(total: float) -> float:
return total
def student(total: float) -> float:
return total * 0.90 # 10% off
def bulk(total: float) -> float:
return total * 0.75 if total > 100 else total # 25% off big baskets only
# The strategies live in a dict, so choosing one is a lookup, not an if/elif chain.
DISCOUNTS = {"none": no_discount, "student": student, "bulk": bulk}
# ---------- FACTORY: pick a payment processor by name, at run time ----------
class CardPayment:
def pay(self, amount: float, currency: str) -> str:
return f"Card charged {amount:.2f} {currency}"
class PayPalPayment:
def pay(self, amount: float, currency: str) -> str:
return f"PayPal charged {amount:.2f} {currency}"
PROCESSORS = {"card": CardPayment, "paypal": PayPalPayment}
def make_processor(kind: str):
"""The factory: the ONE place that decides which class gets built."""
try:
return PROCESSORS[kind]() # look the CLASS up, then call it to build one
except KeyError:
raise ValueError(f"unknown payment type: {kind}") from None
# ---------- The checkout uses all three and knows no concrete detail ----------
def checkout(basket_total: float, discount_name: str, payment_kind: str) -> str:
settings = Settings() # singleton: same object every time
discount = DISCOUNTS[discount_name] # strategy: chosen by name
subtotal = discount(basket_total)
total = subtotal * (1 + settings.vat) # VAT comes from the shared settings
processor = make_processor(payment_kind) # factory: chosen by name
return processor.pay(total, settings.currency)
print(checkout(80.00, "none", "card"))
print(checkout(80.00, "student", "paypal"))
print(checkout(200.00, "bulk", "card"))
# The singleton really is one object, not two that look alike:
print("Same settings object?", Settings() is Settings())
# The factory refuses what it does not recognise, instead of returning None:
try:
make_processor("bitcoin")
except ValueError as e:
print("Error:", e)
# ✅ Expected output:
# Card charged 96.00 GBP
# PayPal charged 86.40 GBP
# Card charged 180.00 GBP
# Same settings object? True
# Error: unknown payment type: bitcoinTo add a "black Friday" discount you add one function and one dictionary entry. To add Apple Pay you add one class and one dictionary entry. checkout() is never touched. When people say patterns make code "extensible", that is the concrete thing they mean.
🎯 Your Turn: A Logger and Two Formatters
One singleton and one strategy table, with three pieces removed. Each blank is one of the ideas from this lesson. Fill them in, run it, and compare with the expected output at the bottom.
Watch blank 2 carefully: you store the function itself, not a call to it. Writing as_csv() with brackets would run the function immediately and store its return value.
# 🎯 YOUR TURN — replace the three ___ blanks
class Logger:
"""SINGLETON: every part of the program writes to the same log."""
_instance = None
def __new__(cls):
# 1) Only build a new object the FIRST time. After that, hand back the one you kept.
if cls._instance is ___: # 👉 what is _instance before anything is built?
cls._instance = super().__new__(cls)
cls._instance.lines = []
return cls._instance
def log(self, message: str) -> None:
self.lines.append(message)
# STRATEGY: two interchangeable ways to turn rows into text.
def as_text(rows):
return "\n".join(", ".join(r) for r in rows)
def as_csv(rows):
return "\n".join(";".join(r) for r in rows)
# 2) Put the strategies in a dict so picking one is a lookup, not an if/elif chain.
FORMATTERS = {"text": as_text, "csv": ___} # 👉 the FUNCTION itself, with no ()
def export(rows, style):
# 3) Look the chosen strategy up by name.
formatter = FORMATTERS[___] # 👉 which variable holds the chosen name?
Logger().log(f"exported as {style}")
return formatter(rows)
rows = [("id", "name"), ("1", "Ada"), ("2", "Grace")]
print(export(rows, "text"))
print("---")
print(export(rows, "csv"))
print("---")
print("Log lines:", Logger().lines)
print("One logger?", Logger() is Logger())
# ✅ Expected output:
# id, name
# 1, Ada
# 2, Grace
# ---
# id;name
# 1;Ada
# 2;Grace
# ---
# Log lines: ['exported as text', 'exported as csv']
# One logger? True1) None — _instance starts as None, so if cls._instance is None is true exactly once.
2) as_csv — the bare name. Adding () would call it right there with no arguments and raise a TypeError.
3) style — the argument that carries the caller's choice.
If "Log lines" shows only one entry, you rebuilt the logger each time — check that __new__ returns cls._instance on every path, not just the first.
🏆 Mini-Challenge: A Self-Registering Factory
Outline only — every line is yours to write. This is the pattern that turns a factory into a plugin system, and it is how real libraries let you drop in a new backend without editing their code.
You will combine the registry factory from section 9 with a decorator. A decorator that takes an argument is a function that returns a decorator — that extra layer is the one bit worth going slowly over.
# 🎯 MINI-CHALLENGE: a self-registering factory
#
# The factories earlier in this lesson keep a hand-written dict. Real plugin
# systems let each class register ITSELF, so adding one never means editing the
# factory. Build that.
#
# 1. Start with an empty dict: SHIPPERS = {}
# 2. Write register(name) — a decorator factory. It takes a name and RETURNS a
# decorator; that inner decorator stores the class in SHIPPERS[name] and then
# returns the class unchanged.
# 3. Write make_shipper(name): look the class up and call it. On a missing key,
# raise ValueError(f"no shipper called {name!r}") — use `from None` so the
# KeyError does not clutter the traceback.
# 4. Define three classes, each decorated with @register("..."), each with a
# quote(self, kg) method:
# standard -> 3.99 + kg * 0.50
# express -> 9.99 + kg * 1.20
# courier -> 15.00 (flat rate, ignores kg)
# 5. Print sorted(SHIPPERS), then for each of standard, express and courier print
# f"{name:9}£{make_shipper(name).quote(4):.2f}"
# 6. Finally, try make_shipper("pigeon") in a try/except and print the error.
#
# ✅ Expected output:
# Registered: ['courier', 'express', 'standard']
# standard £5.99
# express £14.79
# courier £15.00
# Error: no shipper called 'pigeon'
# your code hereSHIPPERS = {}
def register(name):
"""Decorator factory: returns the decorator that records the class."""
def decorator(cls):
SHIPPERS[name] = cls
return cls
return decorator
def make_shipper(name):
try:
return SHIPPERS[name]()
except KeyError:
raise ValueError(f"no shipper called {name!r}") from None
@register("standard")
class Standard:
def quote(self, kg):
return 3.99 + kg * 0.50
@register("express")
class Express:
def quote(self, kg):
return 9.99 + kg * 1.20
@register("courier")
class Courier:
def quote(self, kg):
return 15.00
print("Registered:", sorted(SHIPPERS))
for name in ("standard", "express", "courier"):
print(f"{name:9}£{make_shipper(name).quote(4):.2f}")
try:
make_shipper("pigeon")
except ValueError as e:
print("Error:", e)Adding a fourth shipper now costs one class and one decorator line. Nothing in make_shipper changes, and nothing that already worked can break — which is exactly what "open for extension, closed for modification" means in practice.
🎓 Conclusion
You now fully understand all 3 major design patterns:
Control instance creation, global state, config, caching.
✔ Factory (Simple, Method, Abstract)
- Control which objects are created
- Promote extensibility
- Reduce conditionals
- Enable plugin architectures
- Control how objects behave
- Swap algorithms at runtime
- Enable configurability and clean architecture
These patterns together give you:
- 🔥 scalable systems
- 🔥 cleaner abstraction
- 🔥 professional architecture
- 🔥 maintainable large codebases
- 🔥 flexible logic for real projects
📋 Quick Reference — Design Patterns
| Pattern | Use when |
|---|---|
| Singleton | Only one instance needed (config, DB pool) |
| Factory | Create objects without exposing class details |
| Observer | Event-driven: notify multiple subscribers |
| Strategy | Swap algorithms at runtime |
| Decorator | Add behaviour without changing the class |
🎉 Great work! You've completed this lesson.
You can now recognise and apply the most important design patterns — the vocabulary every senior engineer uses when discussing architecture.
Practice quiz
Which problem does the Singleton pattern solve?
- Swapping algorithms at runtime
- Creating families of related objects
- Ensuring only one instance exists with a global access point
- Adding behavior to a class dynamically
Answer: Ensuring only one instance exists with a global access point. Singleton guarantees a single instance (e.g. a config or DB pool) and provides one global access point to it.
What is the most Pythonic 'singleton' for many simple cases?
- Just a module — imported once and cached in sys.modules
- A metaclass
- A global list
- A frozen dataclass
Answer: Just a module — imported once and cached in sys.modules. A module is imported once and cached in sys.modules, so every import gets the same object — a natural singleton.
In the classic singleton, which method is overridden to control instance creation?
- __init__
- __call__
- __enter__
- __new__
Answer: __new__. __new__ controls object creation, so it returns the stored single instance; __init__ only initializes.
What distinguishes the Borg pattern from a classic singleton?
- It forbids inheritance
- Many instances exist but they share the same state via __dict__
- It allows only one instance
- It caches return values
Answer: Many instances exist but they share the same state via __dict__. Borg instances are distinct objects (s1 is s2 is False) yet share one __dict__, so they share all state.
What does a simple (function-based) factory like create_notifier('email') achieve?
- It centralizes the 'which class?' decision so callers don't use if/elif everywhere
- It caches notifications
- It makes the class a singleton
- It validates argument types
Answer: It centralizes the 'which class?' decision so callers don't use if/elif everywhere. The factory hides class-selection logic in one place, so app code just asks for 'email' without knowing the concrete class.
How does a registry improve a simple factory?
- It adds threading
- It removes the need for classes
- It replaces if/elif chains with a dictionary mapping names to classes
- It enforces a single instance
Answer: It replaces if/elif chains with a dictionary mapping names to classes. A registry dict maps keys to classes, so adding a new type is just one dictionary entry instead of another elif branch.
In the Factory Method pattern, who decides which concrete object is created?
- A standalone function
- Subclasses, by overriding the factory method
- The caller passes the class in
- A global registry only
Answer: Subclasses, by overriding the factory method. Factory Method puts a create_*() method in the base class that each subclass overrides to return its own product.
What does an Abstract Factory return?
- A single object
- A cached value
- A function
- A family of related objects (e.g. matching Button and Checkbox)
Answer: A family of related objects (e.g. matching Button and Checkbox). Abstract Factory produces whole families of related objects, ensuring you never mix incompatible components.
What is the most Pythonic way to implement the Strategy pattern?
- A deep class hierarchy
- Functions as first-class strategies stored in a dict
- A singleton per strategy
- Global if/elif branches
Answer: Functions as first-class strategies stored in a dict. Because functions are first-class in Python, storing them in a strategy dict is the simplest, fastest approach.
Which principle does replacing if/elif algorithm-selection with Strategy uphold?
- DRY only
- Single instance guarantee
- The Open-Closed Principle (open for extension, closed for modification)
- Lazy initialization
Answer: The Open-Closed Principle (open for extension, closed for modification). Strategy lets you add new behaviors without editing existing selection code, honoring the Open-Closed Principle.
Continue this course
- Previous: Mixins, Multiple Inheritance & OOP Patterns
- Next: Module & Package Architecture for Large Codebases — Structure growing projects with namespaces, imports, and __init__.py
- Quick reference: Python cheat sheet