Skip to content

How to Validate Function Arguments and Return Values using Decorators in Python

Learn how to create reusable Python decorators for validating function arguments (*args, **kwargs) and return values to enforce type safety and business rules efficiently.

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):
            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

@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():
                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

@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):
            result = func(*args, **kwargs)
            
            if not validator(result):
                raise ValueError(f"Invalid return value: {result}")
            
            return result
        return wrapper
    return decorator

@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):
        hints = inspect.get_annotations(func)
        
        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:

  1. Use *args wrappers to validate positional arguments order-wise.
  2. Use **kwargs wrappers to validate named arguments explicitly.
  3. Validate Returns by checking the result after calling func().
  4. Keep it clean: Decorators keep your business logic free of repetitive if-else checks.
Navigation