Custom Password Validation in Python (Refactoring the function for password validation)

Michael Otu - Feb 14 '22 - - Dev Community

These posts will be in three parts.

  1. Function for password validation
  2. Refactoring the function for password validation
  3. 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.


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
Enter fullscreen mode Exit fullscreen mode

Refactoring

  • The password parameter, what is its type? When I hovered on it in vscode, it said Any. We expect a string password and not Any. 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 and MAX_SIZE will take care of not passing an argument by returning False. We can return False 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, return False. So when the function returns True, return False.

  • 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 of MIN_SIZE and MAX_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 if password_size < MIN_SIZE and also when password_size > MAX_SIZE. The value we expect from this is False. That is our true success. That is when the password is in the desired range. We should write functions that return True on success and False on failure. So our new function will be better if we return password_size >= MIN_SIZE and password_size <= MAX_SIZE. It is the same as MIN_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 a set. I made valid_chars a set 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 a list or set or even an int? The best way is to use the try and except clause. We can return False on 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
Enter fullscreen mode Exit fullscreen mode

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .