Type Declarations for Python 2.7

So there's a big flamey thread over on python-list about the Python 3.x type-annotation. Thing is, I can't say the annotations seem all *that* important to the behaviour (static type checking): Consider this Python 2.7 decorator-set:

def _func_annotation_for(func):
    """Retrieve the function annotation for a given function or create it"""
    current = getattr(func,'func_annotation',None)
    if current is None:
        current = func.func_annotation = {}
    return current 
def types( *types, **named ):
    def annotate(function):
        base = _func_annotation_for(function)
        base.update(named)
        if hasattr( function, 'func_code' ): # python 2.x
            names = function.func_code.co_varnames
        else:
            names = function.__code__.co_varnames
        
        for name, typ in zip(names, types):
            base[name] = typ
        return function 
    return annotate
def returns( typ ):
    def annotate(function):
        _func_annotation_for(function)['return'] = typ 
        return function 
    return annotate

That's a few lines of code and would seem to do the bulk of what type annotations do, namely populate a func_annotation dictionary and provide a textual clue as to where the type annotations are for AST or similar tools. It's true that it doesn't look as integrated as the syntax-based annotations, but to my eyes it actually looks cleaner when reading:

@types((int, float, complex), d=int, b=str)
@returns(float)
def x(a, b, c, d=3):
    return float(d)

The extra fluff (type annotations) is separated out and doesn't cloud the parameter list, you can use long type expressions to specify unions-of-types and the like without making it impossible to see your parameters. You can just annotate the functions/parameters you care about... etc. Compare the same example with inline syntactic annotations:

def x(a: (int,float,complex), b: str, c, d=3:int) -> float:
    return float(d)

Read that second example and ask yourself "what are the parameter names"... hmm, that took a bit to parse, now what happens when those type expressions grow long and convoluted? You get the nice locality of reference, but now you have type-itis all through your parameter lists.

Bonus points that you can do the decorator-based annotations in Python 2.7 and they should work fine in 3.x as well, while the inline static stuff is 3.x only AFAIK. The inline static stuff, does allow you to annotate nested parameters and wildcards (named/positional args). I don't know how often those are really going to get type annotations, but I wouldn't consider it a blocker until I saw people really stumbling on it... and then I'd likely tell people to write a couple of more lines of code to solve it.

The real issue that argues for the syntactic embedding is refactoring/maintenance not updating the annotations in line with parameter changes (again, locality is somewhat important there).

Just for reference, PyOpenGL has a decorator that does type-annotations in order to get functions pulled out of DLLs via ctypes, that looks like this:

@_f
@_p.types(None,_cs.GLsync,_cs.GLenum,_cs.GLsizei,arrays.GLsizeiArray,arrays.GLintArray)
def glGetSynciv(sync,pname,bufSize,length,values):pass

Where @_f is the "turn it into a ctypes function" and @_p.types( returntype, argtypes ) is the type-annotation code that just adds the type-annotations to the function's dictionary. That works across Python versions, allows IDEs to provide argument names, is fairly concise (PyOpenGL has a lot of function declarations), etc.

Comments

  1. Steve

    Steve on 01/23/2015 2:47 a.m. #

    I like your proposal here much better!

  2. Eric

    Eric on 01/23/2015 10:16 a.m. #

    Aestethics aside, implementing type annotations provide a compile time entry point for optimizations. If only decorators were used, they the annotations are purely for communication. I'm not saying communicating types is not a good thing, but rather annotating types can, hopefully, be used to optimize code as well as communicate type information.

    It is jarring to see the type annotations in Python, but I think it is a welcome change. I've avoided specific refactorings of code because I couldn't clealry communicate I was using a specific object as an argument in order to encapsulate a group of arguments. A subtle side effect of this is that it is easier to use mutable types like a dict, which in a parallel processing scenario becomes a hazard. Obviously type annotations doesn't solve this, but it does provide a tool that might help communicate, and eventually enforce, some type safety between the boundaries of functions, making it easier to use real objects as arguments when a projects gets more complex.

  3. Mike Fletcher

    Mike Fletcher on 01/29/2015 8:28 a.m. #

    But type annotations, AFAIK being *expressions* require that the code be executed/eval'd to get the result of the expression. At that point, the difference between a decorator and type annotation is essentially nothing. You had to evaluate the identity of "@types" and "@return" to be the things you expect to be able to do type checking and optimizations, but you had to evaluate each of the types anyway to do that optimization/checking, so the difference is pretty minimal IMO.

Comments are closed.

Pingbacks

Pingbacks are closed.