Back to Blog
Python

Python bcrypt Password Hashing and Verification

python bcrypt password hashing and verification: Learn how to hash and verify passwords in Python using bcrypt, including byte encoding, salt, cost factor, and common...

bcryptpassword hashingauthenticationPython securitypassword verification
A padlock and a Python code snippet illustrating bcrypt password hashing and verification.

When you store passwords in a Python application, the goal is not to encrypt them but to hash them with a one-way function that resists brute-force and rainbow-table attacks. python bcrypt password hashing and verification is a common requirement for authentication systems. The bcrypt library provides a straightforward API for generating and checking password hashes, but its behavior around byte encoding and the cost factor can trip up developers. This article walks through the practical steps of hashing and verifying passwords with bcrypt in Python, including the details that matter in production.

Why bcrypt for Password Hashing in Python

bcrypt is a password-hashing function based on the Blowfish cipher. It was designed to be computationally expensive, which makes brute-force attacks slower, and it automatically incorporates a random salt, which defeats precomputed rainbow tables. Unlike general-purpose cryptographic hashes like SHA-256, bcrypt is intentionally slow and memory-hard, making it a better fit for password storage.

The Python bcrypt package wraps the reference implementation and exposes a small set of functions. You do not need to manage salt generation or hash format parsing manually; the library handles both.

Installing the bcrypt Library

The bcrypt package is available on PyPI and can be installed with pip:

pip install bcrypt

Once installed, import it in your Python code:

import bcrypt

The library is a C extension with a Python fallback, so it works on most platforms. For production, you should pin the version you use to avoid unexpected changes.

Hashing a Password with bcrypt

Hashing a password with bcrypt requires two steps: generate a salt and then hash the password with that salt. The gensalt function creates a random salt and encodes the cost factor into it. The hashpw function combines the password bytes and the salt to produce the final hash.

import bcrypt password = b"correct horse battery staple" salt = bcrypt.gensalt() hashed = bcrypt.hashpw(password, salt)

The hashed value is a byte string that contains the salt, the cost factor, and the actual hash. It is safe to store this value directly in a database column. The salt is embedded in the hash, so you do not need to store it separately.

You can also pass a rounds argument to gensalt to set the cost factor explicitly:

salt = bcrypt.gensalt(rounds=12)

The default is 12, which is a reasonable starting point for most applications.

Verifying a Password with bcrypt

To verify a password, use checkpw. It takes the plaintext password bytes and the stored hash, and returns True if the password matches, False otherwise.

import bcrypt stored_hash = b"$2b$12$..." # from your database password = b"correct horse battery staple" if bcrypt.checkpw(password, stored_hash): print("Login successful") else: print("Invalid password")

The checkpw function extracts the salt and cost factor from the stored hash and recomputes the hash of the provided password. It is designed to be constant-time for a given hash, which helps mitigate timing attacks.

How bcrypt Salt and Cost Factor Work

The salt is a random 16-byte value that ensures the same password produces different hashes each time it is hashed. This prevents attackers from using precomputed tables and makes it impossible to tell if two users have the same password just by comparing hashes.

The cost factor, often called rounds, controls how many iterations of the key derivation are performed. It is stored as a two-digit number in the hash string, after the $2b$ prefix. For example, in $2b$12$..., the cost factor is 12. Each increment doubles the work required, so a cost of 13 is twice as slow as 12.

When you call gensalt, you specify the cost factor. The resulting salt string includes that value. When you call hashpw, the cost factor from the salt is used. When you call checkpw, the cost factor from the stored hash is used, so you can increase the cost factor over time without breaking existing hashes.

Handling Bytes and Encoding Correctly

bcrypt operates on bytes, not strings. If your password is a Python str, you must encode it to bytes before passing it to hashpw or checkpw. The most common encoding is UTF-8:

password = "correct horse battery staple" password_bytes = password.encode("utf-8") hashed = bcrypt.hashpw(password_bytes, bcrypt.gensalt())

Be consistent with the encoding. If you encode with UTF-8 during hashing, you must encode with UTF-8 during verification. Using different encodings will produce different byte sequences and cause verification to fail.

Another critical detail is that bcrypt only uses the first 72 bytes of the password. Any bytes beyond that are silently ignored. If your application allows passwords longer than 72 bytes, you should either truncate them explicitly or pre-hash the password with a separate algorithm before passing it to bcrypt. The latter approach is more secure because it avoids silent truncation, but it adds complexity.

Common bcrypt Mistakes and Edge Cases

One frequent mistake is passing a str directly to hashpw or checkpw. This raises a TypeError because bcrypt expects bytes. Always encode your password strings.

Another issue is storing the hash as a str instead of bytes. When you read it back from a database, you may get a string. You need to encode it to bytes before calling checkpw. For example:

stored_hash_str = "$2b$12$..." # from a text column stored_hash = stored_hash_str.encode("utf-8")

If you store the hash in a bytes column, you avoid this conversion.

A third mistake is reusing the same salt for multiple passwords. This defeats the purpose of salt. Always call gensalt for each password you hash.

Finally, do not try to implement your own password verification logic by comparing hashes. Use checkpw; it handles the parsing and timing aspects correctly.

Setting the Cost Factor for Production

The cost factor is a tradeoff between security and performance. A higher cost factor makes brute-force attacks more expensive but also increases the CPU time required for every login and registration. You need to choose a value that is high enough to be secure but low enough to keep your authentication endpoints responsive.

A common approach is to benchmark gensalt and hashpw on your production hardware and pick the highest cost factor that still meets your latency budget. The default of 12 is a reasonable baseline, but you should adjust it based on your environment.

Because the cost factor is embedded in the hash, you can increase it over time. When a user logs in, you can check if their hash uses an older, lower cost factor and re-hash the password with the new setting. This allows you to strengthen security without forcing users to reset their passwords.

The bcrypt library also supports a prefix that indicates the bcrypt version, such as $2a$, $2b$, or $2y$. The $2b$ prefix is the current standard and is what gensalt produces by default. If you have legacy hashes with a different prefix, checkpw can still verify them as long as the algorithm is compatible.

python bcrypt password hashing and verification: Practical U | RYUSLOG DEV