Python gives you two ways to pick something at random, and only one of them belongs anywhere near a password.

random is a Mersenne Twister. It is fast, it is reproducible, and that second property is exactly the problem: it is a deterministic algorithm driven by internal state. Observe enough of its output and you can recover that state and predict everything it will produce next. For a dice roll or a shuffled playlist, wonderful. For anything that guards access, disqualifying.

secrets draws from the operating system’s cryptographically secure source, which is designed so that seeing previous output tells you nothing about future output. The API is deliberately small:

import secrets, string

alphabet = string.ascii_letters + string.digits
password = ''.join(secrets.choice(alphabet) for _ in range(16))

That is the swap most articles stop at, and it is genuinely the important one. But there is a second place the wrong generator sneaks back in.

The part that catches people

A common requirement is “at least one uppercase, one digit, one symbol”. The natural way to guarantee it is to pick one of each up front, fill the rest from the full pool, then shuffle so the guaranteed characters are not always in the same positions:

required = [secrets.choice(uppercase), secrets.choice(digits), secrets.choice(symbols)]
rest = [secrets.choice(pool) for _ in range(length - len(required))]
chars = required + rest

random.shuffle(chars)          # wrong

Every character was chosen securely, and the last line hands part of the job back to the wrong generator. random.shuffle uses the Mersenne Twister, so the arrangement comes off a predictable stream even though the contents do not.

It is worth being precise about what that costs, because this is easy to overstate and I did overstate it here at first. The characters keep every bit of entropy secrets.choice gave them; a password does not become guessable because a weak PRNG decided the order. The exposure is narrower than that. The shuffle exists to stop the required uppercase, digit and symbol sitting in fixed positions, and an attacker who can recover the Mersenne Twister’s state - which takes 624 consecutive outputs from it - gets that structure back, along with anything else drawn from the same stream. That is a real weakening of the guarantee I meant to offer, and it is not a broken password.

The rule is simpler than the risk calculation, which is the reason to follow the rule: once a value is security-relevant, every generator that touches it should be the secure one. Mix the two and you have to do the analysis above, every time, correctly.

The fix is a shuffle from the same secure source:

secrets.SystemRandom().shuffle(chars)
password = ''.join(chars)

secrets.SystemRandom() is a random.Random subclass backed by the OS entropy source, so it offers the familiar helpers (shuffle, randrange, sample) with the security properties you actually want. It exists precisely because secrets itself is intentionally minimal.

Two smaller things worth doing

Delete the unused import. If a file imports both random and secrets, and only uses secrets, remove the random line. It is not a vulnerability, it is worse in a subtle way: it leaves the dangerous tool loaded and one autocomplete away, in a file where the reader has to check every call site to be sure. Make the wrong thing unavailable.

Be careful excluding ambiguous characters. Dropping l, 1, O and 0 for readability is reasonable, but every exclusion shrinks the alphabet and therefore the entropy per character. If you strip characters, add length to compensate; going from a 62-character alphabet to 58 costs about 0.1 bits per character, which is cheap to buy back and worth doing deliberately rather than by accident.

The rule

Anything that protects access (passwords, tokens, reset links, session identifiers, API keys) uses secrets. Anything cosmetic or simulated can use random. And when you audit code for this, do not stop at the character picking. Follow the data all the way to the return statement, because the shuffle at the end is part of the password too.