What about some technical stuff this week?
Writing wrappers in Python is a common practice. Whether it’s to simplify function calls, encapsulate complexity, or create a cleaner API, wrapping functions can be a great way to organize code. But there’s a catch: if you’re not typing your wrappers correctly, you might be introducing subtle bugs that your type checker won’t catch.
If you’re using Mypy (or another static type checker), you should be careful about blindly passing *args
and **kwargs
as Any
—because doing so effectively turns off your type checker, making your code vulnerable to runtime errors that should have been caught statically.
Let’s dive into why this is a problem, why traditional approaches fail, and what the correct way to handle wrapped functions is.
The Common but Flawed Wrapper Pattern
Here’s a classic example of an incorrectly typed wrapper function:
import typing
def make_request(url: str, *args: typing.Any, **kwargs: typing.Any):
return send_request(HttpClient(url), *args, **kwargs)
def send_request(client: "HttpClient", method: str = "GET", timeout: int = 5) -> str:
return f"Request sent to {client.url} with method {method} and timeout {timeout}s"
class HttpClient:
def __init__(self, url: str):
self.url = url
What’s the issue here?
At first glance, this seems fine. We’re creating an HttpClient
for a given url and passing all additional arguments directly to send_request().
But the problem arises when you pass the wrong arguments:
make_request("https://example.com", method="POST", timout=10) # ❌ Typo in "timeout"
This will result in a runtime error:
TypeError: send_request() got an unexpected keyword argument 'timout'
Since make_request()
uses *args: Any
and **kwargs: Any
, Mypy won’t flag this mistake. The type checker has no way to verify whether the arguments passed to make_request()
are valid for send_request()
.
Using Any
like this completely disables type checking, making Mypy useless for catching argument mismatches.
What About Using ParamSpec? (And Why It Doesn’t Work)
A natural instinct is to use ParamSpec
to tell Mypy that make_request()
should take the exact same arguments as send_request().
from typing import ParamSpec, Callable
P = ParamSpec("P")
def make_request(url: str, *args: P.args, **kwargs: P.kwargs):
return send_request(HttpClient(url), *args, **kwargs) # ❌ Won't work
Why doesn’t this work?
ParamSpec
is only useful for decorators and higher-order functions—functions that return another function.It does not work for simple wrappers like this, where you’re directly calling the function inside the wrapper.
If you try this, Mypy will complain that ParamSpec is being used incorrectly.
This means that traditional wrapper functions in Python—where you take *args
and **kwargs
and pass them blindly—are no longer a good practice in a world where static typing matters.
The Correct Approach: Using functools.partial
Instead of directly calling send_request() within make_request(), we should return a callable function using functools.partial
.
Here’s how you do it properly:
from functools import partial
def make_request(url: str):
return partial(send_request, HttpClient(url))
# Correct Usage
request = make_request("https://example.com")
print(request(method="POST", timeout=10)) # ✅ Works correctly
# Incorrect Usage
print(request(method="POST", timout=10)) # ❌ Mypy will catch this!
Why This Works
✅ Mypy can now properly check argument correctness: request has the exact same signature as
send_request()
, ensuring proper type safety.✅ No more unexpected runtime errors: if you pass an invalid argument, Mypy will flag it before you even run the code.
✅ More maintainable code: this pattern makes it clear what arguments belong to what function instead of having them blindly passed along.
Key Takeaways
Stop Using
*args: Any, **kwargs: Any
in Wrappers: this disables type checking and opens your code to hard-to-debug errors.ParamSpec is NOT a fix: it only works for decorators and cannot be used to type generic wrapper functions.
Use
functools.partial
Instead
This ensures that type checkers can properly verify arguments while keeping the flexibility of a wrapper.
Final Thoughts
Python’s type system has evolved significantly, and many old habits—like blindly wrapping functions with Any—should now considered bad practice.
By using functools.partial
, you ensure that your wrapped functions remain type-safe, predictable, and error-free.
Start refactoring your wrappers today—you’ll have fewer bugs, cleaner code, and a much happier type checker.
Have you encountered issues with typing wrappers in Python? Do you have alternative approaches? Let’s discuss in the comments! 🚀
If you really want the original signature with ParamSpec you can, but you need to use Concatenate and an intermediate higher-order function as well. Here is an example: https://gist.github.com/catwell/ca31fcd5e13f61bd6142d363b18df833