NOETRION

Choose a topic

DAY 28 OF 30 · BUILD AND DEMONSTRATE

Project: a password generator

Build a generator with secrets, explicit length rules, and tests that verify properties instead of exact outputs.

90–120 minutes suggested study timePython 3.12Not yet completed

Today's outcomes

  • Use secrets for unpredictable choices.
  • Validate generation requirements.
  • Test length and character coverage without logging a real password.

Before you start: Complete Day 27 and its practical challenge.

Understand the concept

Choose randomness for the purpose

A password generator needs randomness that an attacker cannot reasonably predict from a simulation seed. Python's secrets module provides access to the operating system's secure randomness. The random module is useful for simulations and games; it is not the choice for password generation. Today you will use secrets.choice to choose characters from an explicit alphabet.

A generator's contract should state the length, allowed characters, and required character groups. This exercise uses ASCII lowercase letters, uppercase letters, digits, and a fixed symbol set. These are requirements for the project, not a universal claim that every service needs the same composition policy. Different services impose different allowed characters and maximum lengths.

Length and sampling matter as well as the visible appearance of a password. A short password with one character from each group can still have a small search space. This project's default is 16 characters and its accepted lengths are 12 through 128. That is an exercise design choice rather than a guarantee that every resulting credential is sufficient for every threat model.

Generate a candidate, then apply the contract

The reference samples every character independently from one alphabet. If the candidate lacks a required group, it samples a new candidate. This is rejection sampling. All strings in the initial equal-probability sample space that satisfy the group rule remain equally likely under the accepted distribution, assuming the underlying draws behave as intended.

An alternative strategy selects one character from each group and fills the remaining positions, then shuffles. That can meet the composition rule, but it generally does not produce the same distribution over valid strings. Do not describe the two algorithms as interchangeable without defining the property you are comparing.

The loop has no fixed guaranteed attempt count. At our allowed lengths, valid candidates are reasonably likely, but a probabilistic process is still probabilistic. Keep the alphabet nonempty and the group constraints achievable. For this bounded exercise, each required group is present in the alphabet and length is at least twelve.

Test properties instead of one expected password

A correct generator should return different unpredictable content across ordinary runs, so an assertion against one hard-coded password would test the wrong thing. Check length, membership in the alphabet, and presence of each required group. Test invalid lengths separately. A test that two outputs differ is not a rigorous guarantee: independent draws can theoretically agree.

Do not print the actual generated value in the test report. The interactive command-line version prints it once because that is the user's requested output; test logs only need a pass message. This exercise generates strings. It does not store accounts, hash credentials, implement password recovery, or replace a password manager.

Worked example

import secrets
import string

alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
length = 16
candidate = "".join(secrets.choice(alphabet) for _ in range(length))
assert len(candidate) == 16
assert all(character in alphabet for character in candidate)
print("Candidate length:", len(candidate))
print("Group checks are still needed before acceptance")
Property Checked by What it means
Length len(candidate) Exactly the requested character count
Alphabet Every character is a member No unsupported character appears
Group coverage At least one from each group Composition contract holds
Appropriate random source Use secrets Purpose matches password material
Hard-coded exact output Avoid this test It would conflict with unpredictable draws

The worked example is a candidate generator, not the complete challenge. It intentionally shows why length and membership alone do not establish group coverage. A string consisting only of uppercase letters could pass those first two checks.

Guided practice

  1. Predict: count the alphabet size. Explain why repeating a symbol in the alphabet would give that symbol extra probability instead of adding a distinct option.
  2. Modify: replace the symbol set with a smaller set. Keep all characters distinct and adjust the property test to use the same source of truth.
  3. Reconstruct: write a function that reports which required groups a supplied candidate is missing. Use fixed strings for this validator's tests, then use the validator with generated candidates.

Keep generation and reporting separate. A validator can be deterministic even when the generator is not. That distinction lets you diagnose a group-check bug using a fixed input, without repeatedly rerunning a random process and guessing what happened.

Daily challenge

Implement generate_password(length=16). Reject a non-integer or Boolean length and reject lengths outside 12–128 with ValueError. Return a string containing only the chosen alphabet and at least one lowercase letter, uppercase letter, digit, and symbol. Use secrets throughout generation. Provide a command-line entry point that requests length and prints one accepted password.

Case Expected behavior
Default argument 16 characters with all four groups
Length 12 or 128 Accepted under the same rules
Length 11 or 129 ValueError
True, text 16, or float 16.0 ValueError
Repeated tests Check properties without logging passwords

Record the alphabet and policy in your project README. An undocumented generator can produce a correct-looking value that a target service later rejects.

Reference solution

Compare after attempting the challenge
# file: password_app.py
import secrets
import string

GROUPS = (string.ascii_lowercase, string.ascii_uppercase,
          string.digits, "!@#$%^&*")
ALPHABET = "".join(GROUPS)

def meets_policy(candidate):
    return all(any(character in group for character in candidate)
               for group in GROUPS)

def generate_password(length=16):
    if type(length) is not int or not 12 <= length <= 128:
        raise ValueError("length must be an integer from 12 to 128")
    while True:
        candidate = "".join(secrets.choice(ALPHABET) for _ in range(length))
        if meets_policy(candidate):
            return candidate

if __name__ == "__main__":
    try:
        requested = int(input("Password length (12–128): "))
        print(generate_password(requested))
    except ValueError as error:
        print("Invalid length:", error)
# file: test_password.py
from password_app import generate_password, meets_policy, ALPHABET

assert not meets_policy("abcdefghijkl")
assert meets_policy("aB3!abcdefghijkl")
for length in [12, 16, 128]:
    for _ in range(20):
        password = generate_password(length)
        assert len(password) == length
        assert all(character in ALPHABET for character in password)
        assert meets_policy(password)
assert len(generate_password()) == 16
for bad in [11, 129, True, "16", 16.0]:
    try:
        generate_password(bad)
    except ValueError:
        pass
    else:
        raise AssertionError("Invalid length accepted")
print("Generator properties and invalid lengths passed")

Run python test_password.py for tests and python password_app.py for interaction. The group checker is only a composition check; by itself it does not validate length, alphabet membership, or the random source. The generator and tests establish those other parts separately.

Debug and explain

If generated strings never pass, verify that every required group is reachable from the alphabet. If Boolean lengths are accepted, inspect the type check. If a test is flaky, ask whether it asserts a probabilistic outcome such as uniqueness. Explain why a predictable seeded simulation generator is appropriate for tomorrow's game but not today's password project.

Review and next step

Save the policy, deterministic validator cases, and generator test results. You have practiced selecting an API by its guarantee and testing outputs by properties. Day 29 returns to ordinary game randomness and focuses on exhaustive outcome testing and clear state updates.

Sources and verification

Original teaching examples checked with Python 3.12.14. Library behavior reviewed against these primary references on October 3, 2026. Day 26's examples also use Beautiful Soup 4.14.3.

DAILY CHECKPOINT

Check your understanding

Five questions, 20 points each. Pass at 85% or above, which requires all five correct. Feedback explains each answer; revise the lesson and retry when needed.

01. Which module should generate password material here?

02. What is rejection sampling doing in the reference?

03. What should generator tests primarily check?

04. Why are duplicate characters undesirable in the alphabet?

05. Does group coverage alone establish full password validity?

Review your practical work

Check these statements after running your own challenge. This is your self-review; the website does not execute or assess your Python code.

Pass the knowledge check and finish the practical self-review to save completion.