Fernanda Foschiani
Fernanda Foschiani

Reputation: 97

How is the best approach to validate data in python?

I am a newbie in Python, and I´m trying to find the best way to validate data.

I have an object of type "well" that has as attributes other objects. Data can come from a XML file or via code. An example can be seen bellow.


class Well:
    def __init__ (self, name, group):
        self.__name   = name     # Required
        self.__group  = group    # Required
        self.__operate_list = [] # Optional
        self.__monitor_list = [] # Optional
        self.__geometry = None   # Optional
        self.__perf = None       # Optional
        ...

class Operate:

    # *OPERATE (*MAX|*MIN) type value (action)
    # *OPERATE (*PENALTY) type (mode) value (action)
    # *OPERATE (*WCUTBACK) type mode v1 (v2 (v3)) (action)

    def __init__ (self, att:str, type_:str, value: [], mode=None, action=None):
        self.__att = att
        self.__type_ = type_
        self.__mode = mode
        self.__value_list = value
        self.__action = action        

To validate an "operate" for example, i need to check a lot of restrictions and valid values for each attribute. For instance, I have a list of valid "type_" strings and I should assert that type_ is in this list.

1) The best approach to do this is in the constructor? Should I create a method to do this validation? Or should I create a new class only to validade data?

2) Where should I create these lists of valid values? In the constructor? As global variables?

Upvotes: 2

Views: 5817

Answers (4)

smarie
smarie

Reputation: 5156

You can do this with pyfields, which can be seen as an industrialized version of wwii's answer.

You define a field using field(). Fields can be mandatory or optional, and in case the default value is mutable you can specify a default factory (I saw that you use empty lists as default values in your example). Helper function copy_value helps you cover the most common case:

from pyfields import field, copy_value, init_fields
from valid8.validation_lib import is_in

class Well(object):
    name = field()                                        # Required
    group = field()                                       # Required
    operate_list = field(default_factory=copy_value([]))  # Optional
    monitor_list = field(default_factory=copy_value([]))  # Optional
    geometry = field(default=None)                        # Optional
    perf = field(default=None)                            # Optional

Then you can optionally add validation on top of your fields. You can both validate type (with check_type=True) and value (with validators). Validators can rely on existing callables such as is_in as shown below, but generally can leverage any validation callable. Finally the constructor can be generated for you, as shown below:

valid_types = ('type_A', 'type_B')

class Operate(object):
    att = field()                                  # Required
    type_: str = field(check_type=True, validators=is_in(valid_types))   # Required
    value = field(default_factory=copy_value([]))  # Optional
    mode = field(default=None)                     # Optional
    action = field(default=None)                   # Optional

    @init_fields
    def __init__(self):
        pass

o = Operate(att="foo", type_='type_A')  # ok
o.type_ = 1  # <-- raises TypeError: Invalid value type provided

bad_o = Operate(att="foo", type_='type_WRONG')  # <-- raises ValidationError: NotInAllowedValues: x in ('type_A', 'type_B') does not hold for x=type_WRONG

See pyfields documentation for details. I'm the author by the way ;)

Upvotes: 0

wwii
wwii

Reputation: 23743

You could do it with descriptors. The only advantage I can contrive is that it puts the validation in another class - making the class that uses it less verbose. Unfortunately you would have to make one for each attribute with unique validations, Unless you want to include options for membership tests and/or instance-of tests which shouldn't make it too complicated.

from weakref import WeakKeyDictionary

class RestrictedAttribute:
    """A descriptor that restricts values"""
    def __init__(self, restrictions):
        self.restrictions = restrictions
        self.data = WeakKeyDictionary()

    def __get__(self, instance, owner):
        return self.data.get(instance, None)

    def __set__(self, instance, value):
        if value not in self.restrictions:
            raise ValueError(f'{value} is not allowed')
        self.data[instance] = value

When used the descriptor instance must be assigned as a class attribute

class Operate:

    __type_ = RestrictedAttribute(('red','blue'))

    def __init__ (self, att:str, type_:str, value: [], mode=None, action=None):
        self.__att = att
        self.__type_ = type_
        self.__mode = mode
        self.__value_list = value
        self.__action = action        

In use:

In [15]: o = Operate('f',type_='blue',value=[1,2])

In [16]: o._Operate__type_
Out[16]: 'blue'

In [17]: o._Operate__type_ = 'green'
Traceback (most recent call last):

  File "<ipython-input-17-b412cfaa0cb0>", line 1, in <module>
    o._Operate__type_ = 'green'

  File "P:/pyProjects3/tmp1.py", line 28, in __set__
    raise ValueError(msg)

ValueError: green is not allowed

Upvotes: 1

blhsing
blhsing

Reputation: 106445

You can use getter and setter methods via the use of the property function:

class Operate:
    def __init__(self, type):
        self.type = type

    @property
    def type(self):
        return self._type

    @type.setter
    def type(self, value):
        assert value in ('abc', 'xyz')
        self._type = value

so that:

o = Operate(type='123')

would result in:

Traceback (most recent call last):
  File "test.py", line 18, in <module>
    o = Operate(type='123')
  File "test.py", line 8, in __init__
    self.type = type
  File "test.py", line 15, in type
    assert value in ('abc', 'xyz')
AssertionError

Upvotes: 5

user2379875
user2379875

Reputation: 153

assert isinstance(obj) 

Is how you test the type of an object.

if item in container: ...

Is how you would test if an object is in a container.

Whether you do this in the init method or in another method is up to you, it depends which looks cleaner to you, or if you would need to reuse the functionality.

The list of valid values could be passed into the init method or hardcoded into the init method. It can also be a global property of the class.

Upvotes: 1

Related Questions