Stop Writing Blind Wrappers: How to Properly Type Your Python Wrapping Code

Python is a fantastic language for writing elegant and flexible wrappers around functions and classes. Wrapping functions is a common practice in software engineering—whether you’re abstracting a third-party library, simplifying a complex API, or just reducing boilerplate.
However, blindly wrapping functions without proper type annotations is a dangerous habit that can silently introduce hard-to-debug errors in your codebase.
If you’re using Python’s type hints with Mypy (or any static type checker), you should stop writing wrappers that blindly accept *args: Any, **kwargs: Any
—because doing so effectively disables the type checker, making it useless.
Let’s walk through why this is a problem, why traditional fixes don’t work, and how to properly structure typed wrappers in modern Python.
The Wrong Way: Blindly Wrapping Functions with Any
Let’s take a simple example where we wrap a function that formats a string.
import typing
def format_text(prefix: str, *args: typing.Any, **kwargs: typing.Any):
return base_formatter(MyFormatter(prefix), *args, **kwargs)
def base_formatter(formatter: "MyFormatter", text: str, uppercase: bool = False) -> str:
if uppercase:
return f"{formatter.prefix} {text}".upper()
return f"{formatter.prefix} {text}"
class MyFormatter:
def __init__(self, prefix: str):
self.prefix = prefix
🚨 Wrong Example: The “Old Python” Wrapping Style
We can use this function like this:
format_text("Hello", "world", uppercase=True) # Outputs: "Hello WORLD"
But there’s a major issue with this wrapper:
- We blindly accept
*args
and**kwargs
asAny
. - Mypy (or other type checkers) will NOT be able to catch errors if someone calls this function incorrectly.
- If
base_formatter()
doesn’t support a particularkwargs
, Python will fail at runtime, not during static analysis.
The Hidden Problem: Type Safety is Gone
Let’s say we make a typo when calling format_text
:
format_text("Hello", "world", uppercasez=True) # ❌ Incorrect argument name
Since **kwarg
s is typed as Any
, Mypy won’t detect this mistake. Instead, Python will raise an error only at runtime:
TypeError: base_formatter() got an unexpected keyword argument 'uppercasez'
That’s exactly what type checking is supposed to prevent—but we’ve effectively disabled it with Any
.

The First Attempt: Using ParamSpec (And Why It Doesn’t Work)
A natural instinct for someone familiar with Python’s modern typing system would be to use ParamSpec
to tell Mypy that format_text
should take exactly the same arguments as base_formatter
.
Let’s try:
from typing import ParamSpec, Callable
P = ParamSpec("P")
def format_text(prefix: str, *args: P.args, **kwargs: P.kwargs):
return base_formatter(MyFormatter(prefix), *args, **kwargs) # ❌ Won't work
💥 Oops, this doesn’t work.
Why it doesn't work:
- ParamSpec only works for decorators and higher-order functions (functions that return another function).
- This means we cannot use
ParamSpec
in simple function wrappers like this one. - If we try this, Mypy will complain that
ParamSpec
is being used incorrectly.
Takeaway: Traditional wrapper functions like this are fundamentally flawed in modern Python typing.
The Right Way: Using functools.partial
for Correct Typing
If ParamSpec won’t work, how can we wrap functions properly while keeping strong type safety?
The answer lies in higher-order functions. Instead of calling base_formatter
directly inside format_text()
, we should return a partially applied function using functools.partial
.
from functools import partial
def format_text(prefix: str):
return partial(base_formatter, MyFormatter(prefix))
# Usage
formatter = format_text("Hello")
print(formatter("world", uppercase=True)) # Correct usage
print(formatter("world", uppercasez=True)) # ❌ Mypy will catch this error!
✅ Correct Example: Using functools.partial
Why Is This the Right Approach?
- ✅ Mypy can now properly check argument correctness!
- ✅ The returned function has the exact same signature as
base_formatter
—noAny
needed. - ✅ No runtime surprises—if you pass an invalid argument, Mypy will catch it at type-checking time.
The Core Takeaways
- Stop blindly using
*args: Any, **kwargs: Any
in function wrappers. This effectively disables type checking and can lead to runtime errors. - ParamSpec is only useful for decorators and higher-order functions. If you’re not creating a decorator, it won’t work for typing regular wrappers.
- Use functools.partial instead of direct calls. This preserves the function signature, ensuring proper type checking.
Final Thoughts: Why This Matters
Python’s type system has evolved significantly over the years, and many old patterns that worked in dynamic Python no longer make sense in a world where static typing matters.
If you’re writing wrapper functions, think twice before using Any
in *args
and **kwargs
—there’s almost always a better way. Strongly typed functions lead to better maintainability, fewer runtime errors, and a much better developer experience.
Start using functools.partial
, embrace proper typing, and stop writing blind wrappers. Your future self (and your team) will thank you.