Scrollable Nav Bar

Password Generator in Python (CLI) — With Source Code

A password is often the single line of defense between your account and an attacker. Many real-world hacks don’t happen because someone “breaks encryption” — they happen because people reuse weak passwords, choose predictable patterns (name + year), or use the same password across multiple sites.

In this mini project, you’ll build a command-line Password Generator in Python that generates strong, random passwords using only the standard library. Most importantly, we’ll use secrets (cryptographic randomness) instead of random (which is not designed for security).

This tutorial is written so students can follow it step-by-step, professionals can appreciate the clean structure and validation, and developers can reuse the functions in other projects.


What you will build

You will build a terminal-based Password Generator that:

  • Runs in a terminal (Windows CMD/PowerShell, macOS Terminal, Linux shell)
  • Lets the user customize:
    • Password length (default 12)
    • Include lowercase letters (default Yes)
    • Include uppercase letters (default Yes)
    • Include digits (default Yes)
    • Include symbols (default Yes)
    • Number of passwords to generate (default 1)
  • Handles invalid input without crashing (empty input, wrong values, out-of-range)
  • Follows strong-password rules:
    • Length must be within a safe range (e.g., 4–128)
    • If the user disables all character types, show a friendly error and re-prompt
    • If multiple types are selected, each generated password contains at least one character from each selected type
  • After generating passwords, asks if the user wants to generate again (Y/N)

Why this project uses secrets (and not random)

Python provides more than one way to generate “random” values:

  • random is great for simulations, games, and simple demos — but it is not designed for security. In security-sensitive contexts, predictability can become a serious weakness.
  • secrets is specifically built for things like passwords, API keys, session tokens, and security codes. It uses a cryptographically strong source of randomness.

Because this project is a password generator, secrets is the correct tool.


Requirements

  • Python 3.x
  • Any code editor (VS Code, PyCharm, Sublime, Notepad++, etc.)
  • No external packages (only standard library modules)

Folder Structure

Create a folder like this:

password-generator-cli/
  password_generator.py

How it connects: You’ll paste every snippet into the same file (password_generator.py) in the exact order shown below. Each next snippet assumes the previous one is already present.

Lets Build the Code Step-by-Step

In this tutorial, readers will build the full project by copying code snippets step-by-step. Every snippet is explained in detail, including:

  • how this code snippet connects to the previous snippet
  • how it connects to the next snippet
  • which part of the code belongs inside the same function flow (so the program flow stays clear)

Important rule while building: In Python, indentation defines scope. Anything indented under a function like def main(): belongs inside that function. All helper functions like get_int() and generate_password() live at the file level (no indentation), so main() can call them.


Step 1: Imports (Top of the File)

Create a file named password_generator.py and start with these imports:

import secrets
import string
import sys
Code language: JavaScript (javascript)

This is the first code in the file, so there is nothing before it. Here’s why each import matters:

  • secrets provides cryptographically strong randomness (the core security requirement).
  • string gives ready-made character groups like lowercase letters, uppercase letters, digits, and punctuation.
  • sys is used for clean exit behavior when a user cancels input (Ctrl+C / Ctrl+D).

This connects forward because the next snippet defines constants and helper functions that depend on these modules.


Step 2: Add Configuration Constants (Still at the Top Level)

Paste this directly below the imports:

MIN_LEN = 4
MAX_LEN = 128
MIN_COUNT = 1
MAX_COUNT = 50

These constants are the “rules of the program.” Instead of hard-coding numbers in multiple places, you define them once and reuse them throughout.

  • MIN_LEN and MAX_LEN control the allowed password length range.
  • MIN_COUNT and MAX_COUNT control how many passwords can be generated in one run (to avoid accidental huge output).

This connects backward because constants must appear after imports, and it connects forward because input validation functions will use these boundaries.


Step 3: Create a Safe Input Function (Prevents Crashes)

