These posts will be in three parts.
- Function for password validation
- Refactoring the function for password validation
- Unit test the function for password validation
This is the second part of the Custom Password Validation in Python series. In this post, we will be looking at, Refactoring the function for password validation.
There are parts of the code that needs a little "touch" here and there. Here are a few things I think we can modify or improve.
This snippet below was the final implementation we had.
Custom Password Validation in Python (Function for password validation)
Michael Otu ・ Feb 13 '22
from string import (
punctuation, whitespace, digits,
ascii_lowercase, ascii_uppercase)
def is_valid_password(password):
new_password = password.strip()
MIN_SIZE = 6
MAX_SIZE = 20
password_size = len(new_password)
if password_size < MIN_SIZE or password_size > MAX_SIZE:
return False
valid_chars = {'-', '_', '.', '!', '@', '#', '$', '^', '&', '(', ')'}
invalid_chars = set(punctuation + whitespace) - valid_chars
for char in invalid_chars:
if char in new_password:
return False
password_has_digit = False
for char in password:
if char in digits:
password_has_digit = True
break
if not password_has_digit:
return False
password_has_lowercase = False
for char in password:
if char in ascii_lowercase:
password_has_lowercase = True
break
if not password_has_lowercase:
return False
password_has_uppercase = False
for char in password:
if char in ascii_uppercase:
password_has_uppercase = True
break
if not password_has_uppercase:
return False
return True
Refactoring
-
The password parameter, what is its type? When I hovered on it in
vscode
, it saidAny
. We expect astring
password and notAny
. What do we do then?- Adding type annotation solves the parameter type issue (it contributes to the documentation).
- I thought of passing a default value to the parameter (when no argument is passed to the function).
def is_valid_password(password: str = "") -> bool: ...
-
What happens when no argument is passed? The part of the code that checks for the
MIN_SIZE
andMAX_SIZE
will take care of not passing an argument by returningFalse
. We can returnFalse
when an argument is not passed.
def is_valid_password(password: str = "") -> bool: if not password: return False ...
-
Consider the snippet below.
password_has_something = False for char in password: if char in somethings: password_has_something = True break if not password_has_something: return False
Snippets like this have appeared several times. We can create a function for this snippet.
The issue is that there is another similar snippet.
for char in invalid_chars: if char in new_password: return False
If we can change this snippet to look and feel like the others, then the same function would work for this snippet too.
password_has_invalid_chars = False for char in new_password: if char in invalid_chars: password_has_invalid_chars = True break if password_has_invalid_chars: return False
-
In python, we can create a function inside a function. This function becomes local to the function it is within. The problem would be that we can not test the local function directly. For the sake of testing, we put all functions outside the
is_valid_password
function.
def contains_character(password: str = "", sack: str = "") -> bool: has_char = False for char in password: if char in sack: has_char = True break return has_char
-
Let's update those parts of the
is_valid_password
function.Now snippets like this:
password_has_something = False for char in password: if char in somethings: password_has_something = True break if not password_has_something: return False
Will become:
if not contains_character(password, somethings): return False
This is will different though for the invalid characters,
invalid_chars
. When there is an invalid character, returnFalse
. So when the function returnsTrue
, returnFalse
. -
It seems we can abstract
password_size < MIN_SIZE or password_size > MAX_SIZE
.password_size < MIN_SIZE or password_siz > MAX_SIZE
makes use ofMIN_SIZE
andMAX_SIZE
. Do we pass them as arguments? No. I think we shouldn't. We should rather make them local to the (new) function.Let's create this function,
is_valid_size
.
def is_valid_size(password: str = "") -> bool: MIN_SIZE = 6 MAX_SIZE = 20 password_size = len(password) return password_size < MIN_SIZE or password_size > MAX_SIZE
This will return
True
ifpassword_size < MIN_SIZE
and also whenpassword_size > MAX_SIZE
. The value we expect from this isFalse
. That is our true success. That is when the password is in the desired range. We should write functions that returnTrue
on success andFalse
on failure. So our new function will be better if we returnpassword_size >= MIN_SIZE and password_size <= MAX_SIZE
. It is the same asMIN_SIZE <= password_size <= MAX_SIZE
. The new function becomes:
def is_valid_size(password: str = "") -> bool: MIN_SIZE = 6 MAX_SIZE = 20 password_size = len(password) return MIN_SIZE <= password_size <= MAX_SIZE
-
We can also let a function call return the invalid characters. The invalid characters are
string
but we have aset
. I madevalid_chars
aset
so that I don't have to cast it to a set before using it.
valid_chars = {'-', '_', '.', '!', '@', '#', '$', '^', '&', '(', ')'} invalid_chars = set(punctuation + whitespace) - valid_chars
We'd convert the above snippet in:
def get_invalid_chars(): valid_chars = {'-', '_', '.', '!', '@', '#', '$', '^', '&', '(', ')'} invalid_chars = set(punctuation + whitespace) - valid_chars return "".join(invalid_chars)
We would then update the function with the changes made.
What if the user or the data received is not a
string
? What if it is alist
orset
or even anint
? The best way is to use thetry and except
clause. We can returnFalse
on allExceptions
.
The Final Code
This is what we have laboured towards.
from string import (
ascii_lowercase, ascii_uppercase,
digits, punctuation, whitespace)
def contains_character(password: str = "", sack: str = "") -> bool:
has_char = False
for char in password:
if char in sack:
has_char = True
break
return has_char
def is_valid_size(password: str = "") -> bool:
MIN_SIZE = 6
MAX_SIZE = 20
password_size = len(password)
return MIN_SIZE <= password_size <= MAX_SIZE
def get_invalid_chars():
valid_chars = {'-', '_', '.', '!', '@', '#', '$', '^', '&', '(', ')'}
invalid_chars = set(punctuation + whitespace) - valid_chars
return "".join(invalid_chars)
def is_valid_password(password: str = "") -> bool:
try:
if not password:
return False
new_password = password.strip()
if not is_valid_size(new_password):
return False
invalid_chars = get_invalid_chars()
if contains_character(new_password, invalid_chars):
return False
if not contains_character(new_password, digits):
return False
if not contains_character(new_password, ascii_lowercase):
return False
if not contains_character(new_password, ascii_uppercase):
return False
return True
except:
return False
Conclusion
We can still make changes when we are writing tests. We should have written the test first but, "you know". The next post will be on, Unit test the function for password validation. There is more refactoring to do.
Top comments (0)