← All articles

Generating and hashing passwords in Python

The secrets module exists precisely so that you never have to think about randomness again — and argon2-cffi wraps Argon2id in two calls.

1. Generate a password

secrets draws from os.urandom(), and its choice() is already rejection-sampled, so the distribution over the alphabet is uniform:

python
import secrets
import string

ALPHABET = (
    string.ascii_uppercase
    + string.ascii_lowercase
    + string.digits
    + "!@#$%^&*()-_=+[]{};:,.?/~"
)


def generate_password(length: int = 24, alphabet: str = ALPHABET) -> str:
    """Return a cryptographically random password.

    secrets.choice() is backed by os.urandom(); the `random` module is a
    Mersenne Twister and must never be used for anything security related.
    """
    if length < 1 or not alphabet:
        raise ValueError("length and alphabet must be non-empty")
    return "".join(secrets.choice(alphabet) for _ in range(length))

Adding to the same module, to guarantee at least one character from each set and keep the positions unbiased, pick one per set and shuffle with SystemRandom:

python
def generate_structured(length: int = 24) -> str:
    sets = [
        string.ascii_uppercase,
        string.ascii_lowercase,
        string.digits,
        "!@#$%^&*()-_=+[]{};:,.?/~",
    ]

    chars = [secrets.choice(s) for s in sets]
    pool = "".join(sets)
    chars += [secrets.choice(pool) for _ in range(length - len(chars))]

    # SystemRandom.shuffle() is a Fisher-Yates shuffle on os.urandom().
    secrets.SystemRandom().shuffle(chars)
    return "".join(chars)

2. Hash with Argon2id

Install with pip install argon2-cffi. Memory is given in KiB:

python
from argon2 import PasswordHasher
from argon2.exceptions import VerificationError

# time_cost = iterations, memory_cost = KiB, parallelism = lanes.
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4,
                    hash_len=32, salt_len=16)

encoded = ph.hash(password)
# '$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>'

try:
    ph.verify(encoded, candidate)   # raises VerificationError when wrong
    # Re-hash when your parameters have moved on since this hash was stored:
    if ph.check_needs_rehash(encoded):
        new_encoded = ph.hash(candidate)
        save_hash(user_id, new_encoded)
except VerificationError:
    # Wrong password, or a malformed hash. Treat both as failure and return the
    # same response you would give for a non-existent account.
    deny()

Note the argument order: verify(hash, password). Passing them the other way round is a mistake that shows up regularly in real code, and it fails in a confusing way.

3. Hash with bcrypt

python
import bcrypt

hashed = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt(rounds=12))
# b'$2b$12$...'

bcrypt.checkpw(candidate.encode("utf-8"), hashed)   # True / False

Two limits worth designing around: bcrypt ignores everything after the first 72 bytes, and the library rejects passwords containing a NUL byte with a ValueError. Encode explicitly and validate input before it reaches this point.

Pitfalls in Python

  • random.choice(), random.shuffle() and random.randint() are predictable. If you find import random near a password, stop.
  • hashlib.sha256(pw).hexdigest() — and the classic salt + password concatenation — appears in a surprising amount of older code. It is a fast hash: billions of guesses per second on a GPU.
  • Comparing with == instead of checkpw() or verify() leaks timing information.
  • Storing the hash without the parameters makes a future upgrade impossible. Store the whole string — everything needed is inside it.
  • Relying on passlib for new projects is increasingly hard to recommend; it is unmaintained, and it warns about that itself. Use argon2-cffi or bcrypt directly.

Checking a hash by hand

When you are tuning parameters, or debugging a row that will not verify, it helps to see the hash and the comparison outside your test suite. The Verify tab performs the same Argon2id and bcrypt checks and tells you match or no match — handy for confirming that the hash in the database is really the one your code produced.

Verify an argon2-cffi or bcrypt hash — paste the password and the stored string, get a straight answer.