How to Hash Passwords and Authenticate Users in Python
A beginner-friendly dataclass-based design that hashes passwords with PBKDF2 and verifies them securely using constant-time comparisons.
Python code
51 linesimport hashlib
import hmac
import secrets
from dataclasses import dataclass
from typing import Optional
@dataclass
class User:
id: int
username: str
password_hash: str
salt: str
def hash_password(password: str) -> tuple[str, str]:
salt = secrets.token_hex(16)
password_hash = hashlib.pbkdf2_hmac(
"sha256", password.encode(), salt.encode(), 100_000
).hex()
return password_hash, salt
def verify_password(password: str, user: User) -> bool:
computed = hashlib.pbkdf2_hmac(
"sha256", password.encode(), user.salt.encode(), 100_000
).hex()
return hmac.compare_digest(computed, user.password_hash)
def create_user(user_id: int, username: str, password: str) -> User:
password_hash, salt = hash_password(password)
return User(id=user_id, username=username, password_hash=password_hash, salt=salt)
def authenticate(username: str, password: str, users: dict[str, User]) -> Optional[User]:
user = users.get(username)
if user and verify_password(password, user):
return user
return None
if __name__ == "__main__":
users_db = {}
alice = create_user(1, "alice", "S3curePass!")
users_db[alice.username] = alice
result = authenticate("alice", "S3curePass!", users_db)
print(f"Authenticated: {result.username if result else 'failed'}")
wrong = authenticate("alice", "wrongpass", users_db)
print(f"Wrong password: {wrong.username if wrong else 'failed'}")
Output
Authenticated: alice
Wrong password: failed
How it works
This code uses PBKDF2 with SHA-256 and 100,000 iterations to derive a secure password hash, making brute-force attempts computationally expensive. A per-user random salt is generated with secrets.token_hex(16) to prevent rainbow table attacks. The hmac.compare_digest check avoids timing attacks when comparing stored and computed hashes. The dataclass gives a clean, readable structure for user records while plain functions keep authentication logic composable. This pattern mirrors what production frameworks do under the hood.
Common mistakes
- Using fast hashes like MD5 or SHA-1 instead of a dedicated password hashing function like PBKDF2
- Storing plaintext passwords or reusing a single salt for all users
- Comparing hashes with `==` instead of `hmac.compare_digest` when timing matters
- Hardcoding too few PBKDF2 iterations — use at least 100,000 for SHA-256
Variations
- Use `bcrypt` or `argon2-cffi` libraries for even stronger password hashing
- Store users in a SQLite or PostgreSQL table instead of an in-memory dict
Real-world use cases
- Handling user signup and login for a web app where passwords must be stored safely in a database.
- Building an internal CLI admin tool that authenticates operators before exposing sensitive commands.
- Testing auth flows in microservice tests by creating fake users with known credentials.
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.