When seeing this question I felt like answering, but before I got around to it there were some good answers commenting upon style and efficiency already. Therefore I decided to do a performance review instead.
I made a simple function, password_tester
, to run through some STRONG_PASSWORDS
and WEAK_PASSWORDS
, and timed it using %timeit password_tester(test_function)
within IPython. I'll comment each of them whilst presenting the result, but first the specific test code:
import re
STRONG_PASSWORDS = [
"ABCabc12",
"abc123AB",
"123ABCab",
"Aa1bB2Cc",
"abcABC123",
"A TRUELY STRONG password in between the 6 so-called strong passwords!!!!",
]
WEAK_PASSWORDS = [
"aaaa",
"!()!=!()",
"aaaaaaaa",
"AAAAAAAA",
"11121312",
"ABCDabcd",
"abcd1234",
"1234ABCD",
]
def get_non_strong_passwords(passwords, test_function):
"""Return all passwords which are not strong."""
return [password for password in passwords
if not test_function(password)]
def get_non_weak_passwords(passwords, test_function):
"""Return all passwords which are not strong."""
return [password for password in passwords
if test_function(password)]
def password_tester(test_function, with_output=False):
if with_output:
print('Testing {}: '.format(test_function.__name__), end='')
non_strong_passwords = get_non_strong_passwords(STRONG_PASSWORDS, test_function)
non_weak_passwords = get_non_weak_passwords(WEAK_PASSWORDS, test_function)
if not with_output:
return False
if not non_strong_passwords and not non_weak_passwords:
print('All OK');
return True
if non_strong_passwords:
print(' Failed on strong passwords: {}'.format(', '.join(non_strong_passwords)))
if non_weak_passwords:
print(' Failed on weak passwords: {}'.format(', '.join(non_strong_passwords)))
return False
To be able to verify the correctness of various alternatives the test functions has optional printing which was turned of when doing the timing test runs. The code contains two utility functions which returns those passwords not being strong or weak when they were supposed to be the opposite. These might be kind of counter-intuitive, but they served the purpose well when testing for correctness.
Test results by version
All the different password tester are presented in the code block at the bottom, but here is the various result, with a comment:
is_strong_org
– 40.2 µs
Original non-regex version by original poster
is_strong_org_v2
– 27.8 µs, 3rd fastest
Removed intermediate variables from original version by original poster, this removed 12 µs... This version is similar to the version presented by SuperBiasedMan
is_strong_regex
– 103 µs
Original regex version by original poster
is_strong_regex_v2
– 33.3 µs
OP's regex version, but moved the re.compile(...)
out of the loop. This reduced the running time to a third!!
is_strong_regex_v3
– 28.7 µs per loop
Continued with removing the anti-pattern of True if True else False
. This would be similar to the implemented version of alexwlchan's version.
is_strong_regex_v4
– 21.3 µs, 2nd fastest!
And finally removed intermediate variables, and we're at a fifth of the original time used for OP's regex version
is_strong_holroy
– 46.8 µs
This is my version, where I'm using one regex with named capture groups, and can check for existing of each group. Not the fastest, alas.
ALL_CASES = re.compile(r'^(?:(?P<number>\d)|(?P<lower>[a-z])|(?P<upper>[A-Z])|(?P<other>[^a-zA-Z0-9])){8,}$')
def is_strong_holroy(password):
"""Version by holroy, with named capturing groups."""
m = ALL_CASES.search(password)
return m is not None and m.group('number') and m.group('upper') and m.group('lower')
is_strong_hjpotter92
– 15.7 µs, Fastest!!
The version by hjpotter92, with the modification of allowing longer than 8, and other characters rather than only digit, lower- and uppercase characters. That is the following regex:
re.compile(r'^(?=.*[a-z])(?=.*\d)(?=.*[A-Z])(?:.{8,})$')
is_strong_caridorc
– 53.2 µs
A cleaner more pythonic version by Caridorc, where I believe the focus was on clarity and simplicity, not speed.
Performance conclusion
Using the precompiled regex by hjpotter92 using positive lookahead is the fastest version, and most compact version as well. But the OP's version with a little trimming is not lacking that much when it comes to speed (for my testcases).
And then it is up to the reader, which version they think is easiest on the eye, and is most understandable, readable or whatever metric they choose to use!
Here is the complete script used for testing:
from __future__ import print_function
import re
def is_strong_org(password):
"""OP non-regexp version."""
length = len(password) >= 8
case = password != password.upper() and password != password.lower()
digit = any(c.isdigit() for c in password)
return(length and case and digit == True)
def is_strong_org_v2(password):
"""OP non-regex version, with immediate return."""
return (len(password) >= 8
and password != password.upper()
and password != password.lower()
and any(c.isdigit() for c in password))
def is_strong_regex(password):
"""OP's original version with regex."""
length_regex = re.compile(r'.{8,}')
length = True if length_regex.search(password) != None else False
uppercase_regex = re.compile(r'[A-Z]')
lowercase_regex = re.compile(r'[a-z]')
uppercase = True if uppercase_regex.search(password) != None else False
lowercase = True if lowercase_regex.search(password) != None else False
case = True if uppercase and lowercase == True else False
digit_regex = re.compile(r'[0-9]')
digit = True if digit_regex.search(password) != None else False
return(length and case and digit == True)
LENGTH = re.compile(r'.{8,}')
UPPERCASE = re.compile(r'[A-Z]')
LOWERCASE = re.compile(r'[a-z]')
DIGIT = re.compile(r'[0-9]')
### Extracted regex
def is_strong_regex_v2(password):
"""OP regex version, with extracted regex compilation."""
length = True if LENGTH.search(password) != None else False
uppercase = True if UPPERCASE.search(password) != None else False
lowercase = True if LOWERCASE.search(password) != None else False
case = True if uppercase and lowercase == True else False
digit = True if DIGIT.search(password) != None else False
return(length and case and digit == True)
def is_strong_regex_v3(password):
"""OP regex version, with extracted regex, and simpler boolean statements."""
length = LENGTH.search(password) is not None
uppercase = UPPERCASE.search(password) is not None
lowercase = LOWERCASE.search(password) is not None
digit = DIGIT.search(password) is not None
return length and uppercase and lowercase and digit
def is_strong_regex_v4(password):
"""OP regex version, with extracted regex, and immediate return."""
return (LENGTH.search(password)
and UPPERCASE.search(password)
and LOWERCASE.search(password)
and DIGIT.search(password))
HJPOTTER92 = re.compile(r'^(?=.*[a-z])(?=.*\d)(?=.*[A-Z])(?:.{8,})$')
def is_strong_hjpotter92(password):
"""hjpotter92 version from comment."""
return HJPOTTER92.search(password)
from string import ascii_lowercase, ascii_uppercase, digits
def is_strong_caridorc(password):
"""Version by caridorc with 'import string'."""
return len(password) >= 8 and \
any(upper in password for upper in ascii_uppercase) and \
any(lower in password for lower in ascii_lowercase) and \
any(digit in password for digit in digits)
ALL_CASES = re.compile(r'^(?:(?P<number>\d)|(?P<lower>[a-z])|(?P<upper>[A-Z])|(?P<other>[^a-zA-Z0-9])){8,}$')
def is_strong_holroy(password):
"""Version by holroy, with named capturing groups."""
m = ALL_CASES.search(password)
return m is not None and m.group('number') and m.group('upper') and m.group('lower')
STRONG_PASSWORDS = [
"ABCabc12",
"abc123AB",
"123ABCab",
"Aa1bB2Cc",
"abcABC123",
"A TRUELY STRONG password in between the 6 so-called strong passwords!!!!",
]
WEAK_PASSWORDS = [
"aaaa",
"!()!=!()",
"aaaaaaaa",
"AAAAAAAA",
"11121312",
"ABCDabcd",
"abcd1234",
"1234ABCD",
]
def get_non_strong_passwords(passwords, test_function):
"""Return all passwords which are not strong."""
return [password for password in passwords
if not test_function(password)]
def get_non_weak_passwords(passwords, test_function):
"""Return all passwords which are not strong."""
return [password for password in passwords
if test_function(password)]
def password_tester(test_function, with_output=False):
if with_output:
print('Testing {}: '.format(test_function.__name__), end='')
non_strong_passwords = get_non_strong_passwords(STRONG_PASSWORDS, test_function)
non_weak_passwords = get_non_weak_passwords(WEAK_PASSWORDS, test_function)
if not with_output:
return False
if not non_strong_passwords and not non_weak_passwords:
print('All OK');
return True
if non_strong_passwords:
print(' Failed on strong passwords: {}'.format(', '.join(non_strong_passwords)))
if non_weak_passwords:
print(' Failed on weak passwords: {}'.format(', '.join(non_strong_passwords)))
return False
def main():
# Code used to verify correctness
for test_function in (is_strong_regex,
is_strong_regex_v2,
is_strong_regex_v3,
is_strong_regex_v4,
is_strong_org,
is_strong_org_v2,
is_strong_holroy,
is_strong_hjpotter92,
is_strong_caridorc,
):
password_tester(test_function, True)
if __name__ == '__main__':
main()
''' Code run within the IPython shell to get actual timings:
In [300]: for test_function in (is_strong_org,
...: is_strong_org_v2,
...: is_strong_regex,
...: is_strong_regex_v2,
...: is_strong_regex_v3,
...: is_strong_regex_v4,
...: is_strong_holroy,
...: is_strong_hjpotter92,
...: is_strong_caridorc,
...: ):
...: print("\nTesting {}:".format(test_function.__name__))
...: %timeit password_tester(test_function)
...:
'''
^(?=.*[a-z])(?=.*\d)(?=.*[A-Z])([a-zA-Z\d]{8})$
\$\endgroup\$