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
- 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.
- 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.
- 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.
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.
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.