This TypeError is one of the first errors developers encounter when writing or using Python decorators. It means a function was called with an argument, but the function's definition does not accept any. In the context of decorators, this almost always comes down to the wrapper function missing *args and **kwargs. Outside of decorators, the same error surfaces when a class method is missing self or when a variable name shadows a built-in. This article walks through every scenario that triggers this error -- including async decorators and the descriptor protocol mechanics that explain why Python passes self implicitly -- and shows the exact fix for each one.
What the Error Means
The error message TypeError: wrapper() takes 0 positional arguments but 1 was given is Python telling you that a function named wrapper was called with one argument, but its definition says it accepts zero. The word wrapper in the error is the name of the inner function inside a decorator. If you see a different name -- like inner(), wrapped(), or your original function name -- the same diagnosis applies.
The "1 was given" part tells you how many arguments were passed. This number increases with more arguments: "but 2 were given", "but 3 were given", and so on. The root cause is always the same: the function's parameter list cannot accept the arguments it received. CPython raises this error in the function call machinery when the number of positional arguments supplied exceeds the number of positional parameters the code object declares (see the Python docs on TypeError).
What the @ Symbol Does Mechanically
Before diagnosing the error, you need to understand exactly what happens when Python sees @my_decorator above a function definition. The @ symbol is syntactic sugar. These two blocks of code are identical:
# Using @ syntax
@my_decorator
def greet(name):
return f"Hello, {name}"
# What Python actually executes:
def greet(name):
return f"Hello, {name}"
greet = my_decorator(greet) # greet now points to wrapper
After decoration, the name greet no longer points to the original function. It points to whatever my_decorator(greet) returned -- which is the inner wrapper function. Every subsequent call to greet() is a call to wrapper(). This is the single insight that makes every cause in this article click: the arguments you pass to the decorated function go to the wrapper, not to the original function.
What does print(type(greet)) output after @my_decorator is applied?
Reading the Traceback
When this error occurs in real code, the traceback tells you exactly which wrapper failed and where. Knowing how to read it saves significant debugging time:
Traceback (most recent call last):
File "app.py", line 14, in <module>
greet("Alice") # 1. You called greet with "Alice"
File "app.py", line 3, in my_decorator
def wrapper(): # 2. But wrapper takes no args
TypeError: wrapper() takes 0 positional arguments but 1 was given
Line 1 of the traceback shows the call site -- where you invoked the decorated function. Line 2 points to the wrapper definition inside the decorator. The function name in the error message (wrapper) tells you the name of the inner function that received the unexpected argument. If you used @functools.wraps(func), the error would instead show the original function's name (e.g., greet()), which can be confusing -- you would see an error on greet() even though the actual problem is in the decorator wrapping it.
Cause 1: Wrapper Missing *args and **kwargs
This is the single most common cause. The decorator's wrapper function is defined with no parameters, but the decorated function is called with arguments:
# BROKEN -- wrapper takes no arguments
def my_decorator(func):
def wrapper(): # <-- no parameters!
print("Before")
result = func() # <-- also calls func with no args
print("After")
return result
return wrapper
@my_decorator
def greet(name):
return f"Hello, {name}"
greet("Alice")
# TypeError: wrapper() takes 0 positional arguments but 1 was given
When you call greet("Alice"), Python calls wrapper("Alice") because greet now points to wrapper. But wrapper is defined as def wrapper(): -- it accepts nothing. The string "Alice" becomes the unexpected positional argument.
The Fix
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs): # <-- accepts anything
print("Before")
result = func(*args, **kwargs) # <-- forwards everything
print("After")
return result
return wrapper
@my_decorator
def greet(name):
return f"Hello, {name}"
print(greet("Alice")) # Before / Hello, Alice / After
Adding *args and **kwargs to both the wrapper definition and the func() call makes the decorator transparent. It accepts whatever arguments the caller passes and forwards them to the original function unchanged. The @functools.wraps(func) decorator copies the original function's __name__, __doc__, __qualname__, __annotations__, and __module__ onto the wrapper. The Python documentation for functools.wraps states that its main intended use is "in decorator functions which wrap the decorated function and return the wrapper."
Cause 2: Decorating a Function That Takes Arguments
A decorator initially written for a zero-argument function breaks when reused on a function that takes arguments. This is the same mechanical issue as Cause 1, but it surfaces when developers copy a decorator that worked in one context and apply it in another:
# This decorator was written for a specific no-arg function
def announce(func):
def wrapper():
print(f"Running {func.__name__}...")
return func()
return wrapper
# It works fine here
@announce
def startup():
print("System started")
startup() # Running startup... / System started
# But breaks here
@announce
def connect(host, port):
print(f"Connected to {host}:{port}")
connect("db.example.com", 5432)
# TypeError: wrapper() takes 0 positional arguments but 2 were given
The fix is the same: change def wrapper(): to def wrapper(*args, **kwargs): and func() to func(*args, **kwargs). This makes the decorator work for functions with any number of parameters.
Always use *args and **kwargs in decorator wrappers, even if you know the decorated function takes no arguments. This makes the decorator universally reusable and prevents this error from appearing when the decorator is applied to a different function later.
Cause 3: Decorating a Class Method
When a decorator is applied to a method inside a class, the method receives self (or cls for class methods) as an implicit first argument. If the wrapper does not accept arguments, self becomes the unexpected positional argument:
def log_call(func):
def wrapper(): # <-- no parameters
print(f"Calling {func.__name__}")
return func()
return wrapper
class UserService:
@log_call
def get_profile(self, user_id):
return {"id": user_id}
service = UserService()
service.get_profile(42)
# TypeError: wrapper() takes 0 positional arguments but 2 were given
The error says "2 were given" because Python passes both self (the UserService instance) and 42 (the user_id argument) to the wrapper. The wrapper accepts neither.
The Fix
import functools
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs): # <-- captures self + user_id
print(f"Calling {func.__name__}")
return func(*args, **kwargs) # <-- forwards both
return wrapper
class UserService:
@log_call
def get_profile(self, user_id):
return {"id": user_id}
service = UserService()
print(service.get_profile(42)) # Calling get_profile / {'id': 42}
The *args captures self as the first positional argument and user_id as the second. The wrapper does not need to know about self explicitly -- it just passes everything through.
A decorator wrapper uses def wrapper(self, *args, **kwargs) instead of def wrapper(*args, **kwargs). What happens when applied to a plain (non-method) function?
Cause 4: Missing self in a Class Method (Non-Decorator)
This variant of the error has nothing to do with decorators but produces the same error message. When a class method is defined without self as its first parameter, calling it on an instance triggers the error because Python implicitly passes the instance:
class Calculator:
def add(): # <-- missing self!
return 2 + 2
calc = Calculator()
calc.add()
# TypeError: add() takes 0 positional arguments but 1 was given
Python passes calc as the implicit first argument to add(), but add accepts nothing. There are three fixes depending on the intent:
# Fix 1: Add self (if the method needs instance access)
class Calculator:
def add(self):
return 2 + 2
# Fix 2: Use @staticmethod (if the method needs no instance or class access)
class Calculator:
@staticmethod
def add():
return 2 + 2
# Fix 3: Use @classmethod (if the method needs class access but not instance)
class Calculator:
@classmethod
def add(cls):
return 2 + 2
Choose self when the method reads or writes instance attributes. Choose @staticmethod when the method is a utility that belongs to the class namespace but does not need the instance or the class itself. Choose @classmethod when the method needs to reference the class (for example, as an alternate constructor like dict.fromkeys()) but does not need a specific instance.
Cause 5: Parameterized Decorator Without Parentheses
A parameterized decorator is meant to be called with arguments before being applied: @repeat(3). Forgetting the parentheses -- writing @repeat -- passes the decorated function to the outer factory as if it were the configuration argument:
import functools
def repeat(n):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for _ in range(n):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
# WRONG -- missing parentheses
@repeat # repeat receives greet as 'n', decorator is never called
def greet(name):
print(f"Hello, {name}")
greet("Alice")
# TypeError: decorator() takes 1 positional argument but 2 were given
# (or other confusing errors)
Without parentheses, repeat receives the greet function as n. Then greet is replaced by decorator (the returned inner function). When greet("Alice") is called, Python calls decorator("Alice"), which expects a function, not a string.
The Fix
@repeat(3) # <-- parentheses required!
def greet(name):
print(f"Hello, {name}")
greet("Alice") # Hello, Alice (printed 3 times)
A parameterized decorator @cache(maxsize=128) works correctly. A colleague rewrites it as @cache and passes maxsize at call time. What does cache receive as its first argument?
Cause 6: Class-Based Decorator Missing __call__ Arguments
A class-based decorator uses __call__ as its wrapper. If __call__ is defined without *args and **kwargs, it cannot accept the arguments passed to the decorated function:
class Logger:
def __init__(self, func):
self.func = func
def __call__(self): # <-- only accepts self
print(f"Calling {self.func.__name__}")
return self.func()
@Logger
def compute(x, y):
return x + y
compute(3, 5)
# TypeError: Logger.__call__() takes 1 positional argument but 3 were given
The __call__ method receives self (the Logger instance) implicitly, plus 3 and 5 from the caller -- three arguments total, but it only accepts one (self).
The Fix
import functools
class Logger:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
def __call__(self, *args, **kwargs): # <-- accepts anything
print(f"Calling {self.func.__name__}")
return self.func(*args, **kwargs) # <-- forwards everything
@Logger
def compute(x, y):
return x + y
print(compute(3, 5)) # Calling compute / 8
Cause 7: Async Decorator Wrapper Missing Arguments
Async decorators follow the same rules as synchronous ones. If the wrapper is an async def with no parameters, calling the decorated async function with arguments produces the same TypeError:
import asyncio
def log_async(func):
async def wrapper(): # <-- no parameters
print(f"Starting {func.__name__}")
result = await func() # <-- no forwarding
print(f"Finished {func.__name__}")
return result
return wrapper
@log_async
async def fetch_data(url):
await asyncio.sleep(1)
return f"Data from {url}"
asyncio.run(fetch_data("https://example.com"))
# TypeError: wrapper() takes 0 positional arguments but 1 was given
The mechanics are identical to the synchronous case. The @log_async decorator replaces fetch_data with wrapper, so fetch_data("https://example.com") becomes wrapper("https://example.com"). The wrapper cannot accept that string argument.
The Fix
import asyncio
import functools
def log_async(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs): # <-- accepts anything
print(f"Starting {func.__name__}")
result = await func(*args, **kwargs) # <-- forwards everything
print(f"Finished {func.__name__}")
return result
return wrapper
@log_async
async def fetch_data(url):
await asyncio.sleep(1)
return f"Data from {url}"
print(asyncio.run(fetch_data("https://example.com")))
# Starting fetch_data / Finished fetch_data / Data from https://example.com
If your decorator needs to handle both sync and async functions, check with asyncio.iscoroutinefunction(func) inside the decorator and return either a sync or async wrapper accordingly.
An async decorator wrapper uses result = func(*args, **kwargs) instead of result = await func(*args, **kwargs). What happens?
Cause 8: Shadowed Built-in Name
This cause has nothing to do with decorators. If you define a function or variable with the same name as a Python built-in -- like str, list, int, or dict -- you overwrite that built-in in the current scope. When you later try to call the real built-in, you are calling your zero-argument function instead:
# Accidentally overwrites the built-in str
def str():
return "hello"
# Later, when you try to convert an integer to a string:
result = str(42)
# TypeError: str() takes 0 positional arguments but 1 was given
The call str(42) no longer calls the built-in str class. It calls your custom function, which takes no arguments. The same problem occurs with any built-in: list, dict, int, type, id, input, and others.
The Fix
# Rename your function to avoid the collision
def get_greeting():
return "hello"
# The built-in str works normally again
result = str(42) # "42"
This bug is especially hard to spot when the shadowing happens in a different part of a large file. If the error message names a built-in function rather than wrapper, check your entire file for variable or function definitions that reuse that name.
Why Python Passes self Automatically: The Descriptor Protocol
Causes 3, 4, and 5 all involve Python's implicit passing of self. Understanding why Python does this -- and why it trips up decorators -- requires knowing about the descriptor protocol. This is the mechanism that turns a plain function defined in a class body into a bound method that automatically receives the instance as its first argument.
When you access a method on an instance -- for example, obj.some_method -- Python does not simply look up the function in the class dictionary and return it. Instead, the function's __get__ method is invoked. According to the Python Descriptor HowTo Guide, all functions are non-data descriptors that produce bound methods when accessed through an instance. This converts obj.f(*args) into f(obj, *args) at the descriptor level. In pure Python, the binding looks like this:
import types
class Function:
def __get__(self, obj, objtype=None):
if obj is None:
return self
return types.MethodType(self, obj)
# This is a simplified model of CPython's func_descr_get()
# in Objects/funcobject.c
When a decorator replaces a method with a plain wrapper function, that wrapper is still a function object with its own __get__. When you call obj.wrapper(), the descriptor protocol still fires, binding obj as the first argument. If the wrapper was defined as def wrapper():, it cannot receive that argument, and the TypeError is raised.
This is also why @staticmethod fixes the error in Cause 4. The staticmethod descriptor's __get__ returns the underlying function without any binding -- no self is prepended. The Descriptor HowTo Guide confirms that static methods bypass the normal method-binding mechanism entirely, making the function accessible without any implicit first argument.
The Universal Fix
Every cause in this article has the same underlying fix: make the wrapper accept any arguments and forward them to the original function. This is the decorator boilerplate you should use for every decorator:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# your logic here
return func(*args, **kwargs)
return wrapper
If every decorator starts from this template, this TypeError cannot occur. The *args catches all positional arguments (including self from methods), and **kwargs catches all keyword arguments. The wrapper is transparent to any function signature.
Beyond *args, **kwargs: Deeper Solutions
Adding *args, **kwargs fixes the error, but it is the blunt-instrument approach. In production code, there are more precise and more robust strategies depending on the situation.
Type-Safe Decorators with ParamSpec
The *args, **kwargs pattern erases the decorated function's type signature. Type checkers like mypy and pyright cannot verify that callers pass the right arguments. Python 3.10 introduced typing.ParamSpec (backported via typing_extensions for 3.9) to solve this. A ParamSpec-typed decorator preserves the original function's parameter types through the wrapper:
import functools
from typing import TypeVar, ParamSpec, Callable
P = ParamSpec("P")
R = TypeVar("R")
def my_decorator(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
@my_decorator
def greet(name: str, excited: bool = False) -> str:
return f"Hello, {name}{'!' if excited else '.'}"
# mypy now knows: greet(name: str, excited: bool = False) -> str
greet(42) # mypy error: expected str, got int
greet("Alice") # ok
With ParamSpec, the type checker sees through the decorator. It knows the wrapper accepts exactly the same parameters as greet, not a generic *args, **kwargs. This catches argument errors at static analysis time rather than at runtime.
The wrapt Library: Descriptors Handled for You
The third-party wrapt library eliminates the entire class of errors discussed in this article. A wrapt decorator automatically handles the descriptor protocol, so it works correctly on functions, instance methods, class methods, and static methods without the developer needing to think about self binding:
import wrapt
@wrapt.decorator
def my_decorator(wrapped, instance, args, kwargs):
print("Before")
result = wrapped(*args, **kwargs)
print("After")
return result
# Works on plain functions, instance methods, classmethods,
# and staticmethods without any changes to the decorator code
The instance parameter tells you what the function was bound to: None for plain functions, the object instance for methods, or the class for classmethods. This means a single decorator definition works everywhere without modification.
Catching It Before Runtime: Linters and Static Analysis
Rather than discovering this TypeError at runtime, you can catch it during development. The flake8-bugbear plugin flags common decorator mistakes. The pylint checker catches missing self in class methods. And type checkers like mypy --strict and pyright flag signature mismatches when decorators are typed with ParamSpec. Running these tools in a pre-commit hook or CI pipeline prevents the error from ever reaching production.
Debugging with inspect.signature()
When you cannot tell what a decorated function's effective signature is, inspect.signature() shows you:
import inspect
# Without @functools.wraps:
print(inspect.signature(greet)) # (*args, **kwargs) -- opaque
# With @functools.wraps:
print(inspect.signature(greet)) # (name: str, excited: bool = False) -- original preserved
If inspect.signature() shows (*args, **kwargs) when you expected named parameters, the decorator is not using @functools.wraps. If it shows () with no parameters at all, you have found the broken wrapper that is causing the TypeError.
You use ParamSpec to type your decorator. A colleague calls greet(123) where greet expects a str. When is the error caught?
The Silent Return Bug
Once you fix the TypeError by adding *args and **kwargs, there is a second bug that catches developers off guard. If the wrapper calls func(*args, **kwargs) but forgets to return the result, the decorated function silently returns None:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before")
func(*args, **kwargs) # BUG: no return!
print("After")
return wrapper
@my_decorator
def compute(x, y):
return x + y
result = compute(3, 5)
print(result) # None -- the return value was silently lost
The wrapper calls func(3, 5), which returns 8, but that return value is never captured or forwarded. Since the wrapper itself has no return statement, Python returns None by default. This bug produces no error message at all -- which makes it harder to catch than the TypeError. The fix is to always write return func(*args, **kwargs), not just func(*args, **kwargs).
Stacking Multiple Decorators
When multiple decorators are stacked on a single function, they apply from bottom to top. Each decorator wraps the result of the one below it:
@decorator_a
@decorator_b
@decorator_c
def my_function(x):
return x
# Equivalent to:
# my_function = decorator_a(decorator_b(decorator_c(my_function)))
If any decorator in the stack has a wrapper that does not accept *args and **kwargs, the chain breaks. The error message will name whichever wrapper rejected the arguments, which may be several layers removed from the call site. When debugging stacked decorators, start by checking each decorator independently -- temporarily comment out all but one and test them in isolation.
Each code snippet below contains a bug that would produce a TypeError related to positional arguments. Find it.
Bug 1 of 3
import functools
def timer(func):
@functools.wraps(func)
def wrapper():
start = time.time()
result = func()
print(f"Took {time.time() - start:.3f}s")
return result
return wrapper
@timer
def fetch_users(db, limit=100):
return db.query("SELECT * FROM users LIMIT %s", limit)
Bug 2 of 3
import functools
def require_auth(role):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not current_user.has_role(role):
raise PermissionError("Access denied")
return func(*args, **kwargs)
return wrapper
return decorator
@require_auth
def delete_user(user_id):
db.delete(user_id)
Bug 3 of 3
class RateLimiter:
def __init__(self, func):
self.func = func
self.calls = 0
def __call__(self):
self.calls += 1
if self.calls > 10:
raise RuntimeError("Rate limit exceeded")
return self.func()
@RateLimiter
def send_email(to, subject, body):
smtp.send(to, subject, body)
Quick Diagnosis Checklist
wrapper() takes 0 ... but 1 was given
*args, **kwargs*args, **kwargs to wrapper and func callwrapper() takes 0 ... but 2 were given
*args, **kwargs to wrappermethod() takes 0 ... but 1 was given
selfself or use @staticmethoddecorator() takes 1 ... but 2 were given
() after decorator name: @deco()__call__() takes 1 ... but 3 were given
__call__ missing args*args, **kwargs to __call__wrapper() takes 0 ... in async context
*args, **kwargs*args, **kwargs and await func(*args, **kwargs)Error names a built-in like str()
Key Takeaways
- The error means: a function received an argument it cannot accept. The mismatch between the number of arguments passed and the number the function's parameter list accepts is what triggers the
TypeError. - In decorators, the root cause is almost always a wrapper defined without
*argsand**kwargs. The wrapper replaces the original function, so it must be able to accept and forward any arguments the original function expects. - Methods add an implicit argument. Instance methods receive
self, class methods receivecls. This happens through the descriptor protocol -- functions have a__get__method that binds the instance as the first argument. Decorators applied to methods must account for this extra argument through*args. - Parameterized decorators require parentheses. Writing
@repeatinstead of@repeat(3)passes the function to the factory instead of calling the factory first, producing a cascade of confusing errors. - Class-based decorators need
*args, **kwargson__call__. The__call__method is the wrapper, and it follows the same rules as any function-based wrapper. - Async wrappers follow identical rules. An
async def wrapper()without parameters fails the same way a sync one does. Always useasync def wrapper(*args, **kwargs)andawait func(*args, **kwargs). - Shadowed built-ins produce the same error message. If a variable or function name collides with a built-in like
str,list, orint, calling the built-in triggers thisTypeErrorbecause your function is called instead. - Use the universal boilerplate for every decorator. Starting from
def wrapper(*args, **kwargs): return func(*args, **kwargs)with@functools.wraps(func)prevents this entire category of errors.
This TypeError is one of the clearest errors Python produces -- it tells you exactly how many arguments were expected versus received. The fix is equally clear: make the wrapper accept and forward arguments generically. Once the *args, **kwargs pattern becomes a reflex in every decorator you write, this error stops appearing entirely.
Question 1 of 3: After applying @my_decorator to a function greet, what does the name greet refer to?
Question 2 of 3: A decorator works fine on a standalone function but produces TypeError: wrapper() takes 0 positional arguments but 2 were given when applied to a class method called with one argument. Why does the error say 2?
Question 3 of 3: You write a decorator with def wrapper(*args, **kwargs): func(*args, **kwargs). No TypeError occurs, but the decorated function returns None when it should return a value. What is wrong?
How to Fix This Error: Step by Step
- Read the error message. Identify the function name (
wrapper,inner, or your original function name if@functools.wrapsis in use) and the argument count mismatch. The name tells you which function rejected the arguments. - Locate the wrapper definition. Find the function named in the error. If it is inside a decorator, check whether its parameter list includes
*argsand**kwargs. If it is a class method, check whetherselfis the first parameter. - Add
*argsand**kwargsto the wrapper. Changedef wrapper():todef wrapper(*args, **kwargs):and change the innerfunc()call tofunc(*args, **kwargs). This makes the wrapper accept and forward any arguments. - Add
@functools.wraps(func). Addimport functoolsat the top and@functools.wraps(func)above the wrapper definition. This preserves the original function's name, docstring, and module in the wrapper. - Verify the return value is forwarded. Ensure the wrapper uses
return func(*args, **kwargs), not justfunc(*args, **kwargs). Withoutreturn, the decorated function silently returnsNone.
Frequently Asked Questions
What does TypeError: wrapper() takes 0 positional arguments but 1 was given mean?
This error means a function named wrapper was called with one argument, but its definition accepts zero. In decorator contexts, this happens because the decorator replaces the original function with wrapper, so any arguments passed to the original function go to wrapper instead. If wrapper is defined as def wrapper(): with no parameters, it cannot accept those arguments.
How do I fix a decorator that causes TypeError: takes 0 positional arguments?
Add *args and **kwargs to the wrapper function's parameter list and forward them to the original function: def wrapper(*args, **kwargs): return func(*args, **kwargs). This makes the wrapper accept and pass through any arguments the caller provides. Also add @functools.wraps(func) above the wrapper to preserve the original function's metadata.
Why does a class method produce takes 0 positional arguments but 1 was given?
Python uses the descriptor protocol to automatically pass the instance (self) as the first argument to instance methods. When a method is defined without self in its parameter list, that implicit instance argument has nowhere to go, triggering this TypeError. The fix is to add self as the first parameter or use @staticmethod if the method does not need instance access.
Why should I use functools.wraps in every decorator?
functools.wraps copies the original function's __name__, __doc__, __qualname__, __annotations__, and __module__ onto the wrapper function. Without it, debugging tools, documentation generators, and introspection will show the wrapper's metadata instead of the original function's. The Python documentation describes its primary purpose as preserving the identity of the original function when it is enclosed by a decorator.
Does the same error occur with async decorators?
Yes. Async wrapper functions follow the same rules as synchronous ones. If an async def wrapper() is defined without *args and **kwargs, calling the decorated async function with arguments produces the same TypeError. The fix is identical: define the wrapper as async def wrapper(*args, **kwargs) and call the original with await func(*args, **kwargs).
What is the universal decorator boilerplate that prevents this error?
Use this template for every decorator: import functools, then def my_decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper. The *args captures all positional arguments including self from methods, and **kwargs captures all keyword arguments. This makes the wrapper transparent to any function signature.