Last Updated: Friday 23rd August 2013

Overview

So I was playing around with Python decorators the other day (as you do). I always wondered if you could get Python to validate the function parameter types and/or the return type, much like static languages. Some people would say this is useful, whereas others would say it's never necessary due to Python's dynamic nature. I just hacked this up to see if it could be done, and thought it was pretty cool to see what you can do with Python. So I'll leave the politics out of this :-). Feel free to put your thoughts in the comments.

This article assumes that you at least have basic knowledge of decorators, and how they work.

The following code validates the type of the top level of the function arguments and return types. However it doesn't look at the types below a data structure. For example you could specify that the 2nd argument must be a tuple. However you cannot validate sub-values, such as the following: (<type 'int'>, <type 'str'>). If anyone can pull this off, I'd like to see!

So let's get started!

Validator Exceptions

Before we start with the actual code to validate the function parameter and return type, we'll be adding in some custom exceptions. Note that this is not necessary, I just preferred it to abstract some of the messages, along with creating cleaner code. We'll be creating the following exceptions:

  • ArgumentValidationError: When the type of an argument to a function is not what it should be.
  • InvalidArgumentNumberError: When the number of arguments supplied to a function is incorrect.
  • InvalidReturnType: When the return value is the wrong type.

Each exception comes with a custom error message, so the data for the message is passed to the constructor method: __init__(). Let's take a look at the code:

Pretty straight forward. Notice that the ArgumentValidationError exception requires the parameter arg_num, to specify the Nth argument that has the wrong type. This requires an ordinal number: eg. 1st, 2nd, 3rd, etc. So our next step is to create a simple function to convert an int to an ordinal number.

Ordinal Number Converter

We've called it ordinal() just to keep things simple, where it takes one int argument. But how does it work? For numbers 6 - 20, the number suffix (in order from 6 - 20) is always "th". However outside of that range:

  • If the last number ends with 1, the suffix will be "st".
  • If the last number ends with 2, the suffix will be "nd".
  • If the last number ends with 3, the suffix will be "rd".
  • Otherwise (when the last number is >= 4), the suffix will be "th".

In this case we've used a % character, which is the modulus operator. It provides us with the remainder, when divided by a number. In this case, if we divide a number by 10, it will give us the last digit. Note that we could also use str(num)[-1]. However this is inefficient, and ugly.

Now, onto the actual validators!

Function Parameter Validation

We'll start by showing off the code to the function parameter validator (decorator), and go from there. Both validation functions are very similar, however a bit different.

Okay! So what on earth is that? Notice that with regular decorators, there's a function that returns a function. Usually the sub-function will do something either before and/or calling the function to wrap. However in our case, we're using meta-decorators. These take the decorator model one-step further, where they are essentially a decorator with a sub-decorator that accepts the function parameters of the function being checked. Pretty confusing! But once you get it, they're quite straightforward.

We're using the zip() function, which returns a list of tuples. It lets us iterate over two lists at the same time. In our case, the allowed function arguments and the actual function arguments. The zip() call is then wrapped by enumerate(). The enumerate() function itself returns an enumerate object. Every time the enumerate.next() function is called (for example in our for statement), it returns a tuple. This is in the form of loop-index, (list-1-element, list-2-element). This way we can get the index of the argument, to pass to the exception if needed. The isinstance() function is then used to compare the types of the required argument, and the actual argument. You could also use if type(accepted_arg) is type(actual_arg), however I found this way a bit cleaner syntactically.

One note to consider: After our add_nums() function has been called, technically the decorator returned the value and not add_nums(). So if you look at the returned object from add_nums(), it will actually be the decorator. Therefore it will have a different name, and you will lose your doc string. This may or may not be a big deal, however it's easy to fix. Luckily the functools.wraps() function comes to the rescue, which is a decorator itself. To use this you must import the functools module, and add the decorator above your validation decorator.

Return Type Validation

Now onto the return type validation. Here it is, in all it's glory.

As you can see, it's similar to the parameter validation function (validate_accept). However since it doesn't need to iterate all values, we just need to check if the types are the same.

Our Function to Validate

I've decided to use a very simple example function. Because ultimately, no matter how complex it is, it will still work in the same way. No need to make things more complicated than they should be! There's two implemented. One with the correct return type named add_nums_correct and another one which returns a str named add_nums_incorrect. That way we can test if the return validator works.

So let's check it out, and see if it works.

  • Jake Griffin

    The InvalidArgumentNumberError will not be thrown when you expect due to an error in your code on line 19 of your “accepts” decorator:
    >> if len(accepted_arg_types) is not len(accepted_arg_types):
    This will return False in most cases (because the arrays being compared are the same object and thus their lengths are always equal), however, if the array is long enough (> 256 elements), it will return True due to the use of “is not” instead of “!=”. You can demonstrate this in the REPL with the following code:
    >>> a = [1] * 256
    >>> len(a) is not len(a) # Returns False
    >>> a = [1] * 257
    >>> len(a) is not len(a) # Returns True

    Additionally, you are not handling keyword arguments (function_args_dict) properly. The length of the kwargs is not validated at all, nor are the types of the values passed, and you are dropping the kwargs when you call the decorated function (line 31). Also, providing any normal args as keyword args (completely valid in python) results in a TypeError:

    >>> @accepts(int)
    >>> def print_me(value):
    >>> print value

    >>> print_me(1)
    1
    >>> print_me(value=1)
    Traceback (most recent call last):
    File “”, line 1, in
    File “”, line 31, in decorator_wrapper
    TypeError: print_me() takes exactly 1 argument (0 given)

    Note that this is Python’s BUILT IN TypeError that is thrown, not your custom exceptions.

    Interesting project, I may pick up where you left off.