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

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 as Any.
  • 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 particular kwargs, 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 **kwargs 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—no Any needed.
  • No runtime surprises—if you pass an invalid argument, Mypy will catch it at type-checking time.

The Core Takeaways

  1. Stop blindly using *args: Any, **kwargs: Any in function wrappers. This effectively disables type checking and can lead to runtime errors.
  2. 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.
  3. 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.

Read more