Satyajit Sarangi bio photo

Satyajit Sarangi

I am a software engineer currently residing in Bay Area.

Email Twitter LinkedIn Github Stackoverflow

Introduction:

Often times while debugging or if you come from a typed system world it is useful to have some type verification. What I mean by that is you have written your functions in python assuming they are some particular type, however the caller calls your function with some bizarre arguments which you didn’t expect. Instead of informing the caller that the type passed is incorrect, you crash with a bizarre error message which makes no sense.

Decorators come to the rescue:

This is where python decorators come in handy. A google search comes up with many helpful results but I have listed one which I particularly like.

Python Decorators in 12 steps

Usage:

So the first question is how would my code look without such type verification.

def raise_exception(actual_type, expected_type):
    raise Exception("Expected %s to be of type %s" % (actual_type, expected_type))


def old_style_foo(a, b):
    if not isinstance(a, int):
        raise_exception(type(a), int)

    if not isinstance(b, str):
        raise_exception(type(b), str)

    print("%s: %s" % (b, a))

As you can see here, as the number of arguments increase, this becomes quite tedious to do this kind of checking. Now lets see how it would look with the verifier.

@verify(a=int, b=str)
def foo(a, b):
    print("%s: %s" % (b, a))

We could make the type verification more complex by using Union types too for example something like this.

@verify(a=U(int, str), b=str)
def foo(a, b):
    print("%s: %s" % (b, a))

So in this case ‘a’ could be either an int or a string. So the type verifier would check for all the types before raising an error.

So lets see how the error message would look in the case when we pass in a function call such as below.

foo("saa", "test")

The error message is “Exception: Expected b to be of type: str but received type: <class ‘int’>”

So lets see the code for implementing this verify decorator.

def verify(func=None, **options):
    if func is not None:
        # We received the function on this call, so we can define
        # and return the inner function
        def inner(*args, **kwargs):
            if len(options) == 0:
                raise Exception("Expected verification arguments")

            func_code = func.__code__
            arg_names = func_code.co_varnames

            for k, v in options.items():
                # Find the key in the original function
                idx = arg_names.index(k)

                if (len(args) > idx):
                    # get the idx'th arg
                    arg = args[idx]
                else:
                    # Find in the keyword args
                    if k in kwargs:
                        arg = kwargs.get(k)

                if isinstance(v, U):
                    # Unroll the types to check for multiple types
                    types_match = False
                    for dtype in v.types:
                        if isinstance(arg, dtype):
                            types_match = True

                    if types_match == False:
                        raise Exception("Expected " + str(k) + " to be of type: " + str(v) + " but received type: " + str(type(arg)))
                elif not isinstance(arg, v):
                    raise Exception("Expected " + str(k) + " to be of type: " + v.__name__ + " but received type: " + str(type(arg)))

            output = func(*args, **kwargs)
            return output

        return inner
    else:
        # We didn't receive the function on this call, so the return value
        # of this call will receive it, and we're getting the options now.
        def partial_inner(func):
            return verify(func, **options)
        return partial_inner

The structure above is a standard python decorator structure so I won’t explain the nested function structure. Instead I will focus on the type system. Python will deprecate a function called inspect.getargspec which gives the function arguments directly. So I had to directly figure out what argument is to be mapped from the function.

func_code = func.__code__
arg_names = func_code.co_varnames

These 2 lines get the arg names as they appear in function. The next step is to iterate over the argument specification received from the verify decorator.

@verify(a=U(int, str), b=str)

has to be matched to

foo("saa", 5)

So the options gives us what ‘a’ and ‘b’ types are supposed to be. We iterate over them to figure out which argument in the caller matches them.

for k, v in options.items():
    # Find the key in the original function
    idx = arg_names.index(k)

    if (len(args) > idx):
        # get the idx'th arg
        arg = args[idx]
    else:
        # Find in the keyword args
        if k in kwargs:
            arg = kwargs.get(k)

This code basically achieves this. It maps “saa” to ‘a’ and ‘5’ to ‘b’. After that the code is pretty straightforward. If its a union type then it iterates over the types and checks each type. If it’s not a union type then it simply checks the base type it gets with the arg.

So the last thing remaining in this is to see the Union class definition.

class U:
    def __init__(self, *args):
        self.types = args

    def __str__(self):
        return ",".join(self.types)

    __repr__ = __str__

That’s it. The code is not very complex. I haven’t tested thoroughly with keyword args and default arguments but it shouldn’t be too difficult to extend this or fix anything if you happen to use this code.

Github Link