Python in 30 Days - Day 11

Learning Python in 30 Days by going through a refresher course. Day 11 involves decorators.

Day 11 was about decorators in Python, which allow you to write a function that will be used to encapsulate (wrap) another function later.

The Challenge:

  1. Write a decorator called timer that:
  • Records the time before and after the decorated function runs
  • Prints how long the function took in milliseconds
  • Returns the function’s result unchanged
  • (Import time from the standard library — look up time.time())
  1. Write a decorator called logger that:
  • Prints the function’s name and arguments every time it’s called
  • Prints the return value after it runs
  • Returns the result unchanged
  • (Every function has a name attribute — use it)
  1. Write a decorator called validate_positive that:
  • Checks all arguments passed to the function
  • If any argument is zero or negative, prints a warning and returns None without calling the function
  • Otherwise calls the function normally
  1. Apply all three decorators to a function called calculate_discount(price, percent) that returns the discounted price
  • Test it with valid inputs and with a negative number to prove validate_positive works

Things to figure out: how to stack multiple decorators (order matters — think about which runs first), and how to inspect *args inside the wrapper to check values.

My Code:


import time
from functools import wraps


# 1. Timer Decorator
def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()

        # Calculate duration in milliseconds
        duration_ms = (end_time - start_time) * 1000
        print(f"[Timer] {func.__name__} took {duration_ms:.4f} ms")

        return result

    return wrapper


# 2. Logger Decorator
def logger(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[Logger] Calling '{func.__name__}' with args={args}, kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"[Logger] '{func.__name__}' returned: {result}")
        return result

    return wrapper


# 3. Validate Positive Decorator
def validate_positive(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        # Check all positional arguments
        for arg in args:
            if isinstance(arg, (int, float)) and arg <= 0:
                print(
                    f"[Warning] Invalid argument '{arg}': all arguments must be positive."
                )
                return None

        # Check all keyword arguments
        for kwarg in kwargs.values():
            if isinstance(kwarg, (int, float)) and kwarg <= 0:
                print(
                    f"[Warning] Invalid argument '{kwarg}': all arguments must be positive."
                )
                return None

        return func(*args, **kwargs)

    return wrapper


# Apply all three decorators to the target function
# Note: Decorators are executed from top to bottom when checking,
# but they wrap from bottom to top (innermost to outermost).
@logger
@timer
@validate_positive
def calculate_discount(price, percent):
    # Simulating a tiny bit of processing time so the timer registers something
    time.sleep(0.01)
    return price - (price * (percent / 100))


# --- Testing ---

print("--- Testing Valid Inputs ---")
final_price = calculate_discount(100, 20)
print(f"Final output captured: {final_price}")

print("\n--- Testing Negative Inputs ---")
invalid_price = calculate_discount(-50, 20)
print(f"Final output captured: {invalid_price}")

Running the Code:

--- Testing Valid Inputs --- [Logger] Calling 'calculate_discount' with args=(100, 20), kwargs={} [Timer] calculate_discount took 12.5382 ms [Logger] 'calculate_discount' returned: 80.0 Final output captured: 80.0 

--- Testing Negative Inputs --- [Logger] Calling 'calculate_discount' with args=(-50, 20), kwargs={} [Warning] Invalid argument '-50': all arguments must be positive. [Timer] calculate_discount took 0.0041 ms [Logger] 'calculate_discount' returned: None Final output captured: None

I’m not going to lie, decorators have always made me scratch my head a bit. I think the nested nature of it is what causes me to second guess myself all the time. Either way, glad to be done with this one.