Loading episodes…
0:00 0:00

Stop Modifying Your Classes: A Visual Guide to the Decorator Pattern in Python

00:00
BACK TO HOME

Stop Modifying Your Classes: A Visual Guide to the Decorator Pattern in Python

10xTeam December 29, 2025 11 min read

Have you ever found yourself stuck in a loop of endlessly modifying a class? You add a feature, then another, and another. Soon, your once-clean class becomes a tangled mess of conditional logic, violating one of software design’s most sacred principles: the Open/Closed Principle.

[!NOTE] The Open/Closed Principle (OCP) states that software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

This is a common problem. Imagine an InvoiceItem class. First, the business asks for gift-wrapping. Then, they want to add a discount. Next, an optional insurance fee. Each new requirement forces you to modify and re-test the core class, increasing complexity and the risk of introducing bugs.

The Decorator Pattern offers an elegant way out. It’s a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special “wrapper” objects that contain the behaviors.

The Core Idea: Wrapping Objects

Instead of cramming all possible features into a single class, you “decorate” or “wrap” a base object with new functionality. Each decorator adds a specific responsibility and then passes the request on to the object it wraps.

Let’s visualize this flow. We start with a core product and progressively wrap it with decorators for gift wrapping, insurance, and a final discount.

graph TD
    A[Core Product: Laptop @ $1000] --> B(Gift Wrap Decorator);
    B --> C{Wrapped Price: $1000 + $20 = $1020};
    C --> D(Insurance Decorator);
    D --> E{Insured Price: $1020 + $50 = $1070};
    E --> F(Discount Decorator);
    F --> G[Final Price: $1070 * 0.9 = $963];

This approach allows you to mix and match functionalities in any combination without ever touching the original Product class code.

The Problem: A Bloated Class

Let’s look at the code we want to avoid. Here’s a class that has been modified multiple times to accommodate new business rules.

class InvoiceItem:
    def __init__(self, name: str, base_price: float):
        self.name = name
        self.base_price = base_price
        self.has_gift_wrap = False
        self.gift_wrap_cost = 0.0
        self.has_discount = False
        self.discount_rate = 0.0
        self.has_insurance = False
        self.insurance_cost = 0.0

    def get_price(self) -> float:
        price = self.base_price
        
        if self.has_gift_wrap:
            price += self.gift_wrap_cost
            
        if self.has_insurance:
            price += self.insurance_cost

        if self.has_discount:
            price *= (1 - self.discount_rate)
            
        return price

# Client Code
item = InvoiceItem("Laptop", 1000.0)
item.has_gift_wrap = True
item.gift_wrap_cost = 20.0
item.has_discount = True
item.discount_rate = 0.1

# The logic is tangled inside the class
print(f"Final Price: {item.get_price()}")

This class is now difficult to maintain. What if we need to apply the discount before adding the gift wrap cost? The internal logic would need a significant rewrite.

The Solution: Implementing the Decorator Pattern

Let’s refactor this using the Decorator pattern. We’ll organize our code into a more modular structure.

invoice_system/
├── main.py
├── component.py
└── decorators.py

Step 1: Define the Common Interface (The Component)

First, we define an abstract base class (ABC) that provides an interface for both the object we want to decorate and the decorators themselves. This ensures they are interchangeable.

File: component.py

from abc import ABC, abstractmethod

class IInvoiceItem(ABC):
    """The Component Interface"""
    
    @abstractmethod
    def get_details(self) -> str:
        pass

    @abstractmethod
    def get_price(self) -> float:
        pass

Step 2: Create the Base Object (The Concrete Component)

This is the original, core object that we want to add functionality to. It implements the IInvoiceItem interface.

File: component.py

# (Add to the same file)

class Product(IInvoiceItem):
    """The Concrete Component"""
    
    def __init__(self, name: str, price: float):
        self._name = name
        self._price = price

    def get_details(self) -> str:
        return f"Product: {self._name}"

    def get_price(self) -> float:
        return self._price

