EthicsComputerCourses
Join training +91 90532 10052

Clean Code & Design Patterns

Python · Lesson 64 of 68 Advanced 7 min read Updated

Code is read far more often than it is written. In a company, the person who changes your function next month might be a new teammate in Pune, or you after you have forgotten everything about it. Clean code is code that such a reader can understand and change safely. Design patterns are proven ways of arranging code for problems that come up again and again, such as "choose one of several rules at runtime" or "make this class work with a library that has different method names".

Many pattern books were written for Java and C++. Python has first-class functions, duck typing and modules, so several famous patterns become a few lines here. This lesson shows the clean-code habits reviewers look for, and the patterns you will actually meet in Python projects, in their Pythonic form.

Clean code by example

Here is a function that works but would not pass a code review:

Python
def calc(c, m):
    t = 0
    for i in c:
        t = t + i[1] * i[2]
    if m == True:
        t = t - t * 10 // 100
    if t < 499:
        t = t + 40
    return t

What is c? What is i[2]? Why 499 and 40? Is a member discount applied before or after delivery? Every reader has to work it out again. The same logic, cleaned up:

PythonRun it yourself
FREE_DELIVERY_FROM = 499
DELIVERY_CHARGE = 40
MEMBER_DISCOUNT_PERCENT = 10

type CartItem = tuple[str, int, int]     # (name, price in rupees, quantity)


def subtotal(items: list[CartItem]) -> int:
    return sum(price * quantity for _name, price, quantity in items)


def member_discount(amount: int, is_member: bool) -> int:
    return amount * MEMBER_DISCOUNT_PERCENT // 100 if is_member else 0


def delivery_fee(amount: int) -> int:
    return 0 if amount >= FREE_DELIVERY_FROM else DELIVERY_CHARGE


def order_total(items: list[CartItem], is_member: bool) -> int:
    if not items:
        raise ValueError("cart is empty")
    base = subtotal(items)
    after_discount = base - member_discount(base, is_member)
    return after_discount + delivery_fee(after_discount)


cart = [("Notebook", 60, 3), ("Pen set", 150, 1)]
print(order_total(cart, is_member=False))
print(order_total(cart, is_member=True))
print(order_total([("Atlas", 520, 1)], is_member=True))
Output
370
337
508

The habits behind the rewrite:

  • Names that explain. order_total, is_member, quantity instead of calc, m, i[2]. A good name removes the need for a comment.
  • No magic numbers. Business values become named constants at the top, so changing the free-delivery limit is a one-line edit.
  • One job per function. Each small function can be read, tested and reused on its own.
  • Guard clauses. Handle the bad case first (if not items: raise) and return early, instead of nesting the main logic inside if blocks.
  • Pure functions where possible. These functions only calculate; they do not print, read files or call APIs. Pure code is the easiest to test.

The clean version also makes a business question visible: the Atlas costs ₹520, the member discount brings it to ₹468, and that is below ₹499, so delivery is charged. Should the limit apply before or after the discount? In the messy version nobody would have noticed to ask.

Composition over inheritance

Beginners often reach for inheritance to share code: class SmsOrderService(OrderService), class EmailOrderService(OrderService), and soon class SmsAndEmailOrderServiceWithLogging. A deep class tree is hard to change. The usual advice is composition over inheritance: instead of being a kind of something, an object has the parts it needs and you pass those parts in. Keep inheritance for true "is a" relationships with a small, stable base class (see Inheritance).

Strategy and registry: functions as plug-ins

The strategy pattern means choosing one of several interchangeable rules at runtime. In Java this needs an interface and a class per rule. In Python a rule can simply be a function. Combine it with a registry, a dictionary from names to implementations filled by a small decorator, and new rules can be added without touching the code that uses them:

PythonRun it yourself
from collections.abc import Callable

type PriceRule = Callable[[int], int]

RULES: dict[str, PriceRule] = {}


def rule(name: str) -> Callable[[PriceRule], PriceRule]:
    def register(func: PriceRule) -> PriceRule:
        RULES[name] = func
        return func
    return register


@rule("none")
def no_discount(amount: int) -> int:
    return amount


@rule("diwali")
def diwali_sale(amount: int) -> int:
    return amount - amount * 20 // 100


@rule("flat100")
def flat_hundred(amount: int) -> int:
    return max(amount - 100, 0) if amount >= 999 else amount


def checkout(amount: int, coupon: str) -> int:
    apply = RULES.get(coupon, no_discount)
    return apply(amount)


for coupon in ["none", "diwali", "flat100", "FAKE50"]:
    print(f"{coupon:<8} ₹{checkout(1299, coupon):,}")
print(sorted(RULES))
Output
none     ₹1,299
diwali   ₹1,040
flat100  ₹1,199
FAKE50   ₹1,299
['diwali', 'flat100', 'none']

checkout() never changes when marketing invents a new coupon; you add one decorated function. This is also how many frameworks work: Flask's @app.route() and pytest's @pytest.fixture register functions with a decorator in a similar way. The registry is also a simple factory: code asks for an implementation by name instead of naming the class or function directly. Unknown coupons fall back to no_discount instead of crashing, which is a deliberate choice you would also cover with a test.

Dependency injection, protocols and adapters

Dependency injection is a big name for a simple idea: pass an object the things it depends on, instead of letting it create them inside. Then you can swap them: a real SMS gateway in production, a fake one in tests. A Protocol (see Properties & Abstract Classes) describes what the dependency must be able to do, without forcing any inheritance. When a third-party library has the right ability but the wrong method names, a small adapter class translates:

