A Practical Guide to Hashing Passwords in Python with bcrypt

If you’re building any application that involves user accounts, there’s one security rule you absolutely cannot break: never, ever store passwords in plaintext. A single database breach could expose every user’s password, leading to a catastrophic loss of trust and security. The correct way to handle passwords is to hash them. In this guide, we’ll walk through the best way to do this in Python using the bcrypt library.

So, what’s wrong with common hashing algorithms like MD5 or SHA-1? They were designed to be fast. That’s great for verifying file integrity, but terrible for passwords. A fast algorithm means an attacker can try billions of password combinations per second against your leaked hashes. We need something that is intentionally slow. Enter bcrypt.

Bcrypt is a password-hashing function designed by Niels Provos and David Mazières. It has several key features that make it the gold standard for password security:

  • It’s Slow: By design, bcrypt is computationally expensive. This drastically slows down brute-force attacks.
  • It Includes Salt: Bcrypt automatically generates and incorporates a “salt” (a random string) into each hash. This means that even if two users have the same password, their stored hashes will be completely different.
  • It’s Adaptive: As computers get faster, you can increase the “cost factor” of bcrypt to make it even slower, keeping pace with hardware improvements.

Setting Up Your Environment

First things first, let’s get bcrypt installed. It’s always a best practice to work inside a virtual environment to keep your project dependencies isolated.

# Create and activate a virtual environment (optional but recommended)
python -m venv venv
source venv/bin/activate  # On Windows, use venv\Scripts\activate

# Install the bcrypt library
pip install bcrypt

The Core Workflow: Hashing and Verifying

The entire process boils down to two main operations: hashing a password when a user signs up, and checking a password when they try to log in.

A crucial point to remember with bcrypt is that it operates on bytes, not strings. You must always encode your plaintext passwords into bytes (commonly using UTF-8) before hashing and checking them.

1. Hashing a New User’s Password

When a user creates an account, you take their chosen password, hash it with bcrypt, and store the resulting hash in your database. You do not store the plaintext password.

The process involves two functions:

  • bcrypt.gensalt(): Generates a random salt with a default cost factor.
  • bcrypt.hashpw(password_bytes, salt): Hashes the password using the generated salt.

Here’s how you do it:

import bcrypt

# The user's password, as a string
password = "supersecretpassword123"

# 1. Encode the password into bytes
password_bytes = password.encode('utf-8')

# 2. Generate a salt
salt = bcrypt.gensalt()

# 3. Hash the password
hashed_password = bcrypt.hashpw(password_bytes, salt)

print(f"Plaintext Password: {password}")
print(f"Hashed Password (bytes): {hashed_password}")

# The hashed_password is what you store in your database.
# It's a byte string that looks something like this:
# b'$2b$12$Ea3vPOy5fIUGHw2Tfnat7e9d9AdBIu4GshG1/2TLiJq9G05FF3a.O'

# You can decode it for storing in a VARCHAR field if needed
stored_hash_in_db = hashed_password.decode('utf-8')
print(f"Hashed Password to store in DB: {stored_hash_in_db}")

Notice the structure of the resulting hash. It contains the bcrypt version ($2b$), the cost factor ($12$), the 22-character salt, and finally the 31-character hash itself. The library packs everything you need into one convenient string.

2. Verifying a User’s Login Attempt

When a user tries to log in, they provide their password again. Your job is to fetch their stored hash from the database and check if the password they just provided matches.

This is done with the bcrypt.checkpw() function. It automatically extracts the salt from the stored hash, applies it to the attempted password, and compares the results in a way that’s safe against timing attacks.

import bcrypt

# --- Pretend this is a user login attempt ---

# The password the user entered in the login form
login_attempt_password = "supersecretpassword123"

# The hash you retrieved from your database for this user
stored_hash_from_db = b'$2b$12$Ea3vPOy5fIUGHw2Tfnat7e9d9AdBIu4GshG1/2TLiJq9G05FF3a.O'

# --- Verification ---

# 1. Encode the login attempt password into bytes
login_attempt_bytes = login_attempt_password.encode('utf-8')

# 2. Use checkpw to compare the attempted password with the stored hash
if bcrypt.checkpw(login_attempt_bytes, stored_hash_from_db):
    print("Login successful!")
else:
    print("Invalid password.")

# --- Trying with a wrong password ---
wrong_password_attempt = "wrongpassword"
wrong_password_bytes = wrong_password_attempt.encode('utf-8')

if bcrypt.checkpw(wrong_password_bytes, stored_hash_from_db):
    print("This should not appear.")
else:
    print("Invalid password (as expected for the wrong attempt).")

Important Considerations

Strings vs. Bytes: The Most Common Pitfall

I can’t stress this enough: bcrypt works with bytes. Forgetting to .encode('utf-8') your string passwords or .decode('utf-8') your stored hash when reading it as a string will lead to errors. Always be mindful of your data types.

Database Storage

A bcrypt hash is always 60 characters long. When designing your database schema, you can safely use a VARCHAR(60) or CHAR(60) column to store the hash. Since it contains only ASCII characters, you don’t need complex character sets.

Adjusting the Cost Factor

The “cost factor” (or rounds) determines how slow the hashing is. The default value for bcrypt.gensalt() (currently 12) is a good, secure starting point. You can increase it as computers get faster. Increasing the cost factor by 1 doubles the hashing time.

# Generate a salt with a higher cost factor (e.g., 14)
# This will be more secure but take longer to compute.
slower_salt = bcrypt.gensalt(rounds=14)
hashed_password_slower = bcrypt.hashpw(b"some_password", slower_salt)
print(hashed_password_slower)

Be careful not to set this too high, as it can negatively impact your server’s performance and the user’s login/registration experience.

Conclusion

Password security is non-negotiable, and bcrypt makes it incredibly straightforward to do it right in Python. By following the simple workflow of encoding passwords to bytes, using hashpw() for storage, and checkpw() for verification, you can significantly improve the security of your application and protect your users.

Remember the key takeaways:

  • Always hash passwords. Never store them in plaintext.
  • Use a slow, salted algorithm like bcrypt.
  • Bcrypt works with bytes, so encode/decode your strings.
  • Let bcrypt handle the salt; just store the full hash it returns.

Now go forth and build secure applications!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.