Skip to Content
Course content

415: Building a Password Generator

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen a lot of junior devs start their first password generator by reaching for the random module. It feels like the obvious choice. You want something random, and there's a module literally called random. But here is the problem: random is a pseudo-random number generator. It's designed for simulations and games, not for security. If an attacker knows exactly when your program started or can guess the "seed" used by the generator, they can predict every single "random" character your program will ever produce.

Thinking random.choice() is secure

Let's look at what a typical "first attempt" looks like. You might write something like this:

import random
import string

chars = string.ascii_letters + string.digits + string.punctuation
password = ''.join(random.choice(chars) for i in range(12))
print(password)

On the surface, this looks perfect. It's concise, it's fast, and the output looks like a password. But because random uses the Mersenne Twister algorithm, it is completely deterministic. If I can see a few outputs from your generator, I can potentially reverse-engineer the internal state and predict the next password you generate. In the world of security, "looks random" isn't the same as "is cryptographically secure."

Using secrets for true unpredictability

Since Python 3.6, we have the secrets module. I always tell my mentees: if the output is going to be used for a password, a token, or a security key, random is banned. Use secrets. It accesses the most secure source of randomness provided by your operating system (like /dev/urandom on Unix).

The transition is actually very easy because the API is almost identical. Here is how we actually build this to be production-ready:

import secrets
import string

def generate_secure_password(length=16):
    # Define our character pools
    alphabet = string.ascii_letters + string.digits + string.punctuation
    
    # secrets.choice is cryptographically strong
    password = ''.join(secrets.choice(alphabet) for i in range(length))
    return password

print(generate_secure_password())

Now, we've solved the predictability problem, but we have a new one: probability. If you just pick characters randomly, there is a small (but real) chance your 12-character password contains no numbers or no symbols. Most systems require at least one of each. To fix this, I prefer a "guarantee" approach: pick one character from each required set first, then fill the rest of the length randomly, and finally shuffle the whole thing so the required characters aren't always at the beginning.

Guaranteeing complexity through forced inclusion

To make sure the password actually meets complexity requirements, we can't just hope for the best. We need to be explicit. Check out this pattern:

import secrets
import string

def generate_complex_password(length=12):
    if length < 4:
        raise ValueError("Password length must be at least 4 to include all required types.")

    # 1. Define required sets
    sets = [
        string.ascii_lowercase,
        string.ascii_uppercase,
        string.digits,
        string.punctuation
    ]
    
    # 2. Guarantee one character from each set
    password = [secrets.choice(s) for s in sets]
    
    # 3. Fill the remaining length from the combined pool
    all_chars = "".join(sets)
    password += [secrets.choice(all_chars) for _ in range(length - 4)]
    
    # 4. Shuffle the list so the first 4 aren't predictable
    # We use secrets.SystemRandom().shuffle because random.shuffle is not secure
    secrets.SystemRandom().shuffle(password)
    
    return "".join(password)

print(generate_complex_password(16))

By using secrets.SystemRandom().shuffle(), we keep the entire process cryptographically secure while ensuring the password isn't just random, but also compliant with standard security policies.




📋 Practical Task

Build a "Custom Requirement" Password Generator

Your task is to create a function called generate_custom_password(length, use_digits=True, use_special=True). The generator should behave as follows:

  • If use_digits is True, the password must contain at least one digit.
  • If use_special is True, the password must contain at least one punctuation character.
  • The password must always contain at least one lowercase and one uppercase letter.
  • It must use the secrets module for all random selections and shuffling.
  • If the requested length is shorter than the number of required character types, the function should raise a ValueError.

Test your function by generating a 12-character password with use_digits=True and use_special=False, and verify that no special characters appear in the output.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.