Valguard#

Constraint-aware value types for semantic validation in Python pipelines

Valguard is a lightweight framework for defining and validating values in data pipelines. It separates values from constraints. This allows a source to publish the constraint against which it will validate each value. An implies function can determine whether values satisfying an upstream constraint are guaranteed to satisfy a downstream constraint.


Key Features#

  • Declarative value types

  • Validators for clean pipeline integration

  • Type-safe abstractions compatible with Python 3.12+

Usage#

Values#

A Value stores a particular kind of value. It is immutable. Different methods are provided for accessing the value depending on its type: this provides a concise way to validate type and access the value simultaneously. If x is not an integer then x.as_int will raise an exception.

To avoid raising an exception, use isinstance() to test the type beforehand.

A subclass of NumericValue will have the method .to_float.

>>> from valguard import FloatValue, IntValue, NumericValue, BoolValue
>>> fv = FloatValue(1.0)    # Must use 1.0 and not 1
>>> iv = IntValue(1)
>>> bv = BoolValue(True)
>>> isinstance(bv,NumericValue)
False
>>> isinstance(iv,NumericValue)
True
>>> bv.as_float
Traceback (most recent call last):
    ...
valguard.exceptions.TypeMismatchError: Incompatible accessor
>>> iv.as_float
Traceback (most recent call last):
    ...
valguard.exceptions.TypeMismatchError: Incompatible accessor
>>> iv.to_float
1.0
>>> iv.as_int
1
>>> fv.as_float
1.0
>>> fv.to_float
1.0

Constraints#

Constraints can be placed on both type and value.

>>> from valguard import IntValue, BoolValue, IntervalConstraint, NumericConstraint, IntConstraint
>>> iv = IntValue(23)
>>> bv = BoolValue(False)
>>> interval_a = IntervalConstraint(0,100)
>>> interval_b = IntervalConstraint(10,20)
>>> interval_a.validate(iv)
IntValue(23)
>>> interval_b.validate(iv)
Traceback (most recent call last):
    ...
valguard.exceptions.ValidationError: Invalid value: 23.0 lies outside [10.0, 20.0]
>>> interval_a.validate(bv)
Traceback (most recent call last):
    ...
valguard.exceptions.ValidationError: Invalid value: expected a numeric, got BoolValue(False)
>>> IntConstraint().validate(iv)
IntValue(23)
>>> IntConstraint().validate(iv).as_int
23
>>> NumericConstraint().validate(iv).to_float
23.0
>>> NumericConstraint().validate(bv).to_float
Traceback (most recent call last):
    ...
valguard.exceptions.ValidationError: Invalid value: expected a numeric, got BoolValue(False)

The implies function#

The implies function determines whether one constraint implies another constraint. Constraint A implies constraint B if every value that satisfies A will also satisfy B.

>>> from valguard import BoundedIntConstraint, NumericConstraint, FloatConstraint, implies
>>> interval_A = BoundedIntConstraint(0,100)
>>> interval_B = BoundedIntConstraint(20,80)
>>> implies(interval_A, interval_B)
False
>>> implies(interval_B, interval_A)
True
>>> implies(interval_A, FloatConstraint)
False
>>> implies(interval_A, NumericConstraint)
True
>>> implies(NumericConstraint, interval_A)
False

Constraints that are parametrised (such as IntervalConstraint and LiteralStrConstraint) can be used in two ways: as an instance with specific parameters, or as a class. When used as a class, implies behaves differently depending on whether the class appears as the first or second argument to implies. As the first argument, implies(class, B) is True if all instances of class would imply B. As the second argument, implies(A, class) is True if at least one instance of class would be implied by A. For non-parametrised classes, there is no difference whether a class or an instance is used.

Constrained Value Dictionary#

A ConstrainedValueDict behaves like a dictionary but its values are validated against a constraint at the time of insertion.

It is intentional that no type casting is performed, not even from int to IntValue.

>>> from valguard import IntValue, IntConstraint, ConstrainedValueDict
>>> d = ConstrainedValueDict(IntConstraint())                             
>>> d["one"] = IntValue(1)
>>> d["one"]
IntValue(1)
>>> d["two"] = 2
Traceback (most recent call last):
    ...
valguard.exceptions.ValidationError: Invalid value: expected an integer, got 2

Installation#

pip install valguard