How to Validate Function Arguments and Return Values using Decorators in Python
Validating function inputs and outputs is critical for robust applications. While you can write validation logic directly inside every function, this violates the DRY (Don't Repeat Yourself) principle. Python decorators offer a clean, reusable way to wrap functions with validation logic, keeping the core business logic separate from error handling.
This guide explains how to write custom decorators to validate arguments, handle keyword arguments (kwargs), and ensure return values meet specific criteria.
Basic Concept: The Validation Wrapper
A decorator is a function that accepts another function (func), defines a wrapper function that adds logic (like validation), and returns that wrapper.
If validation fails inside the wrapper, we raise an exception (usually ValueError or TypeError) before calling func.
Method 1: Validating Positional Arguments (*args)
This method checks arguments passed by position. The decorator accepts validator functions and applies them to the corresponding arguments of the decorated function.
def validate_args(*validators):
def decorator(func):
def wrapper(*args, **kwargs):
# Check if we have matching validators for the arguments provided
for val_func, arg in zip(validators, args):
if not val_func(arg):
raise ValueError(f"Invalid argument: {arg}")
return func(*args, **kwargs)
return wrapper
return decorator
# ✅ Usage: First arg must be int, second must be positive
@validate_args(lambda x: isinstance(x, int), lambda x: x > 0)
def divide(a, b):
return a / b
print(divide(10, 2)) # Works
try:
print(divide(10, 0)) # Fails (0 is not > 0)
except ValueError as e:
print(f"Error: {e}")
Output:
5.0
Error: Invalid argument: 0
Method 2: Validating Keyword Arguments (**kwargs)
Often, functions are called using named arguments. To validate these, we pass a dictionary of validators to the decorator mapping argument names to validation rules.
def validate_kwargs(**validators):
def decorator(func):
def wrapper(*args, **kwargs):
for name, val_func in validators.items():
# Only validate if the argument is actually present in kwargs
if name in kwargs:
if not val_func(kwargs[name]):
raise ValueError(f"Invalid value for argument '{name}': {kwargs[name]}")
return func(*args, **kwargs)
return wrapper
return decorator
# ✅ Usage: 'age' must be positive, 'name' must be a string
@validate_kwargs(age=lambda x: x > 0, name=lambda x: isinstance(x, str))
def create_user(name, age):
return f"User {name} created."
print(create_user(name="Alice", age=30)) # Works
try:
create_user(name="Bob", age=-5) # Fails
except ValueError as e:
print(f"Error: {e}")
Output:
User Alice created.
Error: Invalid value for argument 'age': -5
Method 3: Validating Return Values
Sometimes you need to ensure a function returns data in a specific format (e.g., a dictionary with specific keys). The validation happens after the function executes.
def validate_return(validator):
def decorator(func):
def wrapper(*args, **kwargs):
# 1. Execute the function first
result = func(*args, **kwargs)
# 2. Validate the result
if not validator(result):
raise ValueError(f"Invalid return value: {result}")
return result
return wrapper
return decorator
# ✅ Usage: Must return a positive integer
@validate_return(lambda x: isinstance(x, int) and x > 0)
def calculate_score(points):
return points * 10
try:
print(calculate_score(5)) # Returns 50 (Valid)
print(calculate_score(-1)) # Returns -10 (Invalid)
except ValueError as e:
print(f"Error: {e}")
Output:
50
Error: Invalid return value: -10
Real-World Example: Type Enforcement
While type hints (def func(x: int)) exist, Python doesn't enforce them at runtime. A decorator can use these hints to strictly enforce types.
import inspect
def enforce_types(func):
def wrapper(*args, **kwargs):
# Get the type hints
hints = inspect.get_annotations(func)
# Check positional args (simplified for demonstration)
# Note: A robust implementation would map args to parameter names
arg_names = inspect.signature(func).parameters.keys()
for name, value in zip(arg_names, args):
expected_type = hints.get(name)
if expected_type and not isinstance(value, expected_type):
raise TypeError(f"Argument '{name}' must be {expected_type.__name__}, got {type(value).__name__}")
return func(*args, **kwargs)
return wrapper
@enforce_types
def repeat_text(text: str, times: int):
return text * times
try:
repeat_text("Hello", "5") # "5" is str, int expected
except TypeError as e:
print(f"Type Error: {e}")
Output:
Type Error: Argument 'times' must be int, got str
Conclusion
Decorators provide a powerful, reusable mechanism for validation:
- Use
*argswrappers to validate positional arguments order-wise. - Use
**kwargswrappers to validate named arguments explicitly. - Validate Returns by checking the result after calling
func(). - Keep it clean: Decorators keep your business logic free of repetitive
if-elsechecks.