GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • ZetCode
  • All
  • Golang
  • Python
  • C#
  • Java
  • JavaScript
  • Subscribe
  • Twitter
  • Github
  • Privacy
  • About

This view

  • Original page
  • New address

GeezerVison is showing zetcode.com — Python bcrypt - hashing passwords in Python with bcrypt

Ebooks

  • PyQt5 ebook
  • Tkinter ebook
  • SQLite Python
  • wxPython ebook
  • Windows API ebook
  • Java Swing ebook
  • Java games ebook
  • MySQL Java ebook

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.

Note: Hashing is not limited to security. It is also used to compare large amounts of data or for fast key lookups.

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.

create_hashed_password.py
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.

hash_structure.py
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.

check_passwd.py
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.

user_store.py
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.

cost_factor.py
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.

rehash.py
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.

long_password.py
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.

derive_key.py
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

Python bcrypt GitHub page

In this article we have used the Python bcrypt module to generate and verify password hashes. See also Python hashing and Python secrets.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Python tutorials.

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.