Storing passwords in plain text is one of the most dangerous security mistakes in software development. If an attacker gains access to your database, every user's password is immediately compromised. The solution is password hashing, a one-way transformation that converts passwords into irreversible, fixed-length strings.
BCrypt is one of the most trusted password hashing algorithms, specifically designed to be slow and resistant to brute-force attacks. This guide covers how to hash and verify passwords in Python using the bcrypt library.
Why BCrypt?
Not all hashing algorithms are suitable for passwords. General-purpose hash functions like MD5 and SHA-256 are designed to be fast, which actually makes them bad for password storage: an attacker can try billions of guesses per second.
BCrypt solves with several key features:
- Intentionally slow: It is computationally expensive, making brute-force attacks impractical.
- Built-in salting: It automatically generates and embeds a unique random salt in each hash, preventing rainbow table attacks.
- Configurable cost factor: You can increase the work factor as hardware gets faster, keeping the algorithm future-proof.
- Same password, different hash: Thanks to the random salt, hashing the same password twice produces different results.
Installation
Install the bcrypt library using pip:
pip install bcryptHashing a Password
To hash a password, you need two steps: generate a salt and then compute the hash.
import bcrypt
password = "my_secure_password"
password_bytes = password.encode('utf-8')
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_bytes, salt)
print("Salt:", salt)
print("Hash:", hashed)Output (varies each run due to random salt):
Salt: b'$2b$12$YcGl/EmtUvPQMHMbQuvh5O'
Hash: b'$2b$12$YcGl/EmtUvPQMHMbQuvh5Ou4wXjuGhWmzeu9CN7VBazVbZmYWE13K'Understanding the Hash Format
The BCrypt hash contains all the information needed to verify a password later:
$2b$12$LJ3m4ys3Lf.mQkWbgyeshOgOBbJGmZ2xTfEBirNaJHrAhcqgni6
│ │ │ │
│ │ │ └── Hashed password
│ │ └── 22-character salt
│ └── Cost factor (2^12 = 4096 iterations)
└── Algorithm identifier (2b = BCrypt)Verifying a Password
To check if a user-entered password matches the stored hash, use bcrypt.checkpw(). This function extracts the salt from the stored hash and re-hashes the input to compare:
import bcrypt
password = "my_secure_password"
password_bytes = password.encode('utf-8')
stored_hash = bcrypt.hashpw(password_bytes, bcrypt.gensalt())
entered_password = "my_secure_password"
entered_bytes = entered_password.encode('utf-8')
if bcrypt.checkpw(entered_bytes, stored_hash):
print("✅ Password matches! Access granted.")
else:
print("❌ Invalid password. Access denied.")Output:
✅ Password matches! Access granted.When Passwords Don't Match
import bcrypt
password = "correct_password"
stored_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
wrong_password = "wrong_password"
if bcrypt.checkpw(wrong_password.encode('utf-8'), stored_hash):
print("✅ Password matches!")
else:
print("❌ Invalid password.")Output:
❌ Invalid password.Adjusting the Cost Factor
The cost factor (also called work factor or rounds) controls how computationally expensive the hashing is. The default is 12, meaning 2¹² = 4,096 iterations. Higher values make hashing slower, which increases security but also increases login time.
import bcrypt
import time
password = b"test_password"
for rounds in [10, 12, 14]:
salt = bcrypt.gensalt(rounds=rounds)
start = time.perf_counter()
hashed = bcrypt.hashpw(password, salt)
elapsed = time.perf_counter() - start
print(f"Rounds: {rounds} | Time: {elapsed:.3f}s")Output (approximate, varies by hardware):
Rounds: 10 | Time: 0.068s
Rounds: 12 | Time: 0.267s
Rounds: 14 | Time: 1.065sComplete Example: Registration and Login System
Here's a practical example that simulates user registration and login:
import bcrypt
def register_user(password: str) -> bytes:
"""Hash a password for storage during registration."""
password_bytes = password.encode('utf-8')
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed
def verify_login(entered_password: str, stored_hash: bytes) -> bool:
"""Verify an entered password against the stored hash."""
entered_bytes = entered_password.encode('utf-8')
return bcrypt.checkpw(entered_bytes, stored_hash)
user_password = "S3cur3P@ssw0rd!"
stored_hash = register_user(user_password)
print(f"Stored hash: {stored_hash}\n")
login_attempts = [
("S3cur3P@ssw0rd!", "correct password"),
("wrongpassword", "wrong password"),
("S3cur3P@ssw0rd", "missing exclamation mark"),
]
for attempt, description in login_attempts:
result = verify_login(attempt, stored_hash)
status = "✅ Granted" if result else "❌ Denied"
print(f" {status}: '{description}'")Output:
Stored hash: b'$2b$12$q2ghLgM.91X5zcIlSTYxfuwSzIReX3/i/MVw5ODew/85ueWSSJhYy'
✅ Granted: 'correct password'
❌ Denied: 'wrong password'
❌ Denied: 'missing exclamation mark'Security Best Practices
Additional Guidelines
- Never log passwords: Not even during debugging.
- Use HTTPS: Ensure passwords are encrypted in transit.
- Enforce strong passwords: Set minimum length and complexity requirements.
- Rate-limit login attempts: Prevent brute-force attacks at the application level.
- Store only the hash: Never store the password or the salt separately since bcrypt embeds the salt in the hash.
BCrypt vs Other Hashing Algorithms
| Algorithm | Purpose | Speed | Built-in Salt | Recommended for Passwords? |
|---|---|---|---|---|
| BCrypt | Password hashing | Slow (tunable) | ✅ | ✅ Yes |
| Argon2 | Password hashing | Slow (tunable) | ✅ | ✅ Yes (newer standard) |
| scrypt | Password hashing | Slow (tunable) | ✅ | ✅ Yes |
| SHA-256 | General hashing | Fast | ❌ | ❌ No |
| MD5 | General hashing | Very fast | ❌ | ❌ No |
Conclusion
BCrypt is a battle-tested, industry-standard algorithm for password hashing in Python. It's intentionally slow, automatically handles salting, and allows you to tune the cost factor as hardware evolves.
Always use bcrypt.hashpw() for hashing and bcrypt.checkpw() for verification; never try to compare hashes manually.
By following these practices, you ensure that even if your database is compromised, user passwords remain protected.