passlib review
Passlib 1.7.4 presents more than 30 password-hash formats through common hash, verify, identify, and upgrade operations. CryptContext can read several historical formats, write new passwords with the preferred scheme, and return a replacement hash after a successful login. That makes it most useful during long account migrations. Our Python 3.12 install was pure Python and imported quickly, but the current PyPI release has not changed since October 2020 and declares no Python version range.
Passlib 1.7.4 installed in 0.2 seconds and used 3 MB in our sandbox, with zero audit findings but no py.typed marker. Keep it for controlled multi-format migrations; do not choose its unchanged 2020 release for a new single-scheme password store.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 3 MB |
| Import | ✓ | import passlib in 0.07s · pure Python |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does passlib install cleanly?
Yes. In a fresh container with an empty cache, pip install passlib finished in 0.2s, leaving 1 package and 3 MB on disk. pip-audit reported no known vulnerabilities.
What does passlib need to run?
Python 3.x, and nothing compiled: it is pure Python. In our run import passlib succeeded in 0.07s.
passlib or pwdlib: which should you use?
pwdlib: Use it for a maintained password API centered on current schemes and automatic upgrades. Passlib 1.7.4 installed in 0.2 seconds and used 3 MB in our sandbox, with zero audit findings but no py.typed marker.
When should you not use passlib?
You are designing a new password store. pwdlib or argon2-cffi has a narrower modern purpose and newer releases.
Use it if
- One account table contains several legacy hash formats that must remain verifiable during migration.
- A successful login should upgrade an obsolete scheme or cost and return the replacement for storage.
- An import job needs to classify configured Django, Unix, LDAP, PBKDF2, bcrypt, or Argon2 encodings.
- A mature application already has a tested CryptContext whose replacement would create migration risk.
- You are designing a new password store. pwdlib or argon2-cffi has a narrower modern purpose and newer releases.
- Security policy requires recent upstream releases. Passlib 1.7.4 was published on 2020-10-08.
- Supported Python versions must be declared in package metadata. Passlib leaves Requires-Python empty.
- First-party typing is mandatory. Our installed distribution had no py.typed marker.
- Only one modern hash format exists in the database. A library covering dozens of old formats adds review surface without migration value.
Setup reality
We installed passlib 1.7.4 in a fresh Python 3.12 Bookworm sandbox in 0.2 seconds. One installed package used 3 MB. pip-audit reported zero known vulnerabilities. Our metadata inspection counted six direct dependencies, found pure Python code, an unspecified Python requirement, a BSD license, and no py.typed marker. import passlib completed in 0.07 seconds.
The base import does not prove a chosen handler can hash. Argon2 needs argon2-cffi, bcrypt uses its bcrypt backend, and encrypted TOTP storage needs cryptography through the relevant extra. Run one hash and one verification in deployment checks. The 2020 release predates several current Python and backend versions, so pin and test the whole resolved environment.
CryptContext order defines policy: the first active scheme writes new hashes, and deprecated auto marks older configured schemes for replacement. verify_and_update returns validity plus an optional new hash but never saves it. Persist the replacement inside the authenticated login transaction, account for simultaneous logins, and expose the same public failure for an unknown user and a wrong password.
identify only recognizes handlers configured on that context. None may mean damage or a valid unconfigured scheme, so quarantine those records during imports. Standard bcrypt processes only its first 72 input bytes; truncate_error can reject longer inputs, while Passlib's bcrypt_sha256 prehash format is not interoperable with plain bcrypt tools. Benchmark cost parameters on deployment hardware before raising them.
Patterns
Write Argon2 while accepting one legacy scheme configure-hash-policy
passwords = CryptContext(schemes=['argon2', 'pbkdf2_sha256'], deprecated='auto')
stored = passwords.hash(plain_password)The first non-deprecated scheme receives new hashes. Argon2 also requires its optional backend.
Check plaintext against a stored value verify-password
if not passwords.verify(submitted_password, user.password_hash):
raise InvalidCredentials()Plaintext is the first argument. Handle a malformed stored value separately in internal logs.
Persist a replacement after authentication upgrade-on-login
valid, replacement = passwords.verify_and_update(password, user.password_hash)
if not valid: raise InvalidCredentials()
if replacement is not None:
user.password_hash = replacement
session.commit()verify_and_update returns the new hash but never writes it to the user record.
Classify a configured hash identify-format
scheme = passwords.identify(stored_hash)
if scheme is None:
quarantine_record(stored_hash)None covers both damaged text and valid formats absent from this context, so migration code should retain the ambiguity.
Flag an outdated stored policy check-upgrade-needed
if passwords.needs_update(user.password_hash):
schedule_rehash_at_next_login(user.id)A new hash still needs the user's plaintext, so the usual replacement point is the next successful login.
Stop bcrypt from truncating input reject-long-bcrypt
strict_bcrypt = bcrypt.using(rounds=12, truncate_error=True)
stored = strict_bcrypt.hash(password)bcrypt's 72-byte limit otherwise discards the remaining input; benchmark the example cost on your hardware.
Spend hash work for a missing account hide-user-enumeration
user = find_user(username)
if user is None:
passwords.dummy_verify()
raise InvalidCredentials()dummy_verify reduces the timing difference between an unknown account and a wrong password for a real one.
Save an htpasswd credential update-htpasswd
users = HtpasswdFile('/etc/nginx/private.htpasswd', new=False)
users.set_password('ada', new_password)
users.save()save rewrites the file; serialize competing writers and keep restrictive permissions on the hash file.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pwdlib | PyPI | Use it for a maintained password API centered on current schemes and automatic upgrades. |
| argon2-cffi | PyPI | Use it directly when Argon2id is the only format and legacy identification is unnecessary. |
| bcrypt | PyPI | Use it when an existing policy or database fixes bcrypt as the sole hash format. |
More security guides
cryptography · pyjwt · jose · requests-oauthlib · oauthlib · dompurify · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

