Usage
To use, import where from predicate.
from predicate import where
where takes any number of keywork arguments as well as one reserved keyword argument that
is combinator= which defaults to "and".
Each keyword argument (except combinator=), generates a predicate function and they are combined
into a single predicate function based on the value of combinator=.
To level set, predicate functions are functions that take a single argument and return True or
False. Their type signature looks like Callable[[Any], bool]. These are useful for separating
a collection of objects or filtering. One of the main uses of predicates is in the builtin filter
function as well as many of the grouping slicing tools in the standard lib itertools.
Inspiration
This project is heavily inspired by django queryset filters. These make it very easy to build complex SQL filters when working with the Django ORM. This project is an attempt to make the developer friendliness of the ORM filtering available to native python collections.
Basic Usage
The most simple usage of where is for simple equality filtering of an attribute.
>>> from predicate import where
>>> is_18 = where(age=18)
>>> is_18({'age': 18})
True
>>> list(filter(is_18, [{'name': 'Joe', 'age': 18}, {'name': 'Bob', 'age': 19}]))
[{'name': 'Joe', 'age': 18}]
The equivalent code could be expressed with a lambda:
is_18 = lambda x: x.get('age', None) == 18
however in this case it may or may not be more readable.
However, one of the things the lambda cant handle that where can is mixed dictionary and
objects.
>>> class Person:
... def __init__(self, name, age):
... self.name = name
... self.age = age
...
... def __repr__(self):
... return f"{self.name} ({self.age})"
>>> mixed_data = [
... {'name': 'Joe', 'age': 18},
... {'name': 'Bob', 'age': 19},
... Person('Jane', 18)
... ]
>>> list(filter(where(age=18), mixed_data))
[{'name': 'Joe', 'age': 18}, Jane (18)]
Compound Predicates
Going further into capabilities, we can pass multiple keyword arguments to where to produce a compound predicate.
>>> from predicate import where
>>> is_18yo_male = where(age=18, gender='M')
>>> is_18yo_male({'age': 18, 'gender': 'F'})
False
Again this logic can be reproduced using a lambda:
is_18yo_male = lambda x: x.get('age', None) == 18 and x.get('gender', None) == 'F'
You can chain as many of them as you need, making the readablity increase with the more complex predicates.
Complex Combinators
By Default, where uses combinator='and', which combines all of the predicates using and. This means
by default, that all of the predicates need to be met for where to also return True. However, there are
other combinators available:
comininator='and'(default) - ReturnsTrueonly ifallof the predicates areTruecombinator='or'- ReturnsTrueifanyof the predicates areTruecombinator='none'- ReturnsTrueonly ifnoneof the predicates areTrue(all areFalse)combinator='nand'- Only ReturnsFalseif all predicates areTrueotherwise returnsTrue. Useful for filtering out specific cases.
>>> from predicate import where
>>> is_18_or_is_female = where(age=18, gender='F', combinator='or')
>>> is_18_or_is_female({'age': 18, 'gender': 'F'})
True
>>> is_18_or_is_female({'age': 17, 'gender': 'M'})
False
>>> is_not_18_or_female = where(age=18, gender='F', combinator='none')
>>> is_not_18_or_female({'age': 18, 'gender': 'M'})
False
>>> is_not_18_or_female({'age': 18, 'gender': 'F'})
False
>>> is_not_18_or_female({'age': 17, 'gender': 'M'})
True
>>> is_not_18_and_female = where(age=18, gender='F', combinator='nand')
>>> is_not_18_and_female({'age': 17, 'gender': 'M'})
True
>>> is_not_18_and_female({'age': 18, 'gender': 'M'})
True
>>> is_not_18_and_female({'age': 18, 'gender': 'F'})
False
Nested Data
Getting beyond the simple use case, where follows django queryset filters in digging into nested
data structures by allowing access to nested fields via __.
>>> from predicate import where
>>> is_smith = where(name__last='Smith')
>>> is_smith({'name': {'first': 'John', 'last': 'Smith'}})
True
This nested operation digs through both objects and dictionaries. Missing keys will be returned as None.
NonEquality Comparisons
Another idea borrowed from django querysets is to do filtering beyond simple equality. These non-equality comparators,
like in django querysets, are a suffix with __. The avaliable comparators are:
__eq(default) - Equals__gtGreater than__gteGreater than or equal to__ltLess than__lteLess than or equal to__neNot equal to__containsChecks for membership usingin. Can be uses on anything thatincan be used with__startswithDoes the field start with the given string__endswithDoes the field end with the given string__exactSame as equals but here for compatibility with django filters__iexactCase insensitive string comparison__inDoes the field exist in a given collection.__typeCheck if the value at a given position is a given type
>>> from predicate import where
>>> is_adult = where(age__gte=18)
>>> is_adult({'age': 17})
False
>>> is_adult({'age': 18})
True
>>> age_is_int = where(age__type=int)
>>> age_is_int({'age': 18.5})
False
Custom Comparisons
If the given set of comparators is not enough, there is a __func, comparator that takes a predicate function that will
be applied at the given key.
>>> from predicate import where
>>> last_name_has_5_characters = where(name__last__func=lambda name: len(name) == 5)
>>> last_name_has_5_characters({'name': {'last': 'Smith'}})
True
>>> last_name_has_5_characters({'name': {'last': 'Bob'}})
False
Custom Combinators
Like with __func, if the given combinators (and, or, none) dont work for you, you can pass your own combinator function in
to the combinator= argument. This function should take have the signature Callable[[Iterable[bool]], bool].
>>> from predicate import where
>>> only_one_true = lambda xs: sum(xs) == 1
>>> is_eligible = where(age=18, name__first__startswith='K', gender='F', combinator=only_one_true)
>>> is_eligible({'age': 18, 'name': {'first': 'John', 'last': 'Smith'}, 'gender': 'M'})
True
>>> is_eligible({'age': 18, 'name': {'first': 'John', 'last': 'Smith'}, 'gender': 'F'})
False