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.
You will build a terminal-based Password Generator that:
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.
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.
In this tutorial, readers will build the full project by copying code snippets step-by-step. Every snippet is explained in detail, including:
Important rule while building: In Python, indentation defines scope. Anything indented under a function like
def main():belongs inside that function. All helper functions likeget_int()andgenerate_password()live at the file level (no indentation), somain()can call them.
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.
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.
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:
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.
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:
y, yes, n, no).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.
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:
twelve (caught by ValueError)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.
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.
string.ascii_lowercase.string.digits.string.punctuation.Returning a dictionary (instead of just one string) gives you two benefits:
"digits"), which is helpful if you ever want to display or log configuration.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.
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().
j from 0 to i.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.
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.
selected_sets is empty, password generation is impossible.ValueError.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.
We build a combined pool and choose secure random characters from it until the desired length is reached.
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.
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.
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.
while True loopThe 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.
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.
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.
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.
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.
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.
__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.
python password_generator.py), Python sets __name__ to "__main__", so main() runs.main() will not auto-run, which makes your code reusable.This connects backward because it depends on main() being defined above it.
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)
cd path/to/password-generator-cli
python password_generator.py
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.
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)