- In the verbal description of the algorithm, "should be less than" implies "not equal", which would not be correct. A wording such as "must be at most" would be totally clear.
 - Your code has a bug because you are not quite following rule 1. The rule says "at any point...", but you only check the rule after a smiley. You miss the case when a lone `)` breaks the rule.
 - Instead of canceling `+= 1` with `-= 1` here...

        left += 1
        if i > 0 and source[i-1] == ':': # :(
            left -= 1
            frown += 1
    ... it would be clearer to use `else`:

        if i > 0 and source[i-1] == ':': # :(
            frown += 1
        else:
            left += 1

 - Instead of `if i > 0 and source[i-1] == ':'` it would be simpler to remember the previous character in a variable:

         previous_char = None 
         for char in source:
         if char == 'c':
             if previous_char == ':':
         ...
         previous_char = char