PythonRun it yourself
from dataclasses import dataclass, field
from typing import Protocol


class Notifier(Protocol):
    def send(self, to: str, message: str) -> None: ...


class ConsoleNotifier:
    def send(self, to: str, message: str) -> None:
        print(f"[to {to}] {message}")


class FakeNotifier:
    def __init__(self) -> None:
        self.sent: list[tuple[str, str]] = []

    def send(self, to: str, message: str) -> None:
        self.sent.append((to, message))


class PartnerSmsApi:
    """Pretend third-party library with its own method names."""

    def deliver_text(self, mobile_number: str, body: str, sender_id: str) -> str:
        return f"{sender_id}->{mobile_number}: {body}"


class PartnerSmsAdapter:
    def __init__(self, api: PartnerSmsApi) -> None:
        self.api = api

    def send(self, to: str, message: str) -> None:
        print(self.api.deliver_text(mobile_number=to, body=message, sender_id="SHOPIN"))


@dataclass
class OrderService:
    notifier: Notifier
    orders: dict[int, str] = field(default_factory=dict)

    def place(self, order_id: int, mobile: str) -> None:
        self.orders[order_id] = "placed"
        self.notifier.send(mobile, f"Order {order_id} placed. Thank you!")


OrderService(ConsoleNotifier()).place(101, "9876500001")
OrderService(PartnerSmsAdapter(PartnerSmsApi())).place(102, "9876500002")

fake = FakeNotifier()
service = OrderService(fake)
service.place(103, "9876500003")
print(fake.sent, service.orders)
Output
[to 9876500001] Order 101 placed. Thank you!
SHOPIN->9876500002: Order 102 placed. Thank you!
[('9876500003', 'Order 103 placed. Thank you!')] {103: 'placed'}

OrderService knows nothing about consoles, partner APIs or tests. It only needs "something with send()". The fake notifier makes the test a one-liner with no mocking library at all, and moving to a different SMS provider later means writing one new adapter. A type checker confirms that each class really matches the Notifier protocol.

Patterns you get for free in Python

Classic patternThe Python way
Singleton (only one instance)A module: it is imported once, so module-level objects are shared
IteratorGenerators with yield (see Iterators & Generators)
DecoratorThe @decorator syntax for functions and classes
Observer (notify listeners of events)A list of callback functions you call in a loop
CommandA function or functools.partial object stored for later
Template methodA base class method that calls methods subclasses override, or a function that takes other functions as arguments

Common mistakes

  • Comments that repeat the code (i += 1 # add one to i). Write comments for why, and let names explain what.
  • Huge functions with flags. process(data, mode=3, skip=True) usually hides several functions. Split them.
  • Creating dependencies inside methods. self.sms = RealSmsGateway() inside __init__ makes testing hard; pass it in.
  • Deep inheritance trees for sharing code. Prefer composition and small helper functions.
  • Java-style getters and setters. Use plain attributes, and @property only when you need logic.

Key points

  • Clean code uses explaining names, named constants, small single-purpose functions and early returns.
  • Keep calculations pure and push printing, files and network calls to the edges of the program.
  • Prefer composition (passing parts in) over deep inheritance.
  • In Python, the strategy pattern is often just a function, and a dictionary registry acts as a simple factory.
  • Dependency injection plus a Protocol lets you swap real services, adapters and test fakes.
  • Use patterns to solve real problems, not to decorate simple code.

Exercise

A delivery app calculates fares in one long function with if vehicle == "bike": ... elif vehicle == "auto": ... elif vehicle == "car": .... Redesign it with a registry: write a fare_rule(name) decorator, three rules (bike: ₹20 + ₹8 per km, auto: ₹30 + ₹12 per km, car: ₹50 + ₹18 per km), and a fare(vehicle, km) function that raises ValueError with the list of known vehicles for an unknown one. Print the fares for 5 km and the error for "truck".

Show solution
PythonRun it yourself
from collections.abc import Callable

type FareRule = Callable[[float], float]
FARE_RULES: dict[str, FareRule] = {}


def fare_rule(name: str) -> Callable[[FareRule], FareRule]:
    def register(func: FareRule) -> FareRule:
        FARE_RULES[name] = func
        return func
    return register


@fare_rule("bike")
def bike(km: float) -> float:
    return 20 + 8 * km


@fare_rule("auto")
def auto(km: float) -> float:
    return 30 + 12 * km


@fare_rule("car")
def car(km: float) -> float:
    return 50 + 18 * km


def fare(vehicle: str, km: float) -> float:
    try:
        rule = FARE_RULES[vehicle]
    except KeyError:
        known = ", ".join(sorted(FARE_RULES))
        raise ValueError(f"unknown vehicle {vehicle!r}; choose from {known}") from None
    return rule(km)


for vehicle in ["bike", "auto", "car"]:
    print(vehicle, fare(vehicle, 5))
try:
    fare("truck", 5)
except ValueError as error:
    print(error)
Output
bike 60
auto 90
car 140
unknown vehicle 'truck'; choose from auto, bike, car

Adding an e-rickshaw now means writing one new decorated function; fare() and its tests stay unchanged. The error message lists the valid choices, which helps whoever calls the function.

Quick Quiz

1. What is a "magic number" in code?

2. In Python, what is the simplest way to implement the strategy pattern?

3. Why does OrderService take a notifier argument instead of creating an SMS client itself?

4. What is the usual Python replacement for the singleton pattern?

Learn Python with a mentor

Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call