Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
316: ParamSpec and Callable Typing
If you've been writing decorators in Python, you've probably hit a wall where your type hints just... give up. You wrap a function to add logging or timing, and suddenly your IDE treats the decorated function as if it takes any arguments and returns anything. It's frustrating because you've spent all this time typing your business logic, only to have a decorator wipe out all that helpful metadata.
The problem with generic decorators
Let's say I'm building a small utility to time how long certain database queries take. My first instinct is to write a simple decorator using Callable. Here is how I usually start, and where I usually mess up:
from typing import Callable, Any
import time
from functools import wraps
def time_execution(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
@time_execution
def fetch_user_data(user_id: int, include_details: bool = False) -> str:
return f"Data for {user_id}"
At first glance, this looks fine. It runs perfectly. But here is the catch: if you try to call fetch_user_data(user_id="not-an-int"), your type checker won't complain. By using Callable[..., Any], I've essentially told Python, "This function now takes whatever it wants and returns whatever it feels like." I've erased the signature of fetch_user_data.
Bringing in ParamSpec to save our signatures
To fix this, we need a way to say: "Whatever parameters the original function had, the wrapper should have those exact same parameters." This is exactly why ParamSpec was introduced in Python 3.10.
I need two things here: a TypeVar to track the return type and a ParamSpec to track the arguments. I'll call the ParamSpec P and the return type R. This is a common convention in the community.
from typing import Callable, TypeVar, ParamSpec
import time
from functools import wraps
P = ParamSpec("P")
R = TypeVar("R")
def time_execution(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
Notice the magic happening in Callable[P, R]. By using P, I'm capturing the full parameter list (including names and default values) of the function being decorated. Then, by using *args: P.args and **kwargs: P.kwargs, I'm telling the type checker that the wrapper's arguments are identical to the original function's arguments.
Seeing it in action
Now, let's look at how this actually behaves when we use it. I'll define a couple of different functions to make sure the generic nature of our decorator is actually working.
@time_execution
def calculate_total(price: float, tax: float) -> float:
return price + (price * tax)
@time_execution
def greet(name: str, greeting: str = "Hello") -> str:
return f"{greeting}, {name}!"
# The IDE now knows exactly what these want:
calculate_total(100.0, 0.05) # Correct
greet("Alice") # Correct
# greet(123) # Type checker error: Expected str, got int
By using ParamSpec, we've created a "transparent" decorator. The logic is injected, but the type identity of the original function remains intact. It's a small change in the code, but it's the difference between a codebase that feels guessed at and one that feels engineered.
📋 Practical Task
Exercise: Type-Safe Authorization Decorator
You are building a system where certain functions require a "user" object to be passed as the first argument for authorization purposes. You need to create a decorator called require_auth that logs the user's ID before executing the function, but it must preserve the type hints of the wrapped function so that other developers don't pass the wrong arguments.
Requirements:
- Use
ParamSpecandTypeVarto ensure the decorated function maintains its original signature. - The decorator should assume the first argument of the wrapped function is a
Userobject (you can define a simpleUserdataclass with anid: intattribute). - The decorator should print
"Authorizing user {user_id}..."before calling the actual function. - Implement a sample function
update_profile(user: User, bio: str) -> booland decorate it withrequire_auth. - Verify that calling
update_profilewith a non-stringbiotriggers a type-checker warning (or simply ensure the signature is correctly preserved).
There are no comments for now.