typed

A Python typing library for data validation.


License
Other
Install
pip install typed==0.6

Documentation

Typed - a Python type and data validation library

Tired of writing your own dataserialization routines? Want to quickly check that the inputs have the expected types? Wish you could easily reduce the space consumption of your data dumps?

Then typed might be for you. The typed library helps you describe your datatypes very quickly and clearly, and can be used for data validation, as well as for loading and saving data using custom serialization formats.

Quickstart

Import the library:

import typed

Simple type-checking:

t1 = typed.int | typed.string | typed.none
t2 = typed.set(1,2,3) | typed.float | typed.list(typed.string)
t3 = typed.tuple(typed.int, typed.string) | typed.bool

assert all([
        t1.test(1), t1.test('abc'), t1.test(u'foo'), t1.test(None), not t1.test(False),
        t2.test(2), not t2.test(4), t2.test(4.0), t2.test(['a', 'b', 'c']),
        t3.test(True), t3.test((1, 'bar'))
    ])

Data dumping:

t = typed.dict({
        'name': typed.string,
        'score': typed.float.optional,
        'active': typed.bool.default(True),
        'category': (typed.set('a', 'b', 'c') | typed.none).default(None),
    }).trimmed

d = {
        'name': 'Raptor',
        #'score': 100.0,
        'active': True,
        'category': 'c',
        'url': 'https://en.wikipedia.org/wiki/Velociraptor',
    }

t.save(d)

assert d == {
        'name': 'Raptor',
        'category': 'c',
    }

Translating between different storage formats:

t_load = typed.dict({
        'id': typed.int,
        'last_modified': typed.datetime.format('%Y-%m-%d %H:%M:%S'),
        'visibility': typed.bool.format({True: '1', False: '0'}),
        'properties': typed.json(typed.dict({
                'title': typed.string.default(''),
                'body': typed.string.default(''),
                'tags': typed.list(typed.string).default([]),
            })).format({'{}': ''}),
    }).format(typed.json)

properties_t = typed.dict({
                'title': typed.string.default(''),
                'body': typed.string.default(''),
                'tags': typed.list(typed.string).default([]),
            })

t_save = typed.dict({
        'id': typed.int,
        'last_modified': typed.datetime.format('%Y-%m-%dT%H:%M:%S'),
        'visibility': typed.bool.format({False: 'hidden'}).default(True),
        'properties': properties_t.default(properties_t.load({})),
    })

data_json = r'''{"id": 1, "last_modified": "2012-04-15 16:02:34", "visibility": "0", "properties": ""}'''

data = t_load.load(data_json)
#process data
data = t_save.save(data)

assert data == {'id': 1, 'last_modified': '2012-04-15T16:02:34', 'visibility': 'hidden'}

data_json = r'''{"id": 2, "last_modified": "2013-06-27 03:16:32", "visibility": "1", "properties": "{\"title\": \"First Post\", \"tags\": [\"life\", \"events\"]}"}'''

data = t_load.load(data_json)
#process data
data = t_save.save(data)

assert data == {'id': 2, 'last_modified': '2013-06-27T03:16:32', 'properties': {'title': 'First Post', 'tags': ['life', 'events']}}

Documentation

The typed library currently supports the following types: any, int, float, none, str, unicode, string, bool, date, datetime, list, dict and tuple, as well as type unions and finite sets of values.

All types support the method t.test(obj), which returns True if the value conforms to the type, and False otherwise.

All types also support methods t.load(stored_obj) and t.save(obj), which are used for deserialization and serialization, respectively. The method obj = t.load(stored_obj) transforms the value from the specified format and adds all the necessary fields with default values, returning an object for which t.test(obj) return True. For any object that t.test(obj) return True, the method t.save(obj) will return an object appropriate for serialization in the format specified. If inappropriate object types are passed to methods load or save, a ValueError exception will be raised. Note that load and save might modify mutable objects, specifically list and dict objects, so that the statement d = t.save(d) is equivalent to t.save(d).

Formatting

All types also support the t.format(fmt) method, which specifies the format you want to save the type in. All types support custom encoding of specific values, specified by passing a dict with objects as keys and encodings as values.

t = typed.int.format({1: 'one', 2: 'two'})

assert all([
        t.load('one') == 1, t.save(1) == 'one',
        t.load('two') == 2, t.save(2) == 'two',
        t.load(2) == 2, t.save(3) == 3,
    ])

All types also support the json format.

t1 = typed.json(typed.list(typed.int))
t2 = typed.dict({'a': typed.int}).format(typed.json)

