Skip to content
Antoine Prouvost edited this page Feb 20, 2018 · 2 revisions

Apply types as decorator

Starting in Python3 we can define types hints in functions, it looks like this:

def f(x: str, y):
    print(type(x), type(y))

Notice we added the str type. This is called a hint because it doesn't mean anything for Python, we can call our function with any type:

>>> f(3, 4)
<class 'int'> <class 'int'>

These hints have been introduced to let programmers define even more awesome libraries. We're going to write a decorator (something that change the behavior of our function) to force the casting into this type. That way, any time we call

f(x, y)

We would actually be calling

f(str(x), y)

Let's define the following decorator (packages are in the default library).

import functools
import inspect

# This is our decorator, it's a function that takes original
# function (func) and return a modified function (wrapper)
def apply_type(func):
    # This decorator let us transpose the meta information
    # of func on our new function wrapper
    @functools.wraps(func)
    # This is the new function we're creating. It takes
    # whatever we give it (*args, **kwargs)
    def wrapper(*args, **kwargs):
        # We inspect the signature of func
        sig = inspect.signature(func)
        # This let us get a parameter name for every argument
        # even if it's apply positionally. It's the resolution
        # Python does.
        bind = sig.bind(*args, **kwargs)
        # Looping through all arguements name and value
        for name, val in bind.arguments.items():
            # We get the annotation (the type hint)
            ann = sig.parameters[name].annotation
            # If there is a type hint
            if ann is not inspect._empty:
                # Then we apply the type hint to the input
                bind.arguments[name] = ann(val)
        # We apply the original function with the modified
        # arguments
        return func(*bind.args, **bind.kwargs)
    # We return the modified function
    return wrapper

Now, let's try it

# This notation just means
# f = apply_type(f)
@apply_type
def f(x: str, y):
    print(type(x), type(y))

And now we have:

>>> f(3, 4)
<class 'str'> <class 'int'>

Hurray !!

Now some ideas where this could get very useful:

  • If you want to use a file path with the great pathlib but you also want to let a user give a string, this will convert it for you
    @apply_type
    def f(path: pathlib.Path):
        ...
  • Sometimes your algorithm need to work with an int but you want to be able to also take a float (because you know it makes sense). This function will manage that for you
    @apply_type
    def f(x: int):
        ...
    If you work with numpy, your functions can also get some annoying numpy.int64 that make your program bug. This is also a defense against that.

This decorator can be modified freely, for instance you could make a decorator that raise an exception if the given parameter if not of the declared type.

Clone this wiki locally