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 validationPart 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.