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 astringpassword 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_SIZEandMAX_SIZEwill take care of not passing an argument by returningFalse. We can returnFalsewhen 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 FalseSnippets 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 FalseIf 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_passwordfunction.
 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_passwordfunction.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 FalseWill become: 
 if not contains_character(password, somethings): return FalseThis 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_SIZEmakes use ofMIN_SIZEandMAX_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_SIZEThis will return Trueifpassword_size < MIN_SIZEand 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 returnTrueon success andFalseon 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 stringbut we have aset. I madevalid_charsasetso that I don't have to cast it to a set before using it.
 valid_chars = {'-', '_', '.', '!', '@', '#', '$', '^', '&', '(', ')'} invalid_chars = set(punctuation + whitespace) - valid_charsWe'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 a- listor- setor even an- int? The best way is to use the- try and exceptclause. We can return- Falseon all- Exceptions.
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)