Skip to content

valid8 + other tools

Other validation styles

The checktypes package provides a way to easily embed your validation requirements as classes, for easy reuse.

from valid8 import validate
from checktypes import checktype
PositiveInt = checktype('PositiveInt', int, lambda x: x > 0)

x = 1
validate('x', x, custom=PositiveInt.validate)

x = -1
validate('x', x, custom=PositiveInt.validate)  # ValidationError

For functions and classes

PEP484 type checkers

Although simple type validation may be performed using valid8, for function inputs/outputs type validation as well as class field type validation you may wish to rely on a proper PEP484 type checker such as enforce or pytypes.

The following snippet shows a build_house function with two inputs name and surface, where each input is:

  • validated against the expected type thanks to the PEP484 type checking library (enforce in this example)
  • further value-validated with valid8 (len(name) > 0 and surface >= 0), with the help of the mini_lambda syntax.
# Imports - for type validation
from numbers import Integral
from typing import Tuple, Optional
from enforce import runtime_validation, config
config(dict(mode='covariant'))  # type validation will accept subclasses too

# Imports - for value validation
from mini_lambda import s, x, Len
from valid8 import validate_arg, is_multiple_of, InputValidationError

# Define our 2 applicative error types
class InvalidNameError(InputValidationError):
    help_msg = 'name should be a non-empty string'

class InvalidSurfaceError(InputValidationError):
    help_msg = 'Surface should be a multiple of 100 between 0 and 10000.'

# Apply type + value validation
@runtime_validation
@validate_arg('name', Len(s) > 0, error_type=InvalidNameError)
@validate_arg('surface', (x >= 0) & (x < 10000), is_multiple_of(100),
              error_type=InvalidSurfaceError)
def build_house(name: str, surface: Optional[Integral]=None) \
        -> Tuple[str, Optional[Integral]]:
    print('Building house... DONE !')
    return name, surface

We can test that validation works:

> build_house('sweet home', 200)    # valid
> build_house('sweet home')         # valid (PEP484 Optional, default=None)

> build_house('', 100)              # InvalidNameError (valid8)
> build_house('sweet home', 10000)  # InvalidSurfaceError (valid8)
> build_house('test', 100.1)        # RuntimeTypeError (enforce)

Note concerning PEP484 type validation: how can you make sure to accept both plain old float, int and bool, as well as their numpy equivalents ? Use the handy Real (=float) and Integral (=int) abstract numeric types provided in the numbers built-in module ! They provide an easy way to support both python primitives AND others, e.g. numpy primitives.

Unfortunately no equivalent type is provided in the stdlib for booleans, so in valid8 we provide an additional Boolean class supporting numpy, to complete the picture.

For classes

pyfields

TODO

autoclass

valid8 plays well with autoclass to quickly create small but validated classes:

If you decorate the whole class with @validate_field the property setters generated by @autoclass will include validation, as expected:

from autoclass import autoclass
from mini_lambda import s, x, Len
from valid8 import validate_arg, instance_of, is_multiple_of

class InvalidNameError(ClassFieldValidationError):
    help_msg = 'name should be a non-empty string'

class InvalidSurfaceError(ClassFieldValidationError):
    help_msg = 'Surface should be a multiple of 100 between 0 and 10000.'

@validate_field('name', instance_of(str), Len(s) > 0, error_type=InvalidNameError)
@validate_field('surface', (x >= 0) & (x < 10000), is_multiple_of(100), 
                error_type=InvalidSurfaceError)
@autoclass
class House:
    def __init__(self, name, surface=None):
        pass

which yields:

> h = House('sweet home', 200)
> h.surface = None   # Valid (surface is nonable by signature)

> h.name = ''        # InvalidNameError
> h.surface = 10000  # InvalidSurfaceError

Note: if you decorate the class constructor with @validate_arg, the property setters generated by @autoclass will include validation too.

from autoclass import autoclass
from mini_lambda import s, x, Len
from valid8 import validate_arg, instance_of, is_multiple_of, InputValidationError

class InvalidNameError(InputValidationError):
    help_msg = 'name should be a non-empty string'

class InvalidSurfaceError(InputValidationError):
    help_msg = 'Surface should be a multiple of 100 between 0 and 10000.'

@autoclass
class House:

    @validate_arg('name', instance_of(str), Len(s) > 0, 
                  error_type=InvalidNameError)
    @validate_arg('surface', (x >= 0) & (x < 10000), is_multiple_of(100), 
                  error_type=InvalidSurfaceError)
    def __init__(self, name, surface=None):
        pass

which yields:

> h = House('sweet home', 200)
> h.surface = None   # Valid (surface is nonable by signature)
> h.name = ''        # InvalidNameError
> h.surface = 10000  # InvalidSurfaceError

Of course you can also add PEP484 type checking on top of that, see autoclass documentation for details.

attrs

valid8 also integrates well with attrs to quickly create small but validated classes: if you decorate a class with @validate_field it will work because attrs generates a compliant class constructor behind the scenes. However WARNING validation will only be called at initial object creation, not at subsequent field modifications!

import attr
from mini_lambda import s, x, Len
from valid8 import validate_field, instance_of, is_multiple_of

class InvalidNameError(ClassFieldValidationError):
    help_msg = 'name should be a non-empty string'

class InvalidSurfaceError(ClassFieldValidationError):
    help_msg = 'Surface should be a multiple of 100 between 0 and 10000.'

@validate_field('name', instance_of(str), Len(s) > 0, error_type=InvalidNameError)
@validate_field('surface', (x >= 0) & (x < 10000), is_multiple_of(100),
                error_type=InvalidSurfaceError)
@attr.s
class House:
    name = attr.ib()
    surface = attr.ib(default=None)

Which we can test:

> h = House('sweet home')     # Valid (surface is nonable by generated signature)
> h.name = ''                 # DOES NOT RAISE InvalidNameError (no setter!)
> House('', 10000)            # InvalidNameError
> House('sweet home', 10000)  # InvalidSurfaceError

As of today attrs does not transform fields into descriptors or properties so there is no way to add validation to field setters. Note that this is actually also the case if you rely on the validation mechanisms built in attrs, as explained here. This feature has been requested here.