argon2-cffi review
argon2-cffi is a Python interface for hashing passwords with Argon2id. Its high-level `PasswordHasher` creates a self-describing string that records the algorithm version, salt, memory use, iteration count, parallelism, and digest. Verification raises a distinct exception for a mismatch, and `check_needs_rehash()` identifies a valid hash made under an older policy. Release 25.1.0 recognizes Python 3.13 and 3.14, lets the rehash check accept bytes, and improves parameter handling on Pyodide and WebAssembly. Our Python 3.12 import succeeded, and the distribution includes typing metadata.
argon2-cffi 25.1.0 installed in 0.4 seconds, occupied 2 MB across 4 packages, imported in 0.13 seconds, and produced no audit findings in our sandbox. It is a good password-hashing dependency when the service can benchmark its Argon2id policy and place a hard limit on concurrent checks.
We installed it
| Install | ✓ · 0.4s | 4 packages on disk · 2 MB |
| Import | ✓ | import argon2 in 0.13s · pure Python · py.typed · requires Python >=3.8 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does argon2-cffi install cleanly?
Yes. In a fresh container with an empty cache, pip install argon2-cffi finished in 0.4s, leaving 4 packages and 2 MB on disk. pip-audit reported no known vulnerabilities.
What does argon2-cffi need to run?
Python >=3.8, and nothing compiled: it is pure Python. In our run import argon2 succeeded in 0.13s, and the package ships py.typed for type checkers.
argon2-cffi or bcrypt: which should you use?
bcrypt: Choose it when an established bcrypt database and compatibility requirements rule out an immediate Argon2 migration. argon2-cffi 25.1.0 installed in 0.4 seconds, occupied 2 MB across 4 packages, imported in 0.13 seconds, and produced no audit findings in our sandbox.
When should you not use argon2-cffi?
Memory is too tight to reserve the selected memory_cost for every concurrent verification; reducing the setting without testing removes much of Argon2's benefit
Use it if
- Your login service needs Argon2id hashes whose salt and cost settings travel with each database value
- You can benchmark the chosen profile on production hardware and limit simultaneous password checks
- Successful logins can upgrade older hashes whenever the application's cost policy changes
- You want the RFC 9106 profiles available through a short Python API without formatting hashes by hand
- Memory is too tight to reserve the selected `memory_cost` for every concurrent verification; reducing the setting without testing removes much of Argon2's benefit
- The application needs one verifier for Argon2, bcrypt, and PBKDF2 records; use a policy layer such as pwdlib for a mixed database
- Django already controls password hashing through `PASSWORD_HASHERS`; configure its Argon2 hasher instead of maintaining a second policy object
- Your async request path cannot send hashing to a bounded worker pool; the synchronous call consumes CPU and memory while it runs
- You also need tokens, encryption, sessions, or signing from the same package; argon2-cffi only handles Argon2 hashing and low-level derivation
Setup reality
We installed argon2-cffi 25.1.0 in a clean Python 3.12 Bookworm container in 0.4 seconds. The environment ended with 4 packages using 2 MB, and pip-audit reported 0 known vulnerabilities. The package has 1 direct dependency, requires Python 3.8 or newer, ships py.typed, and import argon2 took 0.13 seconds. Our metadata check classified the top-level distribution as pure Python; the compiled work lives in its bindings dependency.
No service account or configuration file is involved. Create one PasswordHasher for the application's policy and store the entire encoded value returned by hash(). A bad password raises VerifyMismatchError rather than returning False; malformed database content raises InvalidHashError. Choose the exception handling before wiring the call into a login endpoint.
The direct dependency argon2-cffi-bindings carries the CFFI layer and the Argon2 implementation. Common platforms receive wheels, while an unusual platform or forced source install can need a compiler and libffi headers. Version 25.1.0 supports WebAssembly more carefully, but that runtime requires parallelism of 1. An explicitly unsupported value fails when PasswordHasher is constructed.
Run python -m argon2 on the same instance class used by the service, then test several verifications at once. Each operation uses its configured memory allocation. In an async server, a dedicated executor with a fixed worker count keeps those calls off the event loop and caps simultaneous allocations. After a password verifies, call check_needs_rehash() and replace the stored value while the plaintext is still available.
Patterns
Store one complete Argon2 string hash-password
from argon2 import PasswordHasher
ph = PasswordHasher()
stored = ph.hash("correct horse battery staple")
# Save `stored` exactly as returned.
assert ph.verify(stored, "correct horse battery staple")The returned string already contains the salt, Argon2 version, cost settings, and digest. Keep one hasher instance with the chosen policy instead of rebuilding it inside every request.
Separate a wrong password from damaged data handle-mismatch
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerificationError, VerifyMismatchError
ph = PasswordHasher()
def valid(stored: str, password: str) -> bool:
try:
return ph.verify(stored, password)
except VerifyMismatchError:
return False
except InvalidHashError:
log.error("invalid password hash in database")
return False
except VerificationError:
log.exception("argon2 verification failed")
return False`verify()` signals an ordinary mismatch with `VerifyMismatchError`. Catch it before the broader verification exception, and log an invalid encoded value as a storage or migration problem.
Upgrade a valid hash during login rehash-after-login
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(time_cost=4, memory_cost=131072)
def login(user, password):
try:
ph.verify(user.password_hash, password)
except VerifyMismatchError:
return None
if ph.check_needs_rehash(user.password_hash):
user.password_hash = ph.hash(password)
user.save()
return userCheck for a rehash only after verification passes. That point in the request is when the application has both a trusted old hash and the plaintext needed to create its replacement.
Use a named RFC 9106 parameter set select-profile
from argon2 import PasswordHasher, profiles
ph = PasswordHasher.from_parameters(profiles.RFC_9106_LOW_MEMORY)
high_memory = PasswordHasher.from_parameters(profiles.RFC_9106_HIGH_MEMORY)The low-memory profile uses 64 MiB per operation, while the high-memory profile calls for 2 GiB. Pick from measured server capacity and expected concurrency, not from the profile name alone.
Measure the policy on its deployment hardware benchmark-policy
$ python -m argon2
$ python -m argon2 -m 131072 -t 4 -n 50
$ python -m argon2 --profile RFC_9106_HIGH_MEMORYThe command times the selected settings on the current machine. Repeat the test inside the production container and follow it with concurrent login load, since isolated latency does not show shared memory pressure.
Inventory cost settings in stored rows inspect-parameters
from collections import Counter
from argon2 import extract_parameters
from argon2.exceptions import InvalidHashError
def memory_cost(encoded: str):
try:
return extract_parameters(encoded).memory_cost
except InvalidHashError:
return None
counts = Counter(memory_cost(value) for value in password_hashes)`extract_parameters()` reads the settings embedded in an Argon2 value without verifying a password. A bcrypt string or corrupt row raises `InvalidHashError`, so mixed stores need format detection or explicit error handling.
Bound verification outside the event loop limit-async-work
import asyncio
from concurrent.futures import ThreadPoolExecutor
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher()
pool = ThreadPoolExecutor(max_workers=4)
async def verify(stored, password):
loop = asyncio.get_running_loop()
try:
return await loop.run_in_executor(pool, ph.verify, stored, password)
except VerifyMismatchError:
return FalseA fixed four-worker pool prevents an unbounded number of synchronous checks from blocking the event loop or allocating memory together. Set the real limit from the selected profile and the container budget.
Hash work for an unknown account mask-unknown-user
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher()
DUMMY_HASH = ph.hash("never-a-real-password")
def authenticate(email, password):
user = db.get_user(email)
target = user.password_hash if user else DUMMY_HASH
try:
ok = ph.verify(target, password)
except VerifyMismatchError:
ok = False
return user if user and ok else NoneChecking a prebuilt dummy value keeps unknown-email requests on the same expensive verification path. Generate that dummy once during startup; hashing a fresh dummy for each miss doubles work on an attacker-controlled path.
Produce raw bytes for a key derivation use case derive-key-bytes
import os
from argon2.low_level import Type, hash_secret_raw
salt = os.urandom(16)
key = hash_secret_raw(secret=b"passphrase", salt=salt, time_cost=3, memory_cost=65536, parallelism=4, hash_len=32, type=Type.ID)Raw output has no encoded header. Store the salt and all five derivation settings next to the protected data, or the same key cannot be reproduced later. Password tables are safer with `PasswordHasher` strings.
Put Argon2 first in Django's hasher list configure-django
# pip install "django[argon2]"
PASSWORD_HASHERS = [
"django.contrib.auth.hashers.Argon2PasswordHasher",
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
]Django uses the first entry for new hashes and keeps later entries for existing users. Tune a subclass of Django's `Argon2PasswordHasher` if costs must change, so the framework remains the single policy owner.
Replace bcrypt only after it verifies migrate-bcrypt
import bcrypt
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher()
def check_and_upgrade(user, password: str) -> bool:
stored = user.password_hash
if stored.startswith("$argon2"):
try:
ph.verify(stored, password)
except VerifyMismatchError:
return False
return True
if bcrypt.checkpw(password.encode(), stored.encode()):
user.password_hash = ph.hash(password)
user.save()
return True
return FalseArgon2 verification cannot read a bcrypt value. Dispatch from the encoded prefix, verify the old scheme, and write Argon2 only after success so a failed login never alters the stored credential.
Set WebAssembly parallelism to one support-wasm
import dataclasses
from argon2 import PasswordHasher, profiles
wasm_params = dataclasses.replace(profiles.RFC_9106_LOW_MEMORY, parallelism=1)
ph = PasswordHasher.from_parameters(wasm_params)Version 25.1.0 improves platform detection, yet WebAssembly still supports parallelism of 1. Use the same explicit parameter set on every runtime if newly created hashes should avoid an immediate rehash elsewhere.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| bcrypt | PyPI | Choose it when an established bcrypt database and compatibility requirements rule out an immediate Argon2 migration. |
| pwdlib | PyPI | Choose it when one policy object must verify several password-hash schemes during a gradual upgrade. |
| scrypt | PyPI | Choose it when stored records and surrounding systems already use scrypt and need a focused Python binding. |
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.

