attrs-validators
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