Skip to main content

How to Hash Passwords in Python with BCrypt

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 bcrypt

Hashing a Passwordโ€‹

To hash a password, you need two steps: generate a salt and then compute the hash.

import bcrypt

# The password to hash
password = "my_secure_password"

# Convert the password string to bytes
password_bytes = password.encode('utf-8')

# Generate a salt (random data added to the password before hashing)
salt = bcrypt.gensalt()

# Hash the password with the salt
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)
Same password, different hashes

Because a new random salt is generated each time, hashing the same password produces a different hash on every call:

import bcrypt

password = b"hello123"

hash1 = bcrypt.hashpw(password, bcrypt.gensalt())
hash2 = bcrypt.hashpw(password, bcrypt.gensalt())

print(hash1) # b'$2b$12$abc...'
print(hash2) # b'$2b$12$xyz...' (Different!)

Output:

b'$2b$12$etO.0BLu2zoGyP5jieHkGOFImEmoFa2D1B03LdP8C.gJjJ5IjJKmS'
b'$2b$12$QWPy1CAzFQ92V/9bvUNs7OrdVcDViLvClLEz3TAxhIjOAi2vEdyS6'

This is a security feature. Even if two users have the same password, their hashes will be different.

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

# --- Registration: Hash and store the password ---
password = "my_secure_password"
password_bytes = password.encode('utf-8')
stored_hash = bcrypt.hashpw(password_bytes, bcrypt.gensalt())

# --- Login: Verify the entered password ---
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())

# User enters wrong password
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.065s
Choosing the right cost factor

Each increment doubles the computation time. Choose a value that makes hashing take approximately 250msโ€“500ms on your server:

RoundsApproximate TimeRecommendation
10~70msMinimum for production
12~250msGood default
14~1sHigh security
16~4sToo slow for most applications

You can increase the rounds over time as hardware improves, without needing to re-hash existing passwords immediately.

Complete 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)

# --- Simulate user registration ---
user_password = "S3cur3P@ssw0rd!"
stored_hash = register_user(user_password)
print(f"Stored hash: {stored_hash}\n")

# --- Simulate login attempts ---
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โ€‹

Common mistakes to avoid

Never store passwords in plain text:

# INCORRECT: plain text storage
users_db = {"alice": "password123"}

Never use MD5 or SHA for passwords:

import hashlib
# INCORRECT: too fast, no salt
hashed = hashlib.sha256(b"password").hexdigest()

Never create your own salt manually:

# INCORRECT: predictable salt
salt = b"my_fixed_salt"

Always use bcrypt with auto-generated salts:

import bcrypt
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())

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โ€‹

AlgorithmPurposeSpeedBuilt-in SaltRecommended for Passwords?
BCryptPassword hashingSlow (tunable)โœ…โœ… Yes
Argon2Password hashingSlow (tunable)โœ…โœ… Yes (newer standard)
scryptPassword hashingSlow (tunable)โœ…โœ… Yes
SHA-256General hashingFastโŒโŒ No
MD5General hashingVery 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.