Basic Typechecking

Getting Your Feet Wet

Before we can even start thinking about typechecking, we'll need to import the right stuff from the right modules. For the moment, we only need worry about the main typecheck module and a few of the functions in it. Let's go ahead and import the accepts function:

from typecheck import accepts

accepts is a decorator for functions and methods. It is used to specify what arguments may be passed to a function or method (including instance-, static- and classmethods). In the following code snippet, we'll use accepts to control the arguments coming into a function:

@accepts(Number, Number)
def add(a, b):
	return a + b

The two arguments we've provided to @accepts indicate that we want both the a and b parameters to be Numbers (Number is a typeclass). If we were to try calling add on some strings, or with the wrong number of arguments, the accepts decorator would pick up on this and raise a TypeCheckError exception.

Keyword Parameters

The accepts decorator allows you to provide types for keyword parameters as well. This is done in much the same manner as for positional parameters:

@accepts(Number, Number, Number)
def my_add(a, b, c=5):
	return a + b + c

In the case of keyword parameters, they will only be typechecked if a value is actually specified for them. For example, the following call to my_add will not cause the c argument to be checked:

my_add(4, 5)

Similarly, accepts will not check the default values given for keyword parameters. The following function declaration, then, is legal, even though None is clearly not a number:

@accepts(Number, Number, Number)
def my_add_2(a, b, c=None):
	return a + b, c

Slurpy Parameters

@accepts provides functionality to provide typing information for the slurpy parameters, *vargs and **kwargs. This may be done in one of several equivalent ways:

@accepts(String, [Number], {str: Number})
def my_func(a, *vargs, **kwargs):
    pass

@accepts(String, Number, Number)
def my_func(a, *vargs, **kwargs):
    pass
@accepts(String, vargs=[Number], kwargs={str: Number})
def my_func(a, *vargs, **kwargs):
    pass

@accepts(String, vargs=Number, kwargs=Number)
def my_func(a, *vargs, **kwargs):
    pass

All of these examples provide equivalent signatures, saying that a is a String, all other positional arguments must be Numbers, and all extra keyword arguments must be Numbers. In fact, in both example pairs, the second signature "desugars" to the first.

Putting It All Together

You can combine positional and keyword parameters in both your function definition:

@accepts(Number, String, String, String, Number)
def my_func(a, b='', c=None, *vargs, **kwargs):
    pass

my_func(5, c='y', d=5, e=6)

Checking in on Return Values

In addition to making assertions about what parameters a function takes, typecheck also provides a way of examining a function's return value. It's called, appropriately, returns:

from typecheck import returns

returns works in much the same way as accepts, and in fact, most of the things you'll be taught about one are equally applicable to the other. Let's run through some quick examples of using returns.

@returns(Number)
def add(a, b):
	return a + b
@returns(Number, Number)
def identity(a, b):
	return a, b

Note in that second example, we provided two type to returns. This allows us to example the contents of the tuple that return a, b creates.

You can even use returns in combination with accepts, like so:

@accepts(Number, Number)
@returns(Number)
def my_add(a, b):
	return a + b

The order of accepts and returns does not matter; it would have been equally correct to write the signature with returns first.

A Little Bit of Class

Even though we use the Number and String typeclasses in examples, it's equally trivial to use user-defined classes and types in typechecking signatures. Observe:

class Foo(object):
	...
		
@accepts(Foo, Number)
def set_baz_attr(foo_inst, new_val):
	foo_inst.baz = new_val

We can use this example to illustrate another point about typechecking:

class Bar(Foo):
	...
	
>>> my_bar = Bar()
>>> set_baz_attr(my_bar, 5)

Since Bar is a subclass of Foo, accepts allows it to pass through.

When Things Go Boom

So, now that we've covered how to make sure things are going right, what happens when they go wrong? Let's investigate what happens if a given parameter is of the wrong type:

>>> @accepts(int, int)
... def int_add(a, b):
... 	return a + b
...
>>> int_add(4, 5.0)
Traceback (most recent call last):
[snip]
typecheck.TypeCheckError: Argument b: for 5.0, \
	expected <type 'int'>, got <type 'float'>

typecheck will tell you exactly which parameter was of the wrong type, and why.

Next...

In our next lesson, we'll learn how to use Python's built-in types -- lists, tuples and dicts -- to build complex, powerful typing expressions.

Valid XHTML 1.0 Transitional