Welcome, guest | Sign In | My Account | Store | Cart

The two decorators checkparams and checkreturn allow you to check (at execution time) that function parameters or a function return value are the right type.

Python, 75 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
""" 
File checkparams.py
Version 2.0
Written by ll.snark 

Type Checking in Python using decorators
Use the decorator checkparams to check (only unnamed) parameters type
Use the decorator checkreturn to check a return value type :

@checkreturn(int)
@checkparams(int,int)
def ajoute(a,b) :
    if a<0 : return "No negative numbers"
    return a+b

Then Try : 
ajoute(5,6)
ajoute(5,"Argh")
ajoute(-4,6)

Version 2.0 
(
with help from ActiveState members... :
    -better type checking
    -removed unused lines
    -used functools.wraps to copy __name__ and __doc__
)

Note that the preceding decorated function checks if the parameters are
type int or a subtype of int (this is a different behaviour from version 1, 
but I think it's better)
Consequently, you can check only the second parameter, for instance, writing :
@checkparams(object,int)  
def ajoute(a,c) :
    ...
Indeed, type of parameter a will always be a subclass of object.
"""
import functools
def checkparams(*params) :
    def decorator(fonc) :
        @functools.wraps(fonc)
        def foncm(*pnn,**pn) :
            for i,par in enumerate(zip(pnn,params)) :
                if not isinstance(par[0],par[1]) :
                    raise TypeError(foncm.__name__+" Param "+str(i)+" ("+str(par[0])+") is not "+str(par[1]))
            r=fonc(*pnn,**pn)
            return r
        return foncm
    return decorator

def checkreturn(tor) :
    def decorator(fonc) :
        @functools.wraps(fonc)
        def foncm(*pnn,**pn) :
            r=fonc(*pnn,**pn)
            if not isinstance(r,tor) :
                print(type(r))
                raise TypeError(foncm.__name__+" : Return value ("+str(r)+") is not "+str(tor))                
            return r
        return foncm
    return decorator

if __name__=='__main__' : 

    @checkreturn(int)
    @checkparams(int,int)
    def ajoute(a,b) :
        """ Just a sample function to test decorators. """
        if a<0 : return "No negative numbers"
        return a+b


    print(ajoute(5,6))
    print(ajoute(0,"foo"))
    print(ajoute(-3,4))

Version 1

==========

I don't know if this kind of type checking decorators are well known by all of you. I wrote this to better understand decorators. Note that if type checking fails, a TypeError Exception is raised, giving you the name of the function you wrote, not the name of the decorated function. If you think I do this the wrong way (copying __name__ and __doc__), please, tell me.

I also wonder if this : type(5)==int is the right way to test parameters type...

Version 2

==========

I updated this recipe taking Matthew remarks into account.

5 comments

Matthew Wood 9 years ago  # | flag

I've been messing around with a few things like this, and I have a few things that may help.

I'd check to see if it's an instance of that class instead of comparing the type directly.

if not isinstance(par[0], par[1]):

Also, checkout the functools.wraps method. It does the __name__ and __doc__ replacement for you.

And I guess lastly, I have a personal pet pieve against enumerating zip results as I can't unpack them cleanly. So I convert:

for i, par in enumerate(zip(pnn, params)):
    #mess with par[0] and par[1]

into

for i, arg, target_type in intertools.izip(itertools.count(), pnn, params):
    #mess with arg and target_type

But again, that's just a pet pieve of mine. You could also just unpack them yourself:

for i, par in enumerate(zip(pnn, params)):
    arg, target_type = par
    #mess with arg and target_type

Anyway, this is great! And like I said, I've been messing around with this for quite some time trying to validate user inputs in LOTS of places. This methodology is pretty clean, and I'll likely use something very much like it.

James Mills 9 years ago  # | flag

This is the best kind of optional type-checking I've ever seen for Python! Great work! Way better than Python's recent functional annotation style.

Simeon Shpiz 9 years ago  # | flag

Great stuff. I'm not sure but i think it looks cleaner than what I did when I made something similar a few months ago.

I wondered- why do you prefer to use regular and not keyword args (or allowing both) when defining the types for a func? i.e. Why @checkparams(int,int) and not @checkparams(a= int, b = int) ?

About Matthew's pet peeve - I sympathize; what I usually do is something like this

for i, (arg, target_type) in enumerate(zip(pnn, params)):
    #do something

Which is not as bad as par[0], par[1] i guess.

Martin Miller 8 years, 11 months ago  # | flag

I think it would be useful -- and easy -- to have checkparams() also check the number of parameters passed.

It might also be good to make checkreturn() be able to check the types of a tuple of return values since that's a fairly common Python idiom.

Scott S-Allen 8 years, 11 months ago  # | flag

I started writing a response regarding a few details. One was to pass key-values allowing for specific attributes to be type-checked while ignoring others i.e. 3rd param must be an integer:

@checkit(dict(int_=2))
def checkme(self, one, two, three):
    ....

Another was to draw your attention to some of the Python introspection tools, particularly inspect, which has the ability to return the calling arguments, and much more.

call_args = inspect.getcallargs(funk)

This would allow the prev. example to become more specific to an attribute name, allowing the decorator to handle indexing the arguments, reducing potential errors. Plus, I was going to suggest multi-type definitions. So combined the usage could look like this:

@checkit(dict(two=(int), three=(int, float, long)))
def checkme(self, one, two, three):
    ....

You may also have noted the name-space resolution (no 'int_') in this example too.

But life happened before I could finish some proof-of-concept and hit send... two days later I find a full package on pypi that relates to this recipe.

Please have a look at "typesafe" at pypi.python.org. It actually implements the above suggestions in two forms: strict and lenient.

There is another package called "typecheck" but I've not looked into it.