๐Ÿ  VisualStudioTutor.com  ยท  Python Tutorial Home  ยท  Python Lesson 11 of 40
Lesson 11 of 40 OOP Intermediate โฑ 35 min

Decorators & Descriptors

Create reusable decorators with @wraps, build parameterised decorators, implement descriptor protocol (__get__, __set__) for powerful attribute control.

Part 1: What You Will Learn

  • Write function decorators and preserve metadata with functools.wraps.
  • Create parameterised decorators.
  • Use descriptors to control how attributes are read and written.
  • Recognise when a decorator or descriptor is preferable to repeated validation code.

Part 2: Key Concepts

A decorator receives a function or class and returns a modified replacement. A descriptor is an object that implements methods such as __get__ and __set__ to manage attribute access. Both features let you move cross-cutting behaviour out of business logic.

Part 3: Topic-Specific Code Example

from functools import wraps
from typing import Callable, TypeVar, ParamSpec

P = ParamSpec("P")
R = TypeVar("R")

def announce(label: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
    def decorator(func: Callable[P, R]) -> Callable[P, R]:
        @wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            print(f"[{label}] Calling {func.__name__}")
            result = func(*args, **kwargs)
            print(f"[{label}] Finished {func.__name__}")
            return result
        return wrapper
    return decorator

class PositiveNumber:
    def __set_name__(self, owner: type, name: str) -> None:
        self.private_name = f"_{name}"

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self.private_name)

    def __set__(self, instance, value: float) -> None:
        if value <= 0:
            raise ValueError("Value must be greater than zero")
        setattr(instance, self.private_name, value)

class Product:
    price = PositiveNumber()
    stock = PositiveNumber()

    def __init__(self, name: str, price: float, stock: int) -> None:
        self.name = name
        self.price = price
        self.stock = stock

    @announce("INVENTORY")
    def inventory_value(self) -> float:
        return self.price * self.stock

product = Product("Keyboard", 89.90, 12)
print(f"Inventory value: RM{product.inventory_value():.2f}")
# product.price = -10   # Uncomment to see descriptor validation

Part 4: How the Example Works

@announce("INVENTORY") is a parameterised decorator: the outer function receives configuration, the middle function receives the decorated function, and wrapper() runs before and after it. PositiveNumber is reused for both price and stock, so validation is defined once rather than duplicated in several properties.

Part 5: Hands-On Practice

Mini project โ€” Validated Student Record. Write a Range descriptor that accepts minimum and maximum values. Use it to enforce a student mark from 0 to 100. Add a @log_action decorator to the method that calculates the grade.

Part 6: Next Steps

Run and modify the examples in Visual Studio 2026, then continue to Lesson 12. Return to Python Tutorial Home to review the complete curriculum.

๐Ÿ“˜ Want the complete guide with projects? Get the book โ†’