Skip to content

How to Hash Passwords in Python with BCrypt

Learn how to securely hash passwords in Python using BCrypt to protect user credentials against brute-force attacks and database breaches.

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

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.065s

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)

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.

Navigation