Use secrets, not random, and remember the shuffle
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 throws a good part of it away. random.shuffle uses the Mersenne Twister, so the arrangement becomes predictable even though the contents were not. An attacker who can model that generator no longer faces a fully random permutation.
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.
