5

I want to validate since the instance creation if the type is right or wrong, i tried using @dataclass decorator but doesn't allow me to use the __init__ method, i also tried using a custom like class type

also in order of the type made some validations (if is a int, that field>0 or if is a str clean whitespaces, for example), i could use a dict to validate the type, but i want to know if there's a way to do it in pythonic way

class Car(object):
    """ My class with many fields """

    color: str
    name: str
    wheels: int

    def __init__(self):
        """ Get the type of fields and validate """
        pass
isherwood
  • 58,414
  • 16
  • 114
  • 157
Carlos Rojas
  • 334
  • 5
  • 13
  • Related: [Validating complex types in dataclasses](https://stackoverflow.com/questions/50563546/validating-detailed-types-in-python-dataclasses). If you run into problems because your types aren't something straight forward like `str`, `int`, or `float`, the answer in that post might help. – Arne Nov 07 '18 at 09:25

4 Answers4

8

You can use the __post_init__ method of dataclasses to do your validations.

Below I just confirm that everything is an instance of the indicated type

from dataclasses import dataclass, fields

def validate(instance):
    for field in fields(instance):
        attr = getattr(instance, field.name)
        if not isinstance(attr, field.type):
            msg = "Field {0.name} is of type {1}, should be {0.type}".format(field, type(attr))
            raise ValueError(msg)

@dataclass
class Car:
    color:  str
    name:   str
    wheels: int    
    def __post_init__(self):
        validate(self)
Patrick Haugh
  • 59,226
  • 13
  • 88
  • 96
1

An alternative to @dataclass is to use pyfields. It provides validation and conversion out of the box, and is directly done at the field level so you can use fields inside any class, without modifying them in any way.

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

ALLOWED_COLORS = ('blue', 'yellow', 'brown')

class Car(object):
    """ My class with many fields """
    color: str = field(check_type=True, validators=is_in(ALLOWED_COLORS))
    name: str = field(check_type=True, validators={'should be non-empty': lambda s: len(s) > 0})
    wheels: int = field(check_type=True, validators={'should be positive': lambda x: x > 0})

    @init_fields
    def __init__(self, msg="hello world!"):
        print(msg)

c = Car(color='blue', name='roadie', wheels=3)

c.wheels = 'hello'   # <-- (1) type validation error, see below
c.wheels = 0         # <-- (2) value validation error, see below

yields the following two errors

TypeError: Invalid value type provided for '<...>.Car.wheels'. 
   Value should be of type <class 'int'>. Instead, received a 'str': 'hello'

and

valid8.entry_points.ValidationError[ValueError]: 
   Error validating [<...>.Car.wheels=0]. 
   InvalidValue: should be positive. 
   Function [<lambda>] returned [False] for value 0.

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

smarie
  • 4,568
  • 24
  • 39
0

Dataclasses do not check the data. But I made a small superstructure for dataclasses, and you can use it this way:

import json
from dataclasses import dataclass

from validated_dc import ValidatedDC


@dataclass
class Car(ValidatedDC):
    color: str
    name: str
    wheels: int


# This string was received by api
data = '{"color": "gray",  "name": "Delorean", "wheels": 4}'

# Let's upload this json-string to the dictionary
data = json.loads(data)

car = Car(**data)
assert car.get_errors() is None

# Let's say the key "color" got the wrong value:
data['color'] = 11111

car = Car(**data)
assert car.get_errors()
print(car.get_errors())
# {
#     'color': [
#         BasicValidationError(
#             value_repr='11111', value_type=<class 'int'>,
#             annotation=<class 'str'>, exception=None
#         )
#     ]
# }

# fix
car.color = 'gray'
# is_valid() - Starting validation of an already created instance
# (if True returns, then there are no errors)
assert car.is_valid()
assert car.get_errors() is None

ValidatedDC: https://github.com/EvgeniyBurdin/validated_dc

Evgeniy_Burdin
  • 627
  • 5
  • 14
0

Use pydantic. In this example, the field password1 is only validated for being a string, while other fields have custom validator functions.

from pydantic import BaseModel, ValidationError, validator


class UserModel(BaseModel):
    name: str
    username: str
    password1: str
    password2: str

    @validator('name')
    def name_must_contain_space(cls, v):
        if ' ' not in v:
            raise ValueError('must contain a space')
        return v.title()

    @validator('password2')
    def passwords_match(cls, v, values, **kwargs):
        if 'password1' in values and v != values['password1']:
            raise ValueError('passwords do not match')
        return v

    @validator('username')
    def username_alphanumeric(cls, v):
        assert v.isalnum(), 'must be alphanumeric'
        return v


user = UserModel(
    name='samuel colvin',
    username='scolvin',
    password1='zxcvbn',
    password2='zxcvbn',
)
print(user)
#> name='Samuel Colvin' username='scolvin' password1='zxcvbn' password2='zxcvbn'

try:
    UserModel(
        name='samuel',
        username='scolvin',
        password1='zxcvbn',
        password2='zxcvbn2',
    )
except ValidationError as e:
    print(e)
    """
    2 validation errors for UserModel
    name
      must contain a space (type=value_error)
    password2
      passwords do not match (type=value_error)
    """
Michał Zawadzki
  • 695
  • 6
  • 14