Guardrails AIguardrails-ai 0.11.0 · Python 3.10+
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
27 small wins to finish your pathNext lesson

Two validators in one Guard

Lesson 5 built one Guard per validator. A real support desk applies several rules to the same reply, and the obvious way to write that quietly throws one of them away.

The bug

Example
from guardrails import Guard
from guardrails_ai.ends_with import EndsWith
from guardrails_ai.valid_length import ValidLength

desk = Guard().use(EndsWith(end=".", on_fail="noop")).use(ValidLength(min=1, max=20, on_fail="noop"))
outcome = desk.validate("we can refund order 8821 today")

print(sorted(s.validator_name for s in outcome.validation_summaries))

The reply is thirty characters long and does not end in a full stop, so both rules should have complained. Only one did. EndsWith never ran, and nothing said so: no error, no warning, no entry in the logs from lesson 4.

The reason is in Guard.use's own docstring. Calling it more than once with the same target overwrites what was there before. The Guardrails Custom validators page shows exactly this chained form as the way to combine two validators, which is how the mistake gets copied.

The fix

Example
desk = Guard().use(
    EndsWith(end=".", on_fail="noop"),
    ValidLength(min=1, max=20, on_fail="noop"),
)
outcome = desk.validate("we can refund order 8821 today")

print(sorted(s.validator_name for s in outcome.validation_summaries))

One call, both validators, both complaints. The validators= keyword does the same job when the list is built somewhere else.

Example
rules = [EndsWith(end=".", on_fail="noop"), ValidLength(min=1, max=20, on_fail="noop")]
desk = Guard().use(validators=rules)

print(sorted(s.validator_name for s in desk.validate("we can refund order 8821 today").validation_summaries))

The sorting is deliberate. Validators inside one Guard run concurrently, so the order the summaries come back in is not something to depend on.

The overwrite is per target, not per Guard. use takes an on= argument naming what to validate, and two calls with different targets both stick. Lesson 15 attaches a rule to the incoming question and another to the reply on the same Guard, and lesson 18 does it for two fields of one object.

A form that looks right and is not

The Validators concept page shows a Guard built from a validator class plus its arguments, like Guard().use(ToxicLanguage, threshold=0.5, on_fail="exception"). That does not work in 0.11.0.

Example
try:
    Guard().use(ValidLength, min=1, max=20, on_fail="noop")
except TypeError as error:
    print("TypeError:", error)

use takes validator instances, a validators= list and an on= target, and nothing else. Build the validator first, then hand it over.

Try it yourself
  • Chain three .use() calls and confirm that only the last one runs.
  • Give the two validators different on_fail values, noop and fix, and see which one changes the output. Lesson 10 explains what you see.
  • Pass the class positionally, Guard().use(ValidLength, 1, 20), and read the error. It is a different one, and just as unhelpful.

Little by little, you're building something great.