assert all([
        t1.load('[1, 2, 3]') == [1, 2, 3], t1.save([1, 2, 3]) == '[1, 2, 3]',
        t2.load('{"a": 1}') == {'a': 1}, t2.save({'a': 2}) == '{"a": 2}',
    ])

If available, the typed.json formatter uses the ujson library, otherwise it uses Python's own (slower) json serializer. If ujson serializer is used, typed.json supports an additional parameter double_precision that controls the number of float digits used when serializing floats.

t1 = typed.json(typed.list(typed.float))
t2 = typed.json(typed.list(typed.float), double_precision=3)

assert all([
        t1.save([0.0001, 0.0011, 0.0056, 12345.6789]) == '[0.0001,0.0011,0.0056,12345.6789]',
        t2.save([0.0001, 0.0011, 0.0056, 12345.6789]) == '[0.0,0.001,0.006,12345.679]',
    ])

Be careful when chaining formats:

typed.date.format({datetime.date(2000, 1, 1): 'millennium'})
typed.date.format('%Y-%m-%d').format({'2000-01-01': 'millennium'})

typed.dict({'a': typed.int.default(0)}).format(typed.json).format({'{}': ''})

Specific types might support other formats as well.

Type unions

The typed library supports arbitrary type unions, formed using the | operator. A union of types t1, t1, ..., tn is a type which includes all values belonging to any of the types t1, t2, ..., tn.

t = typed.int | typed.string | typed.float

assert all([
        t.test(0),
        t.test('a'),
        t.test(0.1),
    ])

The methods load and save of a type union will try loading or saving the object using all types in the union in the specified order.

t = typed.date.format('%Y-%m-%d') | typed.date.format('%d/%m/%Y')

assert all([
        t.load('2013-05-07') == t.load('07/05/2013'),
        t.save(t.load('07/05/2013')) == '2013-05-07',
    ])

typed.int type

The typed.int is the type of all integer values. In Python 2.x, it is a union of int and long types. In contrast to Python, typed.bool is not a subtype of typed.int and typed.int.test(True) will return False.

typed.none type

The typed.none type has a JSON-inspired alias typed.null == typed.none.

typed.str type

This is the str type of Python 2.x, and has an alias typed.ascii == typed.str.

typed.string type

This is the basestring type of Python 2.x, and is equivalent to typed.string = typed.ascii | typed.unicode.

typed.date type

This is the proper date type, which doesn't test True for instances of datetime. Its format method accepts standard Python date formatting strings.

typed.datetime type

Its format method accepts standard Python datetime formatting strings.

typed.set(values...) type

This type constructor accepts any number of values and produces a type which tests True for any of these values. For example, the bool type could be impleneted as bool = typed.set(True, False).

typed.list(type) type

The type constructor typed.list(t) produces a type of arbitrary length lists, whose elements are all of type t. The load and save methods of this type modify the argument, which might produce unexpeced results in case of failure. This is also the reason you should take care when using typed.list in type unions.

typed.dict({'field': type, ...}) type

This type constructor produces a type of dictionaries that have specified fields with values of specified types. Fields might be designated optional or have defaul values (all fields with default values are also optional). If typed.dict type is trimmed, it will accept dictionaries with additional fields, while load and save methods will remove these fields. The load and save methods of this type modify the argument, which might produce unexpeced results in case of failure. This is also the reason you should take care when using typed.dict in type unions.

t1 = typed.dict({
        'a': typed.int,
        'b': typed.string.optional,
        'c': (typed.string | typed.none).default(None),
        'd': typed.optional,
    })
t2 = t1.trimmed

assert all([
        not t1.test({}), t1.test({'a': 1}), not t1.test({'a': 2, 'e': None}),
        t1.load({'a': 3}) == {'a': 3, 'c': None},
        t1.save({'a': 4, 'b': 'foo', 'c': None, 'd': [1, 2, 3]}) == {'a': 4, 'b': 'foo', 'd': [1, 2, 3]},
        t2.test({'a': 5, 'e': None}),
        t2.load({'a': 6, 'e': 'something'}) == {'a': 6, 'c': None},
    ])

You can use aliases typed.optional == typed.any.optional and typed.default(x) == typed.any.default(x).

typed.tuple(types...) type

This type constructor produces the type of tuples that have elements of the specified types. Its format method accepts typed.list as an argument, in which case the tuple is encoded as a list (useful when loading/saving JSON).

t = typed.tuple(typed.string, typed.int, typed.float).format(typed.list).format(typed.json)

assert t.load('["1", 2, 3.0]') == ('1', 2, 3.0)