pyca/bcrypt: Password Hashing and Compatibility in Python
Modern(-ish) password hashing for your software and your servers
At a glance
- What is it?
- pyca/bcrypt provides bcrypt password hashing, legacy prefix support, an OpenSSH-compatible PBKDF, and a Rust extension for Python 3.9 and later. It remains usable, but its own README recommends argon2id or scrypt for new designs.
- Who is it for?
- Adopt pyca/bcrypt when an existing Python service needs bcrypt interoperability or the OpenSSH `bcrypt_pbkdf` function. Do not select it automatically for a new password system when argon2id or scrypt can satisfy the service requirements.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What pyca/bcrypt Solves in Python Applications
pyca/bcrypt handles the storage and verification of passwords using bcrypt. A raw password is never the artifact an application should retain. Instead, `hashpw()` combines the password with a generated salt and produces a hash. `checkpw()` later compares a candidate password against that stored hash. This model is aimed at software and server applications that need a password-specific key derivation function rather than a general-purpose digest.
The library also supports databases or workflows tied to the bcrypt format. Its configurable prefixes include `2b`, the default, and `2a`. The `2y` prefix remains accepted by `hashpw`, although the README marks it as deprecated. That compatibility is material for applications migrating older password records, because it gives the implementation a defined path for accepting an existing prefix without implying that the prefix should be selected for new designs by default.
The repository does not present bcrypt as the preferred choice for every new system. Its opening guidance calls bcrypt an “acceptable password hashing” choice but explicitly says to “really use argon2id or scrypt.” The practical audience is therefore narrower than the package description suggests: bcrypt is most convincing where format compatibility matters, an established application already uses bcrypt hashes, or a specific protocol requires `bcrypt_pbkdf`.
How bcrypt Hashing and Verification Move Through pyca/bcrypt
The basic data flow consists of two public operations. `bcrypt.gensalt()` generates the salt configuration, `bcrypt.hashpw(password, hashed_password_salt)` creates the encoded hash, and `bcrypt.checkpw(password, hashed)` verifies a submitted password. Keeping verification separate from hashing matters because an application should not implement its own comparison logic around the stored bcrypt string.
The work factor is logarithmic and configurable. The README's `bcrypt.gensalt(rounds=12)` example states that 12 is the default, while its hash example selects 14 rounds. Because existing hashes carry their cost settings in the encoded value, verification does not require the application to recreate the original `gensalt()` settings when reading a stored password. The README does not document automatic rehash detection or an upgrade policy, so applications should not assume that raising the default for new hashes automatically migrates old records.
There is a second key derivation interface, `bcrypt.kdf()`. Since version 3.0.0, the package exposes `bcrypt_pbkdf`, a KDF used by OpenSSH's newer encrypted private key format. The caller supplies the password, salt, desired output length, and round count. This is not interchangeable with the password-storage examples above. `kdf()` is for a protocol-defined key derivation exchange, while `hashpw()` and `checkpw()` implement the bcrypt password hashing workflow.
Based on `pyproject.toml`, bcrypt is a Python package with a native extension target named `bcrypt._bcrypt`. The build backend uses `setuptools` and `setuptools-rust`, and the extension is defined in `src/_bcrypt/Cargo.toml`. This explains why the README asks for both C and Rust compilers on Linux.
Installing pyca/bcrypt on Linux and Verifying a Password
The README's direct installation command installs the package through `pip`. Some Linux environments will first need native build dependencies.
sudo apt-get install build-essential cargoThis is the documented Debian and Ubuntu dependency command. It installs a C build toolchain and Rust through `cargo`. The README states that the minimum supported Rust version for this build is 1.74.0.
pip install bcryptAfter installation, use byte strings with the hashing API shown in the README. This first use creates a random salt, hashes the password, and verifies the same bytes against the result.
import bcrypt
password = b"super secret password"
hashed = bcrypt.hashpw(password, bcrypt.gensalt())
if bcrypt.checkpw(password, hashed):
print("It Matches!")
else:
print("It Does not Match :(")The expected branch for the unchanged password is `It Matches!`. A different password takes the second branch. The repository classifies bcrypt as `Development Status :: 5 - Production/Stable`, supports CPython Python 3.9 through 3.14 as declared in `pyproject.toml`, and also lists PyPy 3 and stable free-threaded Python builds as compatibility targets.
Configuring pyca/bcrypt Work Factors and Hash Prefixes
New installations can set the bcrypt cost and accepted generation prefix through `gensalt()`. The cost controls how much computation each password operation requires, while the prefix controls compatibility with bcrypt implementations.
import bcrypt
password = b"super secret password"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(14))The README identifies 12 as the default number of rounds and uses 14 here to select a larger work factor. It does not provide a recommendation based on deployment hardware, login latency, or server capacity. As a result, 14 is an example rather than a repository-backed sizing rule. An engineer changing the cost must assess its effect in the target environment without inventing an unstated performance target.
Prefix selection follows the same compatibility-oriented model:
bcrypt.gensalt(prefix=b"2b")`2b` is the default prefix according to the README, while `2a` can also be selected. The `2y` prefix is accepted by `hashpw` but deprecated. This split makes the package suitable for an existing corpus that contains legacy values, although the README does not describe a migration command or a recommended rehash-on-login workflow.
The 72-Byte Password Boundary in pyca/bcrypt
A concrete limitation is the maximum password length. Passing a password longer than 72 bytes to `hashpw()` raises `ValueError`. Earlier behavior silently truncated such passwords to match the original OpenBSD implementation, but the current README explicitly documents the exception. Applications must therefore validate input length before hashing, rather than assuming bcrypt will ignore trailing bytes.
For a password longer than that boundary, the README suggests applying a cryptographic hash, base64 encoding the digest, and passing that result to bcrypt. Its example also references `hashlib.sha256`, although the displayed snippet omits the corresponding imports:
password = b"an incredibly long password" * 10
hashed = bcrypt.hashpw(
base64.b64encode(hashlib.sha256(password).digest()),
bcrypt.gensalt()
)This is a preprocessing workaround, not evidence that unlimited input should silently flow into the password database. The README calls it a common approach and notes that base64 encoding can prevent NULL byte problems. It does not specify a preferred construction, encoding policy, or migration procedure. Password normalization, Unicode handling, maximum length, and security review therefore remain application responsibilities. Treating the 72-byte exception as a validation failure is simpler and less ambiguous than silently transforming input through an undocumented application-wide recipe.
The same warning applies to `kdf()`. The README documents `desired_key_bytes` and `rounds` for that interface but does not state a general input-size policy for it, so callers should not transfer assumptions from `hashpw()` without reviewing the underlying protocol.
pyca/bcrypt Compared with the README's scrypt and argon2id Choices
The repository names two alternative directions rather than presenting bcrypt as the final answer. For scrypt, it points to Python's `hashlib.scrypt` implementation and a key derivation function exposed by `cryptography`. For argon2id, it points to the `argon2_cffi` project. These are actual differences in package and API approach: `bcrypt` provides bcrypt password hashing plus `bcrypt_pbkdf`, while the alternatives provide scrypt or argon2id through their own libraries.
The README does not supply equivalent scrypt or argon2id examples, parameter tables, benchmark results, migration instructions, or compatibility guarantees. It is therefore not possible to derive a detailed performance comparison from this repository. Its recommendation is nevertheless clear at the design level. A new service with no bcrypt compatibility requirement should evaluate argon2id or scrypt before standardizing on bcrypt.
There is one category for which bcrypt is directly documented: OpenSSH's newer encrypted private key format uses `bcrypt_pbkdf`, exposed by `bcrypt.kdf()`. The API takes a password, salt, desired key length, and rounds:
import bcrypt
key = bcrypt.kdf(
password=b'password',
salt=b'salt',
desired_key_bytes=32,
rounds=100
)This call derives a 32-byte key using the parameters shown in the README. It is not a password login check and should not replace the `hashpw()` and `checkpw()` flow for account storage.
Maintenance, Native Builds, and Apache-2.0 Obligations
The repository is not archived, and its last push was on 2026-10-16. That recent repository activity does not establish a release cadence by itself. There are no GitHub releases listed, so upgrade planning must rely on package availability and `CHANGELOG.rst`, which the README identifies as the maintained changelog. The project metadata currently declares version 5.0.0 in `pyproject.toml`.
Native build requirements create a real upgrade consideration. The Python package uses a Rust extension through `setuptools-rust`. The README gives operating-system commands for Debian and Ubuntu, Fedora and RHEL derivatives, and Alpine:
apk add --update musl-dev gcc cargoThat command prepares the documented Alpine toolchain. Wheels, where available, could change which local tools are necessary, but the repository does not document wheel coverage for any platform, so deployment documentation should not assume that source builds are always avoided. Operators need to account for changes in native extension dependencies when upgrading in controlled build or restricted runtime environments.
The package is licensed under `Apache-2.0`, as declared in `pyproject.toml` and represented by the repository's `LICENSE` file. This matters when distributing the library, combining it with an application, or following notices and license obligations. This review does not interpret the license or provide legal advice. Maintenance work is likely to involve Python and Rust toolchains, while licensing compliance follows the actual distribution form used by the adopter.
Editorial conclusion
Adopt pyca/bcrypt when an existing Python service needs bcrypt interoperability or the OpenSSH `bcrypt_pbkdf` function. Do not select it automatically for a new password system when argon2id or scrypt can satisfy the service requirements. Verify first that password inputs are encoded and bounded correctly, that deployed work factors fit the latency budget, and that stored hashes record enough metadata for future rehash decisions.
Frequently asked questions
How do I install pyca/bcrypt in Python?
The README documents `pip install bcrypt`. On Linux, it says a C compiler and Rust compiler are needed when building from source, with Rust 1.74.0 identified as the minimum supported version.
How do I hash and verify a password with pyca/bcrypt?
Pass byte strings to `bcrypt.hashpw(password, bcrypt.gensalt())`, then verify them with `bcrypt.checkpw(password, hashed)`. The stored artifact is the generated bcrypt hash rather than the original password.
Does pyca/bcrypt accept passwords longer than 72 bytes?
`hashpw()` raises `ValueError` when the password exceeds 72 bytes. The README suggests hashing and base64 encoding the password first as a workaround, but that recipe requires application-level imports and policy.
What is the difference between pyca/bcrypt's password hashing and kdf APIs?
`hashpw()` and `checkpw()` provide bcrypt password hashing and verification. `kdf()` exposes `bcrypt_pbkdf`, which the README identifies as the KDF used by OpenSSH's newer encrypted private key format.
Should a new project choose pyca/bcrypt, scrypt, or argon2id?
The README calls bcrypt acceptable but explicitly recommends argon2id or scrypt. It points to `hashlib.scrypt`, `cryptography`, and `argon2_cffi` as alternatives, while bcrypt remains directly relevant to bcrypt compatibility and OpenSSH's `bcrypt_pbkdf` format.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/pyca-bcrypt)Was this useful?