Step 3: Create the Base Decorator

This is the cornerstone of the pattern. It’s an abstract class that also implements the IInvoiceItem interface. Its key job is to hold a reference to the component it wraps and delegate calls to it.

File: decorators.py

from .component import IInvoiceItem

class InvoiceDecorator(IInvoiceItem):
    """The Abstract Base Decorator"""
    
    def __init__(self, wrapped_item: IInvoiceItem):
        self._wrapped_item = wrapped_item

    def get_details(self) -> str:
        return self._wrapped_item.get_details()

    def get_price(self) -> float:
        return self._wrapped_item.get_price()

Step 4: Create Concrete Decorators

Now for the fun part. We create concrete classes for each piece of functionality. Each decorator inherits from InvoiceDecorator, calls the wrapped object’s method, and then adds its own logic.

File: decorators.py

# (Add to the same file)

class GiftWrapDecorator(InvoiceDecorator):
    """A Concrete Decorator to add gift wrapping."""
    
    def __init__(self, wrapped_item: IInvoiceItem, wrap_cost: float):
        super().__init__(wrapped_item)
        self._wrap_cost = wrap_cost

    def get_details(self) -> str:
        return f"{super().get_details()}, Gift Wrapped"

    def get_price(self) -> float:
        return super().get_price() + self._wrap_cost

class InsuranceDecorator(InvoiceDecorator):
    """A Concrete Decorator to add insurance."""

    def __init__(self, wrapped_item: IInvoiceItem, insurance_cost: float):
        super().__init__(wrapped_item)
        self._insurance_cost = insurance_cost

    def get_details(self) -> str:
        return f"{super().get_details()}, Insured"

    def get_price(self) -> float:
        return super().get_price() + self._insurance_cost

class DiscountDecorator(InvoiceDecorator):
    """A Concrete Decorator to apply a discount."""
    
    def __init__(self, wrapped_item: IInvoiceItem, discount_rate: float):
        super().__init__(wrapped_item)
        self._discount_rate = discount_rate

    def get_details(self) -> str:
        discount_percent = self._discount_rate * 100
        return f"{super().get_details()}, {discount_percent}% Discount Applied"

    def get_price(self) -> float:
        return super().get_price() * (1 - self._discount_rate)

Visualizing the New Class Structure

This new structure is much cleaner. The relationships between the components are now explicit and decoupled.

classDiagram
    direction LR
    class IInvoiceItem {
        <<interface>>
        +get_details() str
        +get_price() float
    }
    class Product {
        -name: str
        -price: float
        +get_details() str
        +get_price() float
    }
    class InvoiceDecorator {
        <<abstract>>
        #_wrapped_item: IInvoiceItem
        +get_details() str
        +get_price() float
    }
    class GiftWrapDecorator {
        -wrap_cost: float
        +get_details() str
        +get_price() float
    }
    class InsuranceDecorator {
        -insurance_cost: float
        +get_details() str
        +get_price() float
    }
    class DiscountDecorator {
        -discount_rate: float
        +get_details() str
        +get_price() float
    }

    Product --|> IInvoiceItem
    InvoiceDecorator --|> IInvoiceItem
    InvoiceDecorator o-- "1" IInvoiceItem : wraps
    GiftWrapDecorator --|> InvoiceDecorator
    InsuranceDecorator --|> InvoiceDecorator
    DiscountDecorator --|> InvoiceDecorator

Putting It All Together

Now, let’s see how the client code changes. Instead of flipping boolean flags, we compose the object we need by wrapping it with the desired decorators.

Here is a “diff” view showing the evolution from our old approach to the new, flexible one.

--- a/main_old.py
+++ b/main_new.py
@@ -1,20 +1,20 @@
-class InvoiceItem:
-    # ... all the bloated logic ...
+from component import Product
+from decorators import GiftWrapDecorator, InsuranceDecorator, DiscountDecorator
 
-def get_price(self) -> float:
-    price = self.base_price
-    if self.has_gift_wrap:
-        price += self.gift_wrap_cost
-    if self.has_insurance:
-        price += self.insurance_cost
-    if self.has_discount:
-        price *= (1 - self.discount_rate)
-    return price
+# --- Client Code ---
 
-# Client Code
-item = InvoiceItem("Laptop", 1000.0)
-item.has_gift_wrap = True
-item.gift_wrap_cost = 20.0
-item.has_discount = True
-item.discount_rate = 0.1
+# 1. Start with the core product
+product = Product("Laptop", 1000.0)
 
-print(f"Final Price: {item.get_price()}")
+# 2. Dynamically wrap it with decorators
+product_with_gift_wrap = GiftWrapDecorator(product, 20.0)
+product_with_insurance = InsuranceDecorator(product_with_gift_wrap, 50.0)
+final_product = DiscountDecorator(product_with_insurance, 0.10)
+
+# 3. The final object behaves as one, but is composed of many
+print(final_product.get_details())
+print(f"Final Price: ${final_product.get_price():.2f}")
+
+# --- Output ---
+# Product: Laptop, Gift Wrapped, Insured, 10.0% Discount Applied
+# Final Price: $963.00

Notice how easy it is to change the behavior. Don’t want a discount? Simply stop at the product_with_insurance object. Want to apply the discount before insurance? Just change the wrapping order.

[!TIP] Decorator order matters! Wrapping with DiscountDecorator and then InsuranceDecorator will produce a different result than the other way around. This flexibility is a powerful feature, but one to be mindful of during implementation.

Best Practices and Considerations

  • Single Responsibility: Keep decorators focused. A decorator should do one thing well (e.g., add a cost, apply a discount).
  • Immutability: It’s often a good practice to treat decorated objects as immutable. Instead of changing a decorator’s state, create a new decorated stack.
  • Python’s Syntactic Sugar: Python has a built-in @ syntax for decorators. While it’s based on the same pattern, it’s typically used to decorate functions or entire classes at definition time. The structural Decorator Pattern we’ve discussed here is about decorating objects at runtime, which provides greater flexibility for dynamic scenarios.
Deep Dive: Structural Decorators vs. Python's @decorator Syntax Python's `@` syntax is a clean way to apply a wrapper function to a function or class. ```python def my_decorator(func): def wrapper(): print("Something is happening before the function is called.") func() print("Something is happening after the function is called.") return wrapper @my_decorator def say_hello(): print("Hello!") say_hello() ``` This is powerful, but it's static. The `say_hello` function is *always* decorated. The structural pattern we implemented is more dynamic. It allows a client to decide *at runtime* which decorators to apply to an object, and in what combination, based on application state or user input. Both are forms of the Decorator pattern, but they solve problems at different stages of a program's life.

[!WARNING] The Decorator Pattern is excellent for adding responsibilities, but it’s not designed for removing them. Once an object is wrapped, you cannot easily “unwrap” it to remove a specific decorator from the middle of the chain.

Conclusion

The Decorator Pattern is a powerful tool for building flexible and maintainable systems. It helps you adhere to the Open/Closed Principle by allowing you to extend an object’s behavior without modifying its source code.

By breaking down functionality into a chain of composable, single-responsibility wrappers, you can create complex objects dynamically while keeping your codebase clean, decoupled, and easy to understand.

mindmap
  root((Decorator Pattern))
    Benefits
      Adheres to Open/Closed Principle
      Dynamic & Flexible Behavior
      Avoids Bloated Superclasses
      Composable Functionality
    Components
      Component Interface
      Concrete Component (Base Object)
      Base Decorator (Wrapper)
      Concrete Decorators (New Behaviors)
    When to Use?
      Add responsibilities to objects dynamically
      When subclassing is impractical
      When a class definition should be static, but its functionality can change
    Key Takeaway
      Wrap, don't modify!

Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?