attrs-validators

validationPythonattrsdata classestype checkingattribute validation

Validation Library

attrs-validators

Overview

attrs-validators is a validation functionality integrated with Python's attrs library. It allows embedding validation logic directly in class definitions, helping create type-safe and robust Python code. It provides runtime value validation for attrs data classes using built-in validators and custom validators.

Details

attrs-validators is a powerful validation feature provided as part of the attrs library. attrs is an advanced library for creating Python data classes, offering more functionality than the standard dataclasses. The validation functionality is available through the attr.validators module, supporting various validation patterns including type checking, value range validation, and custom validation logic.

Key features include declarative validation definitions, integration with converters, chaining of multiple validators, and cross-attribute validation. Validation can be executed not only at initialization but also when setting attributes, ensuring object integrity at all times.

Pros and Cons

Pros

  • Declarative syntax: Validation rules can be clearly defined alongside class definitions
  • Rich built-in validators: Common validators like instance_of, in_, ge/gt/le/lt, deep_iterable are built-in
  • Easy custom validator creation: Intuitive custom validator definition using decorators
  • Converter integration: Validation can be executed after value conversion, enabling flexible input processing
  • Cross-attribute validation: Complex validation logic referencing multiple attribute values can be implemented
  • Performance: Fast execution with C extensions and validation disable options

Cons

  • Dependency on attrs library: Requires using the entire attrs library, which may be difficult to introduce to existing projects
  • Learning curve: Need to get familiar with attrs' unique syntax
  • Incompatibility with standard dataclasses: Different approach from Python's standard dataclasses makes migration difficult
  • Heavy decorator usage: Complex validations may require many decorators, potentially making code less readable

References

Usage Examples

Basic Usage

import attr
from attr import validators

@attr.s
class User:
    # Type validation
    name = attr.ib(validator=validators.instance_of(str))
    
    # Range validation
    age = attr.ib(validator=[
        validators.instance_of(int),
        validators.ge(0),    # Greater than or equal to 0
        validators.le(150)   # Less than or equal to 150
    ])
    
    # Choice validation
    status = attr.ib(validator=validators.in_(["active", "inactive"]))

# Usage
user = User(name="John", age=25, status="active")
# user = User(name="John", age=-1, status="active")  # ValueError

Custom Validators

from attrs import define, field

@define
class Product:
    name: str = field()
    price: float = field()
    quantity: int = field()
    
    @price.validator
    def validate_price(self, attribute, value):
        if value <= 0:
            raise ValueError("Price must be greater than 0")
        if value > 1000000:
            raise ValueError("Price must be less than 1,000,000")
    
    @quantity.validator
    def validate_quantity(self, attribute, value):
        if value < 0:
            raise ValueError("Quantity cannot be negative")

# Usage
product = Product(name="Laptop", price=899.99, quantity=10)

Combining Converters and Validators

import attr
from attr import validators

def to_upper(value):
    """Convert string to uppercase"""
    return value.upper() if isinstance(value, str) else value

@attr.s
class Code:
    # Convert then validate
    country_code = attr.ib(
        converter=to_upper,
        validator=[
            validators.instance_of(str),
            validators.in_(["JP", "US", "UK", "CN"])
        ]
    )
    
    postal_code = attr.ib(
        converter=str,  # Convert numbers to string
        validator=validators.matches_re(r"^\d{5}(-\d{4})?$")
    )

# Usage
code = Code(country_code="us", postal_code="12345")  # "us" is converted to "US"
print(code.country_code)  # "US"

Cross-Attribute Validation

from attrs import define, field

def end_after_start(instance, attribute, value):
    """Validate that end date is after start date"""
    if instance.start_date and value <= instance.start_date:
        raise ValueError("End date must be after start date")

@define
class DateRange:
    start_date: str = field()
    end_date: str = field(validator=end_after_start)
    
    @start_date.validator
    def validate_start_date(self, attribute, value):
        # Date format validation
        try:
            from datetime import datetime
            datetime.strptime(value, "%Y-%m-%d")
        except ValueError:
            raise ValueError("Date must be in YYYY-MM-DD format")

# Usage
date_range = DateRange(start_date="2024-01-01", end_date="2024-12-31")

Compound Validation with Logical Operators

from attrs import define, field, validators

@define
class FlexibleInput:
    # Accept either string or integer
    value = field(validator=validators.or_(
        validators.instance_of(str),
        validators.instance_of(int)
    ))
    
    # Optional email address
    email = field(
        default=None,
        validator=validators.optional(
            validators.matches_re(r"^[\w\.-]+@[\w\.-]+\.\w+$")
        )
    )

# Usage
input1 = FlexibleInput(value="text")
input2 = FlexibleInput(value=123, email="[email protected]")
input3 = FlexibleInput(value=123)  # email is None (optional)

Dynamic Validation Control

import attr
from attr import validators

# Temporarily disable validation
with validators.disabled():
    @attr.s
    class TempData:
        value = attr.ib(validator=validators.instance_of(int))
    
    # Validation is disabled, so string doesn't cause error
    temp = TempData(value="not an int")

# Outside context, validation is enabled
# temp2 = TempData(value="not an int")  # This would raise an error