def safe_input(prompt: str) -> str:
    try:
        return input(prompt)
    except (KeyboardInterrupt, EOFError):
        print("
Goodbye!")
        sys.exit(0)
Code language: PHP (php)

This helper exists for user experience and stability. In CLI apps, it’s common for users to press:

  • Ctrl+C (KeyboardInterrupt)
  • Ctrl+D (EOF on macOS/Linux) or Ctrl+Z then Enter (EOF on Windows)

Without handling these, Python can show a stack trace that feels “scary” and unprofessional. With safe_input(), your program exits cleanly.

This connects backward because it uses sys.exit() from the previous imports, and it connects forward because every other prompt function (get_yes_no() and get_int()) will use safe_input() instead of calling input() directly.


Step 4: Build a Reliable Yes/No Prompt (get_yes_no)

def get_yes_no(prompt: str, default: bool = True) -> bool:
    default_hint = "Y" if default else "N"
    while True:
        raw = safe_input(f"{prompt} [Y/N] (default {default_hint}): ").strip().lower()

        if raw == "":
            return default

        if raw in {"y", "yes"}:
            return True
        if raw in {"n", "no"}:
            return False

        print("Please type Y or N (example: Y).")
Code language: PHP (php)

This function is a strong example of beginner-friendly, professional CLI input handling:

  • The prompt includes both choices and the default.
  • If the user presses Enter without typing anything, the default is applied.
  • Common inputs are accepted (y, yes, n, no).
  • Invalid input doesn’t crash the program — it simply re-prompts.

This connects backward because it depends on safe_input(). It connects forward because the next step will create get_int() which follows a similar validation pattern, and later main() will use get_yes_no() for feature selection.


Step 5: Build a Safe Integer Prompt (get_int)

def get_int(prompt: str, default: int, min_value: int, max_value: int) -> int:
    while True:
        raw = safe_input(
            f"{prompt} (example: {default}) (range: {min_value}-{max_value}) "
            f"(press Enter for {default}): "
        ).strip()

        if raw == "":
            value = default
        else:
            try:
                value = int(raw)
            except ValueError:
                print("That wasn't a valid whole number. Try again (example: 12).")
                continue

        if value < min_value or value > max_value:
            print(f"Please enter a number between {min_value} and {max_value}.")
            continue

        return value
Code language: PHP (php)

This function is used for settings like password length and number of passwords.

Notice what it protects you from:

  • Empty input (uses the default)
  • Text input like twelve (caught by ValueError)
  • Out-of-range values (keeps prompting until valid)

This connects backward because it uses safe_input() and also relies on boundary values that will often come from your constants (MIN_LEN, MAX_LEN, etc.). It connects forward because the next step builds the character sets, and later main() uses get_int() to collect length and count safely.


Step 6: Build the Character Sets Based on User Choices

def build_charsets(include_lower: bool, include_upper: bool, include_digits: bool, include_symbols: bool) -> dict:
    sets = {}
    if include_lower:
        sets["lowercase"] = string.ascii_lowercase
    if include_upper:
        sets["uppercase"] = string.ascii_uppercase
    if include_digits:
        sets["digits"] = string.digits
    if include_symbols:
        sets["symbols"] = string.punctuation
    return sets
Code language: JavaScript (javascript)

This function converts the user’s Yes/No choices into real character pools.

  • If lowercase is enabled, we include string.ascii_lowercase.
  • If digits are enabled, we include string.digits.
  • If symbols are enabled, we use string.punctuation.

Returning a dictionary (instead of just one string) gives you two benefits:

  1. You keep the label for each set (like "digits"), which is helpful if you ever want to display or log configuration.
  2. You keep each character group separate, which is required for a later rule: “at least one character from each selected type.”

This connects backward because it depends on the string module imported in Step 1. It connects forward because the next steps will use the returned dictionary to guarantee strong passwords.


Step 7: Add a Secure Shuffle (Fisher–Yates With secrets)

def fisher_yates_shuffle(chars: list) -> None:
    for i in range(len(chars) - 1, 0, -1):
        j = secrets.randbelow(i + 1)
        chars[i], chars[j] = chars[j], chars[i]
Code language: PHP (php)

After generating a password, we want to shuffle the characters so the “required characters” (one from each set) don’t appear in predictable positions.

Python has random.shuffle(), but we avoid random completely in a security project. Here we implement a standard shuffle algorithm (Fisher–Yates) using secrets.randbelow().

  • Starting from the last index, we pick a secure random position j from 0 to i.
  • We swap characters.
  • Repeating this creates a uniform shuffle.

This connects backward because it uses secrets. It connects forward because the next step (generate_password) will build a password list, then call this shuffle to finish securely.


Step 8: Generate a Password That Guarantees All Selected Types

def generate_password(length: int, selected_sets: dict) -> str:
    if not selected_sets:
        raise ValueError("No character types selected.")

    set_values = list(selected_sets.values())
    required_count = len(set_values)

    if length < required_count:
        raise ValueError("Length too small to include all selected character types.")

    # Step 1: Guarantee at least one from each selected set
    password_chars = [secrets.choice(s) for s in set_values]

    # Step 2: Fill the rest from the combined pool
    pool = "".join(set_values)
    remaining = length - required_count
    password_chars.extend(secrets.choice(pool) for _ in range(remaining))

    # Step 3: Shuffle so required characters are not in predictable positions
    fisher_yates_shuffle(password_chars)

    return "".join(password_chars)
Code language: PHP (php)

This is the core security function.

First, we validate the inputs

  • If selected_sets is empty, password generation is impossible.
  • If the user selects 4 sets (lower + upper + digits + symbols) but requests a length of 3, the rule “at least one from each selected type” cannot be satisfied. In that case, we raise a clear ValueError.

Then, we guarantee rule compliance

The first list comprehension:

  • password_chars = [secrets.choice(s) for s in set_values]

ensures that we pick one character from each selected set. If you selected lowercase, uppercase, and digits, you will definitely get at least one of each.

Then, we fill the remaining characters

We build a combined pool and choose secure random characters from it until the desired length is reached.

Finally, we shuffle

Without shuffling, the “required” characters would always appear at the start of the password (in the same order as the sets). That makes the output slightly more predictable than necessary.

This connects backward because it uses secrets.choice() and the shuffle function from Step 7. It connects forward because main() will call generate_password() repeatedly to generate one or multiple passwords for the user.


Step 9: Add a Clean CLI Header

def print_header() -> None:
    print("
" + "=" * 34)
    print(" Password Generator (CLI)")
    print("=" * 34)
    print("Tip: Press Enter to accept defaults.
")
Code language: PHP (php)

Even simple CLI tools feel more professional when they present a clear header. This header also teaches beginners how to use defaults quickly.

This connects forward because main() calls print_header() at the start of each run. It connects backward because it uses no external state — it’s a clean, reusable function.


Step 10: Build the Main Program Loop (main)

Now Add the main program flow. This should be placed after all helper functions, because main() calls them.

def main() -> None:
    while True:
        print_header()

        print("Choose what to include in your password:")
        include_lower = get_yes_no("1) Include lowercase letters (a-z)?", default=True)
        include_upper = get_yes_no("2) Include uppercase letters (A-Z)?", default=True)
        include_digits = get_yes_no("3) Include digits (0-9)?", default=True)
        include_symbols = get_yes_no("4) Include symbols (!@#...)?", default=True)

        selected_sets = build_charsets(include_lower, include_upper, include_digits, include_symbols)

        # If user disables everything, re-prompt
        while not selected_sets:
            print("
You turned OFF all character types.")
            print("Please select at least one option to generate a password.
")
            include_lower = get_yes_no("1) Include lowercase letters (a-z)?", default=True)
            include_upper = get_yes_no("2) Include uppercase letters (A-Z)?", default=True)
            include_digits = get_yes_no("3) Include digits (0-9)?", default=True)
            include_symbols = get_yes_no("4) Include symbols (!@#...)?", default=True)
            selected_sets = build_charsets(include_lower, include_upper, include_digits, include_symbols)

        min_len_dynamic = max(MIN_LEN, len(selected_sets))
        length = get_int("Password length", default=12, min_value=min_len_dynamic, max_value=MAX_LEN)
        count = get_int("How many passwords to generate", default=1, min_value=MIN_COUNT, max_value=MAX_COUNT)

        print("
Generated password(s):")
        for i in range(1, count + 1):
            try:
                pwd = generate_password(length, selected_sets)
            except ValueError as e:
                print(f"Error: {e}")
                break
            print(f"{i}) {pwd}")

        print()
        if not get_yes_no("Generate again?", default=True):
            print("Done. Stay safe!")
            break
Code language: PHP (php)

This is where all earlier functions finally come together.

The outer while True loop

The program is designed to support repeated use. After generating passwords, the user is asked whether they want to generate again. If they say yes, the loop repeats and the tool runs again with fresh choices.

This connects backward because the loop depends on print_header(), get_yes_no(), and all other helpers. It connects forward because after main() is defined, the final step is to add the entry point so Python can run the program.

Asking for character types first

The first prompts ask for lowercase, uppercase, digits, and symbols. Notice how the prompt text includes examples like (a-z) and (0-9). This is beginner-friendly and reduces confusion.

After collecting the choices, build_charsets() produces the dictionary of selected sets.

Friendly re-prompt when all types are disabled

A common beginner mistake is to turn everything off. Instead of failing silently, we re-prompt until the user selects at least one type.

That inner loop is important because it guarantees that selected_sets is never empty when we move forward.

Dynamic minimum length

This line:

  • min_len_dynamic = max(MIN_LEN, len(selected_sets))

is a key stability and security rule.

  • MIN_LEN is the absolute minimum allowed length.
  • len(selected_sets) is the number of required categories.

If the user selects 4 categories, then the minimum length must be at least 4 to ensure “one from each set.” This prevents impossible combinations.

Generating one or multiple passwords

We print a label and then generate passwords in a loop. Each password is generated independently with secure randomness.

The try/except is defensive programming: even though earlier validation should protect us, we still catch ValueError so the tool never crashes.

Asking to generate again

Finally, we ask the user whether they want to run again. If they enter n, we print a friendly closing message and break the loop.


Step 11: Add the Entry Point (__name__ Guard)

Add this at the very bottom of the file:

if __name__ == "__main__":
    main()
Code language: JavaScript (javascript)

This is the standard Python entry point.

  • When the file is executed directly (python password_generator.py), Python sets __name__ to "__main__", so main() runs.
  • If you later import this file into another project, main() will not auto-run, which makes your code reusable.

This connects backward because it depends on main() being defined above it.

How to Run the Project

Run normally (recommended)

Open a terminal in the folder and run:

python password_generator.py
Code language: CSS (css)

On some systems you may need:

python3 password_generator.py
Code language: CSS (css)

If you installed Anaconda

  • Open Anaconda Prompt
  • Go to your folder and run:
cd path/to/password-generator-cli
python password_generator.py

If you want to try Jupyter Notebook

Because this is a CLI tool (it uses input()), it works best in a terminal. Jupyter can run input() in many cases, but the user experience is cleaner in the terminal.


Example Output (What You’ll See in the Terminal)

Here’s a realistic example of the user experience:

==================================
 Password Generator (CLI)
==================================
Tip: Press Enter to accept defaults.

Choose what to include in your password:
1) Include lowercase letters (a-z)? [Y/N] (default Y):
2) Include uppercase letters (A-Z)? [Y/N] (default Y):
3) Include digits (0-9)? [Y/N] (default Y):
4) Include symbols (!@#...)? [Y/N] (default Y):
Password length (example: 12) (range: 4-128) (press Enter for 12): 16
How many passwords to generate (example: 1) (range: 1-50) (press Enter for 1): 3

Generated password(s):
1) 9t$X]sM/`q1Y&vB!
2) kA{8f^2QzW+@dH0~
3) _pN3!wR7mG#1vL}C

Generate again? [Y/N] (default Y): n
Done. Stay safe!
Code language: PHP (php)