Checking password strength while honoring functional requirements
Your flags shouldn't be in the types definition.
types should be a constant, move it out of the function.
It'd be simpler if you used set.__isub__ and set.__and__, and looped through the types array.

It leaves all the unwanted values in the password too. Allowing for possible improvements on point 3.
You can join the sum and the base for loop together, removing the need for a types list.




TYPES = (
    frozenset(string.whitespace),
    frozenset(string.ascii_lowercase),
    frozenset(string.ascii_uppercase),
    frozenset(string.digits),
    frozenset(string.punctuation)
)

def estimate_quality(password):
    if not password:
        return 0

    password_set = set(password)
    space = 0
    for group in TYPES:
        if password_set &amp; group:
            space += len(group)
        password_set -= group

    if password_set:
        return -1

    return math.ceil(math.log2(space) * len(password))