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.
Python code
36 linesimport 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
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
- Use `hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100_000)` for stronger key stretching
- 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
More from Auth & security at scale
- ACME LetsEncrypt Mock Challenge Server in Python medium
- AES GCM encryption and decryption in Python medium
- Build a Mock OIDC Userinfo Endpoint in Python with Flask easy
- ChaCha20-Poly1305 mock in Python medium
- ECDH key agreement in Python with cryptography medium
- Enforce TLS 1.2 Minimum in Python easy
Keep learning
Related tutorials and quizzes for this topic.