all and any

    How can you check whether all items match a condition in Python? And how can you check whether any item matches a condition? You might think to do something like this:

    # Hinweis
    all_good = True
    for item in iterable:
        if not condition(item):
            all_good = False
            break
    

    Or something like this:

    # Hinweis
    any_bad = False
    for item in iterable:
        if condition(item):
            any_bad = True
            break
    

    But there are two built-in Python functions that can help make those code blocks more descriptive and more readable. Let's talk about using Python's any and all functions to check whether all iterable items match a condition.

    Checking a condition for all items

    This function accepts a list (or iterable) of numbers and checks whether all the numbers are between 0 and 5 (inclusive):

    def ratings_valid(ratings):# check, if all numbers in [0,5]
        for rating in ratings:
            if rating < 0 or rating > 5:
                return False
        return True
        
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    This code:

    • Loops over all given numbers (using a for loop)
    • Returns False as soon as a negative number or a number greater than 5 is found (using an if statement to check that condition)
    • Returns True if all numbers are between 0 and 5

    Note that this function returns as soon as it finds an invalid number, so it only iterates all the way through the number range if all the numbers are valid.

    Using the any and all functions in Python

    Let's first look at a refactoring of our loop that checks a condition for each item and then discuss what we did, why we did it, and how we did it. We started with this:

    def ratings_valid(ratings):
        for rating in ratings:
            if rating < 0 or rating > 5:
                return False
        return True
    
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    We're going to end up either this:

    def ratings_valid(ratings):
        return not any(
            rating < 0 or rating > 5
            for rating in ratings
        )
        
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    Or we'll end up with this (which is even more readable):

    def ratings_valid(ratings):
        return all(
            0 <= rating <= 5
            for rating in ratings
        )
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    Okay, let's break this code down to see how it works. First we need to talk about the any and all functions. Then we'll talk about why any and all tend to be used with generator expressions, how to choose between them. Finally, we'll wrap up with a cheat sheet.

    Python's any function

    Python has a built-in any function that returns True if any items are truthy

    print(any([True, False, False, True, False]))
    print()
    print(any([False, False, False, False, False]))
    print()
    print(any(['', '', '']))
    print()
    print(any(['', 'Python', '']))

    Truthy typically means non-empty or non-zero, but for our purposes you can think of it pretty much the same as True. Python's any is equivalent to this:

    # Hinweis
    def any(iterable):
        for element in iterable:
            if element:
                return True
        return False

    Notice the similarity between any and our ratings_valid function: their structure is very similar, but not quite the same. The any function checks for the truthiness of each item in a given iterable, but we need something a little more than that: we need to check a condition on each element. Specifically, we need to check whether a number is between 0 and 5.

    Python's all function

    Python also has a built-in all function that returns True if all items are truthy.

    print(all([True, False, True, True, False]))
    print(any([True, False, True, True, False]))
    print()
    print(all([True, True, True, True, True]))
    print(any([True, True, True, True, True]))
    print()
    print(all(['Python', 'is', 'not', 'Perl']))
    print(any(['Python', 'is', 'not', 'Perl']))
    print()
    print(all(['', 'Python', 'is', 'cool']))
    print(any(['', 'Python', 'is', 'cool']))

    Python's all is equivalent to this:

    # Hinweis
    def all(iterable):
        for element in iterable:
            if not element:
                return False
        return True

    The any and all functions are two sides of the same coin:

    • any returns True as soon as it finds a match
    • all returns False as soon as it finds a non-match

    Let's try using a list comprehension instead. The any and all functions accept an iterable and check the truthiness of each item in that iterable. These functions are typically used with iterables of boolean values. We could re-implement our ratings_valid function by building up a list of boolean values and then passing those values to any:

    def ratings_valid(ratings):
        rating_is_invalid = []
        for rating in ratings:
            rating_is_invalid.append(rating < 0 or rating > 5)
        return not any(rating_is_invalid)
    
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    Or we could copy-paste that for loop into a list comprehension:

    def ratings_valid(ratings):
        return not any([
            rating < 0 or rating > 5
            for rating in ratings
        ])
    
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    That code is shorter, but there's a problem: we're building up a new list just to loop over it once! This is less efficient than our original approach! Our original approach only looped all the way through the list if all ratings were valid (see the return within the for loop).

    def ratings_valid(ratings):
        for rating in ratings:
            if rating < 0 or rating > 5:
                return False
        return True
    
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    Let's fix this inefficiency by turning our list comprehension into a generator expression.

    Using a generator expression

    A generator expression is like a list comprehension, but instead of making a list it makes a generator object.

    def ratings_valid(ratings):
        return not any((
            rating < 0 or rating > 5
            for rating in ratings
        ))
    
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    We can actually even drop the double parentheses and still have valid Python code:

    def ratings_valid(ratings):
        return not any(
            rating < 0 or rating > 5
            for rating in ratings
        )
    
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    It's so common to pass a generator expression directly into a function call that the Python core developers added this feature just for this use case. Generators are lazy iterables: they don't compute the items they contain until you loop over them. Instead they compute their values item-by-item. This allows us to stop computing as soon as the any function finds a truthy value!

    There's one more thing we can do to improve the readability of this code: we could switch to using Python's all function.

    Choosing between any and all

    We started with a for loop, an if statement, a return statement within our loop, and a return statement at the end of our loop:

    def ratings_valid(ratings):
        for rating in ratings:
            if rating < 0 or rating > 5:
                return False
        return True
        
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    We turned all that into a generator expression passed to the any function:

    def ratings_valid(ratings):
        return not any(
            rating < 0 or rating > 5
            for rating in ratings
        )
        
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    Note that we negated the final result (see the not in return not any...) In English, this reads "there is not any rating outside the target range". Using the all function we're able to lose that negation:

    def ratings_valid(ratings):
        return all(
            not (rating < 0 or rating > 5)
            for rating in ratings
        )
        
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    Or if we rely on Python's chained comparisons, we could write:

    def ratings_valid(ratings):
        return all(
            0 <= rating <= 5
            for rating in ratings
        )
    print(ratings_valid([0,1,4])) 
    print(ratings_valid([1,4,6,7,8,9]))    

    In English, this reads "all the ratings are within the target range". These two statements are equivalent in Python:

    # Hinweis
    all_match = all(condition(x) for x in iterable)
    all_match = not any(not condition(x) for x in iterable)

    And these two statements are equivalent as well:

    # Hinweis
    any_match = any(condition(x) for x in iterable)
    any_match = not all(not condition(x) for x in iterable)

    Typically one of your any or all expressions will read more clearly than the other one. In our case, it's the all expression that reads more clearly.

    Using any and all in an if statement

    I've mostly been showing any and all used in a return statement, but that's actually not its most readable use. If you don't find the behavior of these functions to be intuitive, you might find them easier to understand when used in an if statement. For example let's say we have an is_valid_rating function like this:

    # Hinweis
    def is_valid_rating(rating):
        return 0 <= rating <= 5

    We could use that function to write an if statement like this:

    def is_valid_rating(rating):
        return 0 <= rating <= 5
    
    ratings = [2, 1, 3, 4, 7, 11]
    if all(is_valid_rating(r) for r in ratings):
        print("All the ratings are valid")
    else:
        print("Not all ratings are valid")

    In English I would describe that code as checking whether "all ratings are valid" and then printing based on the result. Compare that code to this:

    def is_valid_rating(rating):
        return 0 <= rating <= 5
        
    ratings = [2, 1, 3, 4, 7, 11]
    all_valid = True
    for r in ratings:
        if not is_valid_rating(r):
            all_valid = False
            break
    
    if all_valid:
        print("All the ratings are valid")
    else:
        print("Not all ratings are valid")

    In English this reads as "loop over all ratings and set all_valid to False if an invalid rating is found or set it to True otherwise" and then print based on the value of all_valid. This for loop doesn't translate into English nearly as easily as that if statement with all do! I very much prefer this first approach of using a generator expression with all because I find it more descriptive than the equivalent for loop.

    Python's any and all functions are for checking a condition for every item in a list (or any other iterable). While any and all are handy, sometimes there's an even simpler condition checking tool available. For example this loop:

    numbers = [2, 1, 3, 4, 7, 11]
    has_five = False
    for n in numbers:
         if n == 5:
             has_five = True
             break
    print(has_five)

    Could be written like this:

    numbers = [2, 1, 3, 4, 7, 11]
    print(has_five = any(n == 5 for n in numbers))

    But this could be simplified even further. We're trying to check whether a given number is contained in the numbers list. The in operator is made exactly for this purpose:

    numbers = [2, 1, 3, 4, 7, 11]
    print(has_five = 5 in numbers)

    Python's in operator is for containment checks. Anytime you need to ask whether a particular item is contained in a given iterable, the in operator is the tool to reach for. Additionally, when you find yourself searching for a matching item in an iterable, in general you may want to question whether you should use a dictionary instead.

    Cheat sheet: Python's any and all

    Here's a quick cheat sheet for you. This code uses a break statement because we're not returning from a function: using break stops our loop early just as return does.

    You can convert this code:

    # Hinweis
    all_good = True
    for item in iterable:
        if not condition(item):
            all_good = False
            break

    To this code:

    # Hinweis
    all_good = all(
        condition(item)
        for item in iterable
    )

    And you can convert this code:

    # Hinweis
    any_good = False
    for item in iterable:
        if condition(item):
            any_good = True
            break

    To this code:

    # Hinweis
    any_good = any(
        condition(item)
        for item in iterable
    )

    Check whether all items match a condition with the any and all functions Python's any and all functions were made for use with generator expressions (discussed here & here) and they're rarely used without them. The next time you need to check whether all items match a condition, try Python's any or all functions along with a generator expression.

    For more of my recommendations on how to use Python's built-in functions, see Built-in functions in Python.


    Nächste Kurseinheit: 02 Fallunterscheidungen