Weak passwords are one of the easiest ways attackers gain access to accounts and systems. Building your own password generator in Python is a practical project that teaches you about randomness, the standard library, and security-aware coding — all at once.
This project uses nothing but Python's standard library. No installs required. By the end, you will have a working script that generates cryptographically secure passwords of any length, with full control over which character types to include. You will also understand why each design decision was made — which matters more than the code itself.
Why Use secrets, Not random
Python's built-in random module is useful for simulations, games, and sampling — but it is not appropriate for generating passwords. The random module uses a pseudo-random number generator (PRNG) seeded by the system clock. Given the seed, an attacker could theoretically predict its output. That is a serious problem for anything security-related.
The secrets module, introduced in Python 3.6, is designed specifically for generating cryptographically strong random values. It draws from the operating system's entropy source — on Linux and macOS that is /dev/urandom, on Windows it is CryptGenRandom. These sources are far harder to predict than a clock-based seed.
The Python documentation explicitly states that secrets should be used "for generating cryptographically strong random numbers suitable for managing data such as passwords, account authentication, security tokens, and related secrets." When it comes to passwords, always reach for secrets first.
The key function you will use is secrets.choice(sequence). It picks one item from a sequence at random using the OS entropy source. That is the only function you need for this project.
Building the Character Set
A strong password draws from a large, unpredictable pool of characters. Python's string module gives you ready-made constants for every character category you need:
string.ascii_lowercase— the 26 lowercase letters (a–z)string.ascii_uppercase— the 26 uppercase letters (A–Z)string.digits— the 10 digit characters (0–9)string.punctuation— 32 special characters such as!,@,#,$, and so on
You build your character pool by concatenating whichever of these you want to allow. For example, a pool of lowercase letters and digits looks like this:
import string
pool = string.ascii_lowercase + string.digits
print(pool)
# abcdefghijklmnopqrstuvwxyz0123456789
The wider the pool, the more possible passwords exist for any given length, which makes brute-force attacks exponentially harder. A 16-character password drawn from all four character types has roughly 95^16 possible combinations — a number with 31 digits.
Some services reject certain special characters in passwords. If you are generating passwords for a specific site, you can build a custom punctuation string instead of using the full string.punctuation constant. Simply define punctuation = "!@#$%^&*()" and use that in your pool.
Writing the Generator Function
With the right module and character pool in place, the core generator function is straightforward. The logic is: pick one character at a time from the pool using secrets.choice(), repeat that for the desired password length, and join the results into a single string.
import secrets
import string
def generate_password(length=16, use_upper=True, use_digits=True, use_symbols=True):
pool = string.ascii_lowercase
if use_upper:
pool += string.ascii_uppercase
if use_digits:
pool += string.digits
if use_symbols:
pool += string.punctuation
if not pool:
raise ValueError("At least one character type must be selected.")
password = ''.join(secrets.choice(pool) for _ in range(length))
return password
A few things worth noting about this function. The length parameter defaults to 16, which is a reasonable minimum for general-purpose passwords. The boolean flags let callers opt in or out of each character category. The guard clause at the bottom raises a clear error if someone somehow passes all flags as False, which would otherwise produce an empty pool and a confusing result.
The generator expression secrets.choice(pool) for _ in range(length) calls secrets.choice() once per character position. Each call is independent and draws from OS entropy, so there is no pattern linking one character to the next.
This approach does not guarantee that the generated password will contain at least one character from each selected category. For a password of length 16 or more, that is rarely a practical problem — but if you need to guarantee inclusion of at least one digit, one symbol, and so on, you can seed the password with one secrets.choice() call per required category and then fill the remaining positions from the full pool, shuffling the result with secrets.SystemRandom().shuffle().
The Full Script with User Input
A function is useful on its own, but a complete script that accepts user preferences makes the tool practical. Here is the finished version, ready to run from the command line:
import secrets
import string
def generate_password(length=16, use_upper=True, use_digits=True, use_symbols=True):
pool = string.ascii_lowercase
if use_upper:
pool += string.ascii_uppercase
if use_digits:
pool += string.digits
if use_symbols:
pool += string.punctuation
if not pool:
raise ValueError("At least one character type must be selected.")
return ''.join(secrets.choice(pool) for _ in range(length))
def get_yes_no(prompt):
while True:
answer = input(prompt).strip().lower()
if answer in ('y', 'yes'):
return True
elif answer in ('n', 'no'):
return False
print("Please enter y or n.")
def main():
print("=== Python Password Generator ===\n")
while True:
try:
length = int(input("Password length (minimum 8): "))
if length < 8:
print("Length must be at least 8.")
continue
break
except ValueError:
print("Please enter a whole number.")
use_upper = get_yes_no("Include uppercase letters? (y/n): ")
use_digits = get_yes_no("Include digits? (y/n): ")
use_symbols = get_yes_no("Include special characters? (y/n): ")
try:
password = generate_password(
length=length,
use_upper=use_upper,
use_digits=use_digits,
use_symbols=use_symbols
)
print(f"\nGenerated password: {password}")
except ValueError as e:
print(f"Error: {e}")
if __name__ == "__main__":
main()
Running this script prompts you for a length and three yes/no questions, then prints the generated password. The if __name__ == "__main__": guard means the script can also be imported as a module in a larger project — calling generate_password() directly — without triggering the interactive prompts.
The get_yes_no() helper function keeps the input handling clean. Rather than repeating validation logic three times, it loops until the user provides a recognized answer. This is a simple pattern worth remembering whenever you need validated boolean input.
"Attackers don't hack in — they log in." — Common security industry saying
Credential-based attacks remain one of the leading causes of breaches. Building tools that make strong passwords easy to generate is one of the smallest changes with the largest security payoff.
Key Takeaways
- Always use
secretsfor passwords: Therandommodule is not cryptographically secure. Thesecretsmodule is the correct choice for any security-sensitive random value in Python. - Character pool width determines strength: More character categories and longer lengths multiply the search space an attacker must cover, making brute-force attacks impractical.
- Keep generation logic separate from I/O: Putting the core logic in a standalone function makes it reusable, testable, and easy to integrate into other scripts or web applications.
- Validate user input explicitly: Security tools should fail clearly and loudly when given bad input, not silently produce unusable output.
From here, you can extend this script in several directions — generating multiple passwords at once, writing them to a file, adding a command-line argument interface with argparse, or wrapping it in a simple web front end with Flask. The core logic stays the same regardless of how you package it.