I'm writing a validator class to validate certain request objects against a known format. Rule declarations and the validator will both be written entirely in Python, and I don't need to store the structures in the DB or go across the wire in JSON or anything of that type. I'm considering using the builtin function any
as a value to indicate that any value is permissible for a particular key.
In other words, I'm tempted to do this:
validation_rules = {
"foo": ['a', 'b'],
"bar": any
}
# Meanwhile in the validator...
for key, value in request_object.entries():
if validation_rules[key] is any:
# validation succeeds because this key allows any value
elif value not in validation_rules[key]:
# reject for invalid value
else:
# validation succeeds
Pros: it's actually really easy to read, and the semantics are reasonably clear both in the declared validation rules and in the validator. It's much more comprehensible than using None
, which was my other idea.
Cons: It's surprising, to say the least, to see a builtin function being used in this manner, where the actual usage of the function is irrelevant and it's only being used because the name is descriptive.
If I do this, will I go to hell? Will my coworkers be mad at me? Will I get made fun of on Hacker News?
3 Answers 3
Instead of using a sentinel value like any
or None
with special-cased logic, you could create a type the implements the desired "everything goes" behavior using the same interface as the other validation rules. Namely, checking container membership.
Python has an abstract notion of container objects. Containers are any objects that define the __contains__
special method. Such objects can be used with the membership operators in
/not in
just like lists or other builtin containers.
For this use case, we can define a container that treats all objects as being members of it:
class _Everything:
def __contains__(self, item: Any) -> bool:
return True
everything = _Everything()
>>> 3 in everything
True
>>> "foo" in everything
True
>>> object in everything
True
>>> everything in everything
True
If we swap any
in your code with an instance of Everything
, then your validation logic can be simplified to a single membership test:
validation_rules = {
"foo": ['a', 'b'],
"bar": everything,
}
# Meanwhile in the validator...
for key, value in request_object.entries():
if value not in validation_rules[key]:
# reject for invalid value
else:
# validation succeeds
As an extra bonus, this also makes it possible to give validation_rules
a meaningful type hint for the purposes of static type checking. Specifically, we can mark validation_rules
as being a dict with values of type Container[str]
:
from collections.abc import Container
validation_rules: dict[str, Container[str]] = {
"foo": ['a', 'b'],
"bar": everything,
}
-
3Just a small nitpicking, "anything" would be better than "everything" semantically (but I don't know Python well, maybe it's a reserved keyword?)Kaddath– Kaddath11/10/2023 11:08:57Commented Nov 10, 2023 at 11:08
-
3@Kaddath I disagree,
everything
here is behaving as a set to which all things belong, whereas I'd expect ananything
object to behave as any one thing.Pete Kirkham– Pete Kirkham11/10/2023 12:44:57Commented Nov 10, 2023 at 12:44 -
2The type should be
dict[str, Container[str]]
Barmar– Barmar11/10/2023 15:39:31Commented Nov 10, 2023 at 15:39 -
This is a good answer for the stated requirements. If there are other, potentially more complex rules, I would probably take it another step and use function references for the validation rules.JimmyJames– JimmyJames11/10/2023 16:47:52Commented Nov 10, 2023 at 16:47
-
1@JimmyJames I think this answer actually generalises really nicely using the concept of proper classes ( en.wikipedia.org/wiki/Class_(set_theory) ) - the class here is defined by the predicate that returns true regardless of input and can easily be generalised into
class ProperClass(): def __init__(self, predicate): self.predicate = predicate def __contains__(self, item): return self.predicate(item) everything = ProperClass(lambda x: True) from numbers import Number numbers = ProperClass(lambda x: isinstance(x, Number))
Cong Chen– Cong Chen11/12/2023 14:47:40Commented Nov 12, 2023 at 14:47
I made the types Anything
and Something
for this purpose. Anything
compares True
with any other value, while Something
compares True
on anything that is not None
:
>>> Anything == 42
True
>>> {'x': 10, 'y': -3} == {'x': 10, 'y': Anything}
True
>>> Anything == None
True
>>> Something == None
False
>>> Something == 1
True
They're available as the package anything
on PyPi.
-
Neat concept! This answer would be more useful if it explained how the OP's business rules can be written with
Anything
andSomething
.David Pement– David Pement11/12/2023 01:55:05Commented Nov 12, 2023 at 1:55
Here's another solution similar to Brian61354270's answer that's a little more generic. You might want to use this if you have to support other rules that are more complicated. As an example, I added regex validator:
import re
def in_list(*items):
return lambda x: x in items
def any_value(value):
return True
def regex(pattern: str):
pattern = re.compile(pattern)
return lambda x: pattern.fullmatch(x) is not None
validation_rules = {
"foo": in_list('a', 'b'),
"bar": any_value,
"sna": regex("a.z")
}
request_object = {
"foo": 'c',
"bar": 'something',
"sna": 'buz',
}
# Meanwhile in the validator...
for key, value in request_object.items():
if not validation_rules[key](value):
print(key, value, "no!")
else:
print(key, value, "yes!")
-
Stylistically I prefer this approach to the one in my answer. I takes fewer brain cycles to understand, and generalizes very nicely to more complex requirements.Brian61354270– Brian6135427011/10/2023 17:48:12Commented Nov 10, 2023 at 17:48
-
2Nitpicks: shadowing the builtin
all
could be confusing/bug-prone. Maybeall
could be named something likealways_valid
? You could also save some unnecessary indirection by definingin_list
asdef in_list(*items): return items.__contains__
:)Brian61354270– Brian6135427011/10/2023 17:48:49Commented Nov 10, 2023 at 17:48 -
@Brian61354270 Thanks for the catch. Yeah, shadowing built-ins is a recipe for frustration. And using the dunder methods is definitely valid. I was erring on the side of clarity because functions that return functions can be confusing enough.JimmyJames– JimmyJames11/10/2023 17:55:57Commented Nov 10, 2023 at 17:55
Any
orobject
type instead (rather than the builtinany()
function). Or a custom marker object (e.g.novalidate = object ()
, thenif rule is novalidate: succeed
). You might also consider using an existing validation framework like Pydantic.