Combine Multiple Filter Conditions in Python

Janne Kemppainen |

This problem occurred when I was trying to think of an alternative way for returning the first item from a list that matches given constraints. Using if statements inside a for loop is the obvious way to comb through the items but the built-in filter() function provides an interesting alternative.

So let’s say you have a list of products with different properties and you want to get the first product that matches the criteria passed to the function. These can be a minimum or maximum price, name containing a certain string, matching product category, etc. The function implementation could look something like this:

def get_first_match(
    products,
    name_contains=None,
    min_price=None,
    max_price=None,
    product_categories=None,
):
    filters = []
    if name_contains:
        filters.append(lambda p: name_contains in p["name"])
    if min_price:
        filters.append(lambda p: p["price"] >= min_price)
    if max_price:
        filters.append(lambda p: p["price"] <= max_price)
    if product_categories:
        filters.append(lambda p: p["category"] in product_categories)
    try:
        return next(filter(lambda p: all(f(p) for f in filters), products))
    except StopIteration:
        return None

First we define an array to hold the filters and add only those arguments that have been provided to the function. No filters are applied by default.

The built in filter() function loops through an iterable, in this case a list, and returns an iterator. Since we want the first item instead of an iterator we need to call the next() function to get the first result. If there are no matching items, calling the next function would throw a StopIteration exception. Therefore we need to wrap the function call in a try..catch statement.

The actual filter call contains two parameters: the lambda function that is used to evaluate each item and the iterable that we want to go through. The all() function collects the results of the enabled filters and returns true only if all of them evaluate to True.

Note
You can optimize the code by applying the most restrictive filters first. The for loop inside all() is evaluated lazily so if one function return False the remaining functions don’t need to be called.

You might wonder what happens when the filter list is empty. This edge case is automatically taken care of since calling all() on an empty list is always True. Therefore the filtering lambda function is also always True, and the first item will be returned.

Let’s test the function with some dummy data:

products = [
    {"name": "Race car", "category": "Toys", "price": 120},
    {"name": "T-shirt", "category": "Clothing", "price": 20},
    {"name": "Tricycle", "category": "Toys", "price": 240},
    {"name": "LED bulb", "category": "Home", "price": 13},
    {"name": "Jeans", "category": "Clothing", "price": 80},
]

You can see that the function works as expected:

>>> get_first_match(products)
{'name': 'Race car', 'category': 'Toys', 'price': 120}
>>> get_first_match(products, min_price=150)
{'name': 'Tricycle', 'category': 'Toys', 'price': 240}
>>> get_first_match(products, min_price=50, max_price=100)
{'name': 'Jeans', 'category': 'Clothing', 'price': 80}
>>> get_first_match(products, name_contains="cycle")
{'name': 'Tricycle', 'category': 'Toys', 'price': 240}
>>> get_first_match(products, product_categories=["Home", "Clothing"])
{'name': 'T-shirt', 'category': 'Clothing', 'price': 20}
>>> get_first_match(products, min_price=50, product_categories=["Clothing"])
{'name': 'Jeans', 'category': 'Clothing', 'price': 80}
>>> get_first_match(products, min_price=50, product_categories=["Unknown"])
None

If there are no items that match all enabled filters the function returns None.

Note
If you want to loop through multiple results instead of fetching the first match you can omit the next() function call and the try..except block. Use the iterator from filter() in a for loop or cast the return value to a list.

I hope you found this quick tutorial helpful. See you in the next one!

Subscribe to my newsletter

What’s new with PäksTech? Subscribe to receive occasional emails where I will sum up stuff that has happened at the blog and what may be coming next.

powered by TinyLetter | Privacy Policy