How to Salt Passwords per User in Python

Hash each user's password with a unique random salt using hashlib, and verify logins with timing-safe comparison.

Easy Python 3.9+ Aug 9, 2026 Auth & security at scale 14 views 0 copies

Python code

36 lines
Python 3.9+
import hashlib
import secrets

def hash_password(password: str, salt: str | None = None) -> tuple[str, str]:
    """Hash a password with a random salt (or provided salt).

    Returns:
        (salt_hex, password_hash_hex)
    """
    if salt is None:
        salt = secrets.token_hex(16)
    salted = (salt + password).encode("utf-8")
    digest = hashlib.sha256(salted).hexdigest()
    return salt, digest

def verify_password(password: str, salt: str, expected_hash: str) -> bool:
    """Check if password matches the stored salt + hash."""
    _, computed_hash = hash_password(password, salt)
    return secrets.compare_digest(computed_hash, expected_hash)

if __name__ == "__main__":
    users = {
        "alice": None,
        "bob": None,
    }

    for username in users:
        users[username] = hash_password("correct horse battery staple")

    print("Stored hashes (salt, hash):")
    for username, (salt, pw_hash) in users.items():
        print(f"{username}: salt={salt[:8]}... hash={pw_hash[:12]}...")

    print("\nVerification:")
    print("alice correct:", verify_password("correct horse battery staple", *users["alice"]))
    print("alice wrong:", verify_password("wrong", *users["alice"]))

Output

stdout
Stored hashes (salt, hash):
alice: salt=3f9a1c2b... hash=4b0e8d12...
bob: salt=7e2d4a09... hash=9c3f5a67...

Verification:
alice correct: True
alice wrong: False

How it works

The secrets.token_hex(16) call generates a cryptographically strong 32-character salt, ensuring each user's hash is unique even with identical passwords. Concatenating salt + password before SHA-256 prevents rainbow table attacks. secrets.compare_digest performs constant-time comparison, avoiding timing attacks that reveal hash matches. The hash function returns both salt and digest so you can store them together in your user database.

Common mistakes

  • Using a single global salt instead of a unique one per user
  • Reversing the order to password + salt can cause length-extension concerns (use HMAC for stricter needs)
  • Comparing hashes with `==` instead of `secrets.compare_digest`
  • Storing the salt and hash in plaintext without separating them clearly in the DB schema

Variations

  1. Use `hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100_000)` for stronger key stretching
  2. Adopt the `bcrypt` library for automatic salt handling and built-in work factor

Real-world use cases

  • Storing user credentials in a production authentication system to protect against credential theft.
  • Hashing passwords for internal tools or dashboards where users log in with personal accounts.
  • Creating secure API keys or tokens that must be verified without storing them in plaintext.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Auth & security at scale

Related tutorials and quizzes for this topic.