How to use Python decorators to check function arguments?

From the Decorators for Functions and Methods:

Python 2

def accepts(*types):
    def check_accepts(f):
        assert len(types) == f.func_code.co_argcount
        def new_f(*args, **kwds):
            for (a, t) in zip(args, types):
                assert isinstance(a, t), \
                       "arg %r does not match %s" % (a,t)
            return f(*args, **kwds)
        new_f.func_name = f.func_name
        return new_f
    return check_accepts

Python 3

In Python 3 func_code has changed to __code__ and func_name has changed to __name__.

def accepts(*types):
    def check_accepts(f):
        assert len(types) == f.__code__.co_argcount
        def new_f(*args, **kwds):
            for (a, t) in zip(args, types):
                assert isinstance(a, t), \
                       "arg %r does not match %s" % (a,t)
            return f(*args, **kwds)
        new_f.__name__ = f.__name__
        return new_f
    return check_accepts

Usage:

@accepts(int, (int,float))
def func(arg1, arg2):
    return arg1 * arg2

func(3, 2) # -> 6
func('3', 2) # -> AssertionError: arg '3' does not match <type 'int'>

arg2 can be either int or float


On Python 3.3, you can use function annotations and inspect:

import inspect

def validate(f):
    def wrapper(*args):
        fname = f.__name__
        fsig = inspect.signature(f)
        vars = ', '.join('{}={}'.format(*pair) for pair in zip(fsig.parameters, args))
        params={k:v for k,v in zip(fsig.parameters, args)}
        print('wrapped call to {}({})'.format(fname, params))
        for k, v in fsig.parameters.items():
            p=params[k]
            msg='call to {}({}): {} failed {})'.format(fname, vars, k, v.annotation.__name__)
            assert v.annotation(params[k]), msg
        ret = f(*args)
        print('  returning {} with annotation: "{}"'.format(ret, fsig.return_annotation))
        return ret
    return wrapper

@validate
def xXy(x: lambda _x: 10<_x<100, y: lambda _y: isinstance(_y,float)) -> ('x times y','in X and Y units'):
    return x*y

xy = xXy(10,3)
print(xy)

If there is a validation error, prints:

AssertionError: call to xXy(x=12, y=3): y failed <lambda>)

If there is not a validation error, prints:

wrapped call to xXy({'y': 3.0, 'x': 12})
  returning 36.0 with annotation: "('x times y', 'in X and Y units')"

You can use a function rather than a lambda to get a name in the assertion failure.