(trapped) error reading bcrypt version
Traceback (most recent call last):
File "/app/.venv/lib/python3.12/site-packages/passlib/handlers/bcrypt.py", line 620, in _load_backend_mixin
version = _bcrypt.__about__.__version__
^^^^^^^^^^^^^^^^^
AttributeError: module 'bcrypt' has no attribute '__about__'
passlib reads bcrypt.__about__.__version__ to find out which bcrypt it is talking to. Newer bcrypt releases no longer ship __about__, so the lookup fails. Depending on the combination of versions, this is either a logged warning that you can ignore or the first step of a real failure.
TLDR
(trapped) error reading bcrypt versionfollowed byAttributeError ... '__about__': passlib only logged it and hashing still works. It is noise, but it means passlib and bcrypt are out of step.ValueError: password cannot be longer than 72 byteson a short password, raised fromdetect_wrap_bug: bcrypt 5 or later with passlib 1.7.4. Pinbcrypt<5or drop passlib.ValueErrorwith your own long password: bcrypt only uses the first 72 bytes. Pre-hash with SHA-256 or switch to Argon2.- Long-term fix: call
bcryptdirectly or usepwdlib. Both are shown below.
The warning: bcrypt has no attribute __about__
This is the typical setup that prints it:
from fastapi import FastAPI
from passlib.context import CryptContext
app = FastAPI()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@app.post("/register")
def register(password: str):
return {"hash": pwd_context.hash(password)}
The first call to pwd_context.hash loads the bcrypt backend, and that is when passlib touches bcrypt.__about__. The traceback is marked (trapped) because passlib catches the exception itself and carries on. If your endpoint returns a hash, the warning is cosmetic.
You can confirm which versions you have:
pip list 2>/dev/null | grep -i -E "passlib|bcrypt"
passlib 1.7.4 is the last passlib release. It was written before bcrypt moved to a Rust implementation and changed its package layout, which is why the two disagree.
To silence the message without changing libraries, pin an older bcrypt that still provides __about__:
pip install "bcrypt<4.1"
This keeps you on an old bcrypt, so treat it as a stopgap.
The failure: password cannot be longer than 72 bytes on a short password
With bcrypt 5 and passlib 1.7.4 the same first call can fail instead of warning:
Traceback (most recent call last):
File "/app/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 301, in app
raw_response = await run_endpoint_function(
File "/app/app/routes/auth.py", line 14, in register
return {"hash": pwd_context.hash(password)}
File "/app/.venv/lib/python3.12/site-packages/passlib/context.py", line 2258, in hash
return record.hash(secret, **kwds)
File "/app/.venv/lib/python3.12/site-packages/passlib/handlers/bcrypt.py", line 655, in _finalize_backend_mixin
if detect_wrap_bug(IDENT_2A):
File "/app/.venv/lib/python3.12/site-packages/passlib/handlers/bcrypt.py", line 380, in detect_wrap_bug
if verify(secret, bug_hash):
File "/app/.venv/lib/python3.12/site-packages/passlib/handlers/bcrypt.py", line 685, in _calc_checksum
hash = _bcrypt.hashpw(secret, config)
ValueError: password cannot be longer than 72 bytes, truncate manually if necessary (e.g. my_password[:72])
Look at the frames: detect_wrap_bug is passlib’s startup self-test, which hashes a built-in string longer than 72 bytes. Your password never reaches bcrypt. Older bcrypt silently truncated that test string; bcrypt 5 rejects it. The error therefore appears on the first hash or verify after the process starts, for every password, including "abc".
Reading a trace like this is mostly a matter of spotting which frame is yours and which is library internals. Pasting it into the Debugly stack trace formatter separates the FastAPI frames from the passlib ones, which makes it obvious that the failing call is inside passlib’s own detection code.
Fix by pinning:
pip install "bcrypt<5"
or by removing passlib, which is the better option.
Replace passlib with bcrypt directly
If you only use the bcrypt scheme, passlib adds nothing:
import bcrypt
def hash_password(password: str) -> str:
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def verify_password(password: str, hashed: str) -> bool:
return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))
Hashes produced by passlib’s bcrypt scheme use the standard $2b$ format, so existing rows in your database keep verifying with bcrypt.checkpw.
Replace passlib with pwdlib and Argon2
The current FastAPI security tutorial uses pwdlib rather than passlib:
pip install "pwdlib[argon2]"
from pwdlib import PasswordHash
password_hash = PasswordHash.recommended()
hashed = password_hash.hash("correct horse battery staple")
assert password_hash.verify("correct horse battery staple", hashed)
This does not verify existing bcrypt hashes. To migrate without forcing a reset, keep verifying old $2b$ hashes with bcrypt.checkpw and re-hash with Argon2 on the next successful login.
A real password longer than 72 bytes
bcrypt only uses the first 72 bytes of its input. Two passwords that share their first 72 bytes produce the same hash, and non-ASCII characters use more than one byte each, so the limit can be reached with fewer than 72 characters. bcrypt 5 raises ValueError here instead of truncating.
Reject or pre-hash such input explicitly. Pre-hashing with SHA-256 and base64 keeps the result under the limit and avoids NUL bytes, which bcrypt does not accept:
import base64
import hashlib
import bcrypt
def _prepare(password: str) -> bytes:
digest = hashlib.sha256(password.encode("utf-8")).digest()
return base64.b64encode(digest) # 44 bytes
def hash_password(password: str) -> str:
return bcrypt.hashpw(_prepare(password), bcrypt.gensalt()).decode("utf-8")
def verify_password(password: str, hashed: str) -> bool:
return bcrypt.checkpw(_prepare(password), hashed.encode("utf-8"))
This changes the hash format of stored passwords, so it cannot be applied to existing users without a migration like the one above. Argon2 has no 72-byte limit and avoids the problem entirely.
Hashing blocks the event loop
Both bcrypt and Argon2 are deliberately slow. Calling them inside an async def endpoint blocks the event loop for the duration of the hash. Declare the endpoint with plain def so FastAPI runs it in a thread pool, or wrap the call:
from fastapi.concurrency import run_in_threadpool
@app.post("/login")
async def login(password: str):
ok = await run_in_threadpool(verify_password, password, stored_hash)
return {"ok": ok}
The same blocking behaviour is covered in FastAPI async and sync blocking. For the Flask side of password hashing, see Flask password hashing mistakes.