Typedecorator
A decorator-based implementation of type checks for Python.
Provides @params
, @returns
and @void
decorators for describing
the type of the function arguments and return values. If the types mismatch,
an exception can be thrown, the mismatch can be logged, or it can be ignored.
Example:
@returns({str: int})
@params(data={str: int}, key=str, val=int)
def add_to_data(data, key, val):
data[key] = val
return data
Works on Python 2.5+ and Python 3.2+.
Quickstart
Install the package using pip:
pip install typedecorator
You can now start using it in your code
# import the decorators and the typecheck setup function
from typedecorator import params, returns, setup_typecheck
# decorate your functions
@returns(int)
@params(a=int, b=int)
def add(a, b):
return a + b
# set up the type checking
setup_typecheck()
add(1, 2) # works fine
add('one', 2) # will raise a TypeError
You ony need to call setup_typecheck
once to enable it and optionally
configure exceptions thrown and logging level. You can use it multiple
time to change, disable, or re-enable typechecks at runtime. See the
Setup section for more information.
Type Signatures
Both @params
and @returns
take type signatures that can describe both
simple and complex types. The @void
decorator is a shorthand for
@returns(type(None))
, describing a function returning nothing.
A type signature can be:
-
A type, such as
int
,str
,bool
,object
,dict
,list
, or a custom class, requiring that the value be of specified type or a subclass of the specified type. Since every type is a subclass ofobject
,object
matches any type. -
A list containing a single element, requiring that the value be a list of values, all matching the type signature of that element. For example, a type signature specifying a list of integers would be
[int]
. -
A tuple containing one or more elements, requiring that the value be a tuple whose elements match the type signatures of each element of the tuple. For eample, type signature
(int, str, bool)
matches tuples whose first element is an integer, second a string and third a boolean value. -
A dictionary containing a single element, requiring that the value be a dictionary with keys of type matching the dict key, and values of type matching the dict value. For example,
{str:object}
describes a dictionary with string keys, and anything as values. -
A set containing a single element, requiring that the value be a set with elements of type matching the set element. For example,
{str}
matches any set consisting solely of strings. -
xrange
(orrange
in Python 3), matching any iterable (including generators and lists). -
An instance of
typedecorator.Union
, requiring that the value be of any of the types listed when creating theUnion
instance. For example,Union(int, str, type(None))
matches integers, strings andNone
. -
An instance of
typedecorator.Nullable
, requiring that the value is either of the type specified when creating theNullable
instance, or None. For example,Nullable(str)
matches strings andNone
. -
A string containing the type name. This is useful in places where the type is not yet available when the decorator is wrapping the function, for example in method definitions (where class is not yet available)
These rules are recursive, so it is possible to construct arbitrarily complex type signatures. Here are a few examples:
-
{str: (int, [MyClass])}
- dictionary with string keys, where values are tuples with first element being an integer, and a second being a list of instances of MyClass -
{str: types.FunctionType}
- dictionary mapping strings to functions -
{xrange}
- set of iterators -
{str: Union(int, float)}
- dictionary mapping strings to values that are either integers or floating point numbers -
MyObject
- any type named MyObject (more precisely, any type whose__name__
attribute is equal toMyObject
)
Note that [object]
is the same as list
, {object:object}
is the same
as dict
and {object}
is the same as set
.
Setup
The function setup_typecheck
takes care of enabling, disabling, and
configuring type checks at "compile" (parse) time and at runtime.
The function takes three optional arguments:
enabled
- whether to enable checks of any kind (default:True
)exception
- which exception to raise if type check fails (default:TypeError
), orNone
to disable raising the exceptionloglevel
- the log level at which to log the type error (see the standardlogging
module for possible levels), orNone
to disable type error logging.
By default, the type checking system is inactive unless activated through
this function. However, the type-checking wrappers are in place, so the
type checking can be enabled or disabled at runtime (multiple times).
These wrappers do incur a very small but real performance cost. If you
want to disable the checks at "compile" time, call this function with
enabled=False
before defining any functions or methods using the
typecheck decorator.
For example, if you have a config.py
file with USE_TYPECHECK
constant
specifying whether you want the type checks enabled:
#!/usr/bin/env python
from typecheck import setup_typecheck, params
import config
setup_typecheck(enabled=config.USE_TYPECHECK)
@params(a=int, b=int):
def add(a, b):
return a + b
Note that in this case, the checks cannot be enabled at runtime.
Type checking methods
When using @params
with instance methods, you should specify object
as
the type of the self
argument. This is required because when the decorator
is executing, the class itself still isn't created so the type that self
will have doesn't exist yet.
There is no problem in using a catch-all object
type though, since Python
will enforce correct type for the instance upon method invocation anyways.
When using @params
on a class method, there is no problem since the class
itself is instance of the type
type.
But there's still a problem if a method takes additional parameters that must be instances of the same class, or if it returns an instance of the class. In this case, and in any other case where the type might not be available at the time of decorator running, a string specifying the type name can be used instead. Using strings should be avoided in other cases, because they're more prone to type errors (the existence of the type is never checked, the check only does a name match).
When combining type checking decorators with @classmethod
, @staticmethod
or @property
decorators, the type checking decorators must run first (ie.
be "closer" to the body of the function being defined).
Here's an example:
class Accumulator(object):
sum = 0
@void
@params(self=object, a=int)
def add(self, a):
self.sum = self.sum + a
@classmethod
@returns(int)
@params(cls=type, a=int, b=int)
def add2(cls, a, b):
return a + b
@property
@returns(int)
def total(self):
return self.sum
@returns('Accumulator')
@params(self=object, other='Accumulator')
def __add__(self, other):
result = Accumulator()
result.sum = self.sum + other.sum
return result
Mocking
Since the type signatures compare the actual value types, the parameters
can't be mocked. To minimize the problem, Mock
type from mock
library
(the most used mocking library in Python, and part of Python 3 standard
library) is special cased - an instance of Mock
(or any of its subclasses,
such as MagicMock
) will pass any check.
Python 3 annotations
If used with Python 3, the parameters and return type signatures can also
be specified using the Python 3 function annotation syntax. To enable these,
use the @typed
decorator on an annotated function. For example:
@typed
def add_to_data(data: {str: int}, key: str, val: int) -> {str: int}:
data[key] = val
return data
The behaviour is identical as if @params
and @returns
were used, the only
difference is in nicer syntax.
License
Copyright (C) 2014. Senko Rasic [email protected]
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.