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.