Python bcrypt
last modified October 30, 2026 Python 3.15
In this article we show how to hash passwords in Python with the bcrypt library. We define basic terms including encryption, hashing, and salt.
The Python bcrypt module is a library for generating strong
password hashes. It is installed with the following command:
$ pip install bcrypt
Encryption
Encryption is the process of encoding a message or information in such a way that only authorized people can read it with a corresponding key. The intended information or message, referred to as plaintext, is encrypted using an encryption algorithm – a cipher – generating ciphertext that can be read only if decrypted. Encryption is a two-way function. When we encrypt something, we do so with the intention of decrypting it later. Encryption is used to protect data when transmitted, for example in mail communication.
Hashing
Hashing is the process of using an algorithm to map data of any size to a fixed length. The result is called a hash value. Whereas encryption is a two-way function, hashing is a one-way function. It is not possible to compute the input from the hash; an attacker can only guess inputs and compare their hashes. While encryption is meant to protect data in transit, hashing is meant to verify that data has not been altered and that it is authentic.
Passwords are not stored as plaintext in databases but as hashed values. When a user logs in, the application hashes the provided password and compares the result with the stored hash.
Salt
Salt is a fixed-length cryptographically-strong random value that is added to the input of hash functions to create unique hashes for every input. A salt makes a password hash unique even for users adopting common passwords, and it makes precomputed tables of hashes (rainbow tables) useless.
The bcrypt hashing function
bcrypt is a password hashing function designed by Niels Provos and David Mazières, based on the Blowfish cipher. It is the default password hash algorithm for OpenBSD. There are implementations of bcrypt for C, C++, C#, Java, JavaScript, PHP, Python and other languages.
The bcrypt algorithm salts and hashes the password for us. Unlike fast hash functions such as SHA-256, it is deliberately slow. The computation cost of the algorithm is parameterized, so it can be increased as computers get faster. The computation cost is called work factor or cost factor. It slows down the hashing, making brute force attempts harder and slower. The downside of a high cost factor is increased load on system resources and longer response times for users.
Python bcrypt create hashed password
In the next example, we create a hashed password.
import bcrypt passwd = b's$cret12' salt = bcrypt.gensalt() hashed = bcrypt.hashpw(passwd, salt) print(salt) print(hashed)
The example creates a salt and a hashed password with bcrypt. The library works with bytes, so the password is a bytes literal.
salt = bcrypt.gensalt()
A salt is generated with the gensalt function. It uses the
default cost factor of 12.
hashed = bcrypt.hashpw(passwd, salt)
A hashed value is created with the hashpw function, which takes
the cleartext value and a salt as parameters.
We run the program twice. Note that the salt is the first part of the generated hash value, and that each run produces a unique salt and hash.
$ python create_hashed_password.py b'$2b$12$Ix5G/YcivaEqQsttniYBoO' b'$2b$12$Ix5G/YcivaEqQsttniYBoOwaa7d7vrp/wxWL6UhiYYthE2HXKpU5a' $ python create_hashed_password.py b'$2b$12$NAU6hHbZ.A7zUDOqXFEqf.' b'$2b$12$NAU6hHbZ.A7zUDOqXFEqf.GOOCfRD7sz8sxj4LL88G2Y78i4.TOSS'
The structure of a bcrypt hash
A bcrypt hash is a 60-character string that contains everything needed to verify a password: the algorithm version, the cost factor, the salt, and the hash itself.
import bcrypt
hashed = bcrypt.hashpw(b's$cret12', bcrypt.gensalt())
_, prefix, cost, rest = hashed.decode().split('$')
salt, digest = rest[:22], rest[22:]
print(f'hash: {hashed.decode()} ({len(hashed)} chars)')
print(f'prefix: {prefix}')
print(f'cost: {cost}')
print(f'salt: {salt} ({len(salt)} chars)')
print(f'digest: {digest} ({len(digest)} chars)')
We split the hash into its parts.
_, prefix, cost, rest = hashed.decode().split('$')
salt, digest = rest[:22], rest[22:]
The fields are separated with the $ character. The
2b prefix identifies the current version of the algorithm. The
last field holds a 22-character salt followed by a 31-character digest, both
in a bcrypt-specific Base64 alphabet. Since the salt is stored in the hash, we
do not need to store it separately.
$ python hash_structure.py hash: $2b$12$3wzVPbk5ZboCQSBczCt4wuv3gDBQGDwk8SKeaZbAh6RPTjkuDzB4C (60 chars) prefix: 2b cost: 12 salt: 3wzVPbk5ZboCQSBczCt4wu (22 chars) digest: v3gDBQGDwk8SKeaZbAh6RPTjkuDzB4C (31 chars)
Python bcrypt check password
The following example checks passwords against a hashed value.
import bcrypt
passwd = b's$cret12'
hashed = bcrypt.hashpw(passwd, bcrypt.gensalt())
for attempt in [b's$cret12', b's$cret13', b'S$CRET12']:
if bcrypt.checkpw(attempt, hashed):
print(f'{attempt}: match')
else:
print(f'{attempt}: does not match')
A password is checked with the checkpw function. It extracts the
salt and the cost from the stored hash, hashes the attempt with them, and
compares the results in constant time, so an attacker cannot learn anything
from response times.
$ python check_passwd.py b's$cret12': match b's$cret13': does not match b'S$CRET12': does not match
Storing hashes in a database
The hash consists of ASCII characters only, so it can be decoded and stored in a text column. In the next example, we register and log in users with an in-memory SQLite database.
import sqlite3
import bcrypt
con = sqlite3.connect(':memory:')
con.execute('CREATE TABLE users(name TEXT PRIMARY KEY, pwd_hash TEXT)')
def register(name, password):
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
con.execute('INSERT INTO users VALUES (?, ?)', (name, hashed.decode()))
def login(name, password):
row = con.execute('SELECT pwd_hash FROM users WHERE name = ?',
(name,)).fetchone()
if row is None:
return False
return bcrypt.checkpw(password.encode(), row[0].encode())
register('john', 'falcon42')
register('jane', 'žltý kôň')
print(login('john', 'falcon42'))
print(login('john', 'falcon43'))
print(login('jane', 'žltý kôň'))
print(login('roger', 'falcon42'))
for name, pwd_hash in con.execute('SELECT name, pwd_hash FROM users'):
print(name, pwd_hash)
The register function stores the user name and the password hash;
the login function verifies a password.
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
con.execute('INSERT INTO users VALUES (?, ?)', (name, hashed.decode()))
Passwords are strings, so we encode them to UTF-8 bytes before hashing. The resulting hash is decoded to a string for storage.
return bcrypt.checkpw(password.encode(), row[0].encode())
When checking, both the password and the stored hash must be encoded back to bytes. An unknown user name simply fails the login.
$ python user_store.py True False True False john $2b$12$QFIxoBoDiag2BISSeLY/HeXnfJT/HX8XaKCT5GkCXNTX38uNp.3Nq jane $2b$12$pKt.rJjALWt0wpqK7.0bY.P4tXvLfZhiblAW26I2NOvHHy9j.T70.
Python bcrypt cost factor
The cost factor increases security by slowing down the hashing. The number
of rounds is 2cost, so increasing the cost by one
doubles the time.
import time
import bcrypt
passwd = b's$cret12'
for rounds in [4, 8, 10, 12, 13, 14]:
salt = bcrypt.gensalt(rounds=rounds)
start = time.perf_counter()
hashed = bcrypt.hashpw(passwd, salt)
end = time.perf_counter()
print(f'rounds={rounds:2} {end - start:8.4f} s {hashed.decode()[:7]}')
We set the cost factor with the rounds parameter of
gensalt and measure the time to generate the password hash with
time.perf_counter. The valid range is 4 to 31.
print(f'rounds={rounds:2} {end - start:8.4f} s {hashed.decode()[:7]}')
The cost is part of the hash prefix. A common goal is a value that takes about a quarter of a second on the production server; the default cost of 12 is close to that on the machine used here.
$ python cost_factor.py rounds= 4 0.0010 s $2b$04$ rounds= 8 0.0149 s $2b$08$ rounds=10 0.0587 s $2b$10$ rounds=12 0.2360 s $2b$12$ rounds=13 0.4668 s $2b$13$ rounds=14 0.9455 s $2b$14$
Upgrading the cost factor
When we raise the cost factor, the existing hashes keep their old cost. Since the plain password is available only during login, that is the time to rehash it.
import bcrypt
CURRENT_COST = 12
def cost_of(hashed):
return int(hashed.split(b'$')[2])
def login(password, hashed):
if not bcrypt.checkpw(password, hashed):
return False, hashed
if cost_of(hashed) < CURRENT_COST:
hashed = bcrypt.hashpw(password, bcrypt.gensalt(CURRENT_COST))
print(f'rehashed with cost {CURRENT_COST}')
return True, hashed
stored = bcrypt.hashpw(b's$cret12', bcrypt.gensalt(rounds=10))
print(f'stored cost: {cost_of(stored)}')
ok, stored = login(b's$cret12', stored)
print(ok, f'stored cost: {cost_of(stored)}')
ok, stored = login(b's$cret12', stored)
print(ok, f'stored cost: {cost_of(stored)}')
The cost_of function reads the cost from the hash.
if cost_of(hashed) < CURRENT_COST:
hashed = bcrypt.hashpw(password, bcrypt.gensalt(CURRENT_COST))
After a successful login, a hash with a lower cost is replaced with a new one. In a real application, the new hash would be saved to the database. The second login finds the current cost and does nothing.
$ python rehash.py stored cost: 10 rehashed with cost 12 True stored cost: 12 True stored cost: 12
The 72-byte limit
bcrypt uses at most 72 bytes of the password. Older versions of the library
silently ignored the rest; since version 5.0, hashpw raises a
ValueError for longer passwords. The limit is in bytes, and
non-ASCII characters take more than one byte in UTF-8.
import base64
import hashlib
import hmac
import bcrypt
passwd = 'ľ' * 40
data = passwd.encode()
print(len(passwd), len(data))
try:
bcrypt.hashpw(data, bcrypt.gensalt())
except ValueError as e:
print(f'ValueError: {e}')
PEPPER = b'server-side secret key'
def prehash(password):
digest = hmac.digest(PEPPER, password.encode(), hashlib.sha256)
return base64.b64encode(digest)
key = prehash(passwd)
print(key, len(key))
hashed = bcrypt.hashpw(key, bcrypt.gensalt())
print(bcrypt.checkpw(prehash(passwd), hashed))
print(bcrypt.checkpw(prehash(passwd[:-1]), hashed))
The password has 40 characters, but each ľ character takes two
bytes, so the encoded password is 80 bytes long.
def prehash(password):
digest = hmac.digest(PEPPER, password.encode(), hashlib.sha256)
return base64.b64encode(digest)
A common solution is to pre-hash the password with HMAC-SHA256 and a secret key (a pepper) kept outside the database. The digest is Base64-encoded to 44 bytes; the encoding avoids NUL bytes, which some bcrypt implementations treat as the end of the password.
$ python long_password.py 40 80 ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72]) b'zzrzQIzQpGIV/vIPy5Wf6Bg72cL8jx+oXGbGotJkHnw=' 44 True False
Deriving keys with bcrypt
The kdf function implements bcrypt_pbkdf, a key derivation
function. It turns a password into a key of the desired length, for example
for symmetric encryption. OpenSSH uses it to protect private keys.
import os import bcrypt passwd = b's$cret12' salt = os.urandom(16) key = bcrypt.kdf(password=passwd, salt=salt, desired_key_bytes=32, rounds=100) print(len(key), key.hex()) key2 = bcrypt.kdf(password=passwd, salt=salt, desired_key_bytes=32, rounds=100) print(key == key2)
We derive a 32-byte key from a password and a random salt. Unlike
hashpw, the salt is not part of the output; it must be stored
along with the encrypted data. The same password, salt, and number of rounds
always produce the same key.
$ python derive_key.py 32 23ca1b7414d33793e39a1c2642f9460a331712d6d289d27c0757bd469b78f27f True
Source
In this article we have used the Python bcrypt module to generate and verify password hashes. See also Python hashing and Python secrets.
Author
List all Python tutorials.