13

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?

1
  • 11
    Consider using the Any or object type instead (rather than the builtin any() function). Or a custom marker object (e.g. novalidate = object (), then if rule is novalidate: succeed). You might also consider using an existing validation framework like Pydantic.
    – amon
    Nov 9, 2023 at 22:38

3 Answers 3

25

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,
}
6
  • 3
    Just a small nitpicking, "anything" would be better than "everything" semantically (but I don't know Python well, maybe it's a reserved keyword?)
    – Kaddath
    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 an anything object to behave as any one thing. Nov 10, 2023 at 12:44
  • 2
    The type should be dict[str, Container[str]]
    – Barmar
    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
    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
    Nov 12, 2023 at 14:47
6

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.

1
  • Neat concept! This answer would be more useful if it explained how the OP's business rules can be written with Anything and Something. Nov 12, 2023 at 1:55
5

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!")
3
  • 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. Nov 10, 2023 at 17:48
  • 2
    Nitpicks: shadowing the builtin all could be confusing/bug-prone. Maybe all could be named something like always_valid? You could also save some unnecessary indirection by defining in_list as def in_list(*items): return items.__contains__ :) 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
    Nov 10, 2023 at 17:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.