Decorators Library

Design reusable decorator libraries that power frameworks, APIs, and production systems

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Why Build Your Own Decorator Library?

High-level Python systems need common behaviors that decorators provide:

A centralized decorator library provides tested utilities, ensures consistent behavior, and speeds up development.

The Core Pattern

Every reusable decorator follows this pattern. Always use functools.wraps to preserve metadata.

Reusable Timer Decorator

A practical decorator for measuring function execution time.

Decorators With Arguments (Decorator Factories)

To accept arguments, wrap your decorator in another function that returns the actual decorator.

Reusable Logging Decorator

Log function calls with customizable logger for debugging and monitoring.

Type Validation Decorator

Enforce type checking using function annotations for safer code.

Access Control Decorator

Enforce role-based access control, commonly used in web frameworks and APIs.

Memoization / Caching Decorator

Cache expensive function results to improve performance.

Registry Pattern for Plugins

Automatically register functions or classes for plugin systems and routing.

Async-Compatible Decorators

Support both sync and async functions in your decorator library.

Stacking Decorators Properly

Multiple decorators execute bottom-to-top, but wrap top-to-bottom. Order matters!

Building a Complete Decorator Library

Organize decorators into a reusable library structure for production use.

Advanced: Exponential Backoff Retry

Production-grade retry decorator with exponential backoff and jitter.

Summary

You've learned to build production-grade decorator libraries:

Decorator libraries power major frameworks like Django, FastAPI, Flask, and Celery. A well-designed decorator library becomes reusable infrastructure that speeds up development across multiple projects.

📋 Quick Reference — Decorator Libraries

You can now build reusable decorator libraries — retry logic, rate limiting, caching, auth guards — used across entire codebases.

Up next: SQLite & ORMs — work with databases using both raw SQL and SQLAlchemy.

Practice quiz

Why should every reusable decorator apply @functools.wraps(func) to its wrapper?

  • It makes the wrapper run faster
  • It automatically caches the result
  • It preserves the original function's __name__ and __doc__
  • It is required for the decorator to work at all

Answer: It preserves the original function's __name__ and __doc__. Without functools.wraps the wrapped function's __name__ would become 'wrapper'; wraps copies over the original name and docstring.

How do you write a decorator that accepts arguments, like @retry(times=5)?

  • Wrap the decorator in an outer function that returns the actual decorator (a decorator factory)
  • Add the arguments directly to wrapper
  • Use @functools.wraps with arguments
  • It is impossible in Python

Answer: Wrap the decorator in an outer function that returns the actual decorator (a decorator factory). A decorator factory is an outer function taking the arguments and returning the real decorator, which returns the wrapper.

In the lesson's memoize decorator, after calling fibonacci(10) the cache holds how many entries?

  • 1
  • 10
  • 55
  • 11

Answer: 11. The recursive fibonacci(10) computes and caches results for n = 0 through 10, which is 11 distinct entries.

What does functools.lru_cache(maxsize=128) provide over a hand-written memoize?

  • Nothing, they are identical
  • A built-in, size-bounded cache with cache_info() statistics
  • Automatic async support
  • Type validation of arguments

Answer: A built-in, size-bounded cache with cache_info() statistics. lru_cache is a tested standard-library cache with a max size and a cache_info() method reporting hits and misses.

When decorators are stacked, in what order do they execute around the call?

  • The innermost (bottom) wraps first, and the outermost (top) runs first around the call
  • Top-to-bottom both wrapping and executing
  • Random order
  • Only the top decorator runs

Answer: The innermost (bottom) wraps first, and the outermost (top) runs first around the call. The decorator closest to the function wraps first, but the outermost decorator's 'before' code runs first when the function is called.

How does the universal_timer decorator support both sync and async functions?

  • It uses two threads
  • It only works on sync functions
  • It checks inspect.iscoroutinefunction(func) and returns an async or sync wrapper accordingly
  • It converts async functions to sync

Answer: It checks inspect.iscoroutinefunction(func) and returns an async or sync wrapper accordingly. inspect.iscoroutinefunction(func) detects coroutines so the decorator can return an async wrapper that awaits, or a plain sync wrapper.

What is the purpose of the registry pattern shown with @register_command('greet')?

  • To cache return values
  • To automatically register functions in a lookup so they can be dispatched dynamically
  • To enforce type annotations
  • To time function execution

Answer: To automatically register functions in a lookup so they can be dispatched dynamically. The decorator stores each function in a registry dict keyed by name, enabling dynamic command dispatch like routing or plugins.

The enforce_types decorator validates argument types using what?

  • Random sampling
  • A hardcoded list of types
  • The __doc__ string
  • The function's annotations read via inspect.signature

Answer: The function's annotations read via inspect.signature. It builds inspect.signature(func), binds the call's arguments, and compares each value against the parameter's annotation.

Why does the require_role access-control decorator raise PermissionError?

  • When functools.wraps is missing
  • When the user's role does not match the required_role
  • When the function is async
  • When the cache is empty

Answer: When the user's role does not match the required_role. If user.role differs from the required role the wrapper raises PermissionError, denying access.

What does exponential backoff with jitter prevent in the backoff_retry decorator?

  • Memory leaks
  • Type errors
  • Many clients retrying in lockstep (the thundering-herd problem)
  • Infinite recursion

Answer: Many clients retrying in lockstep (the thundering-herd problem). Increasing delays plus random jitter spread out retries so failed clients don't all hammer the service at the same instant.