mrkeyoor.com_
Sun 20 Sept 11:48 UTC
PyPISecurityupdated 20 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed argon2-cffiScreenshot of argon2-cffi documentation
Install✓ · 0.4s4 packages on disk · 2 MB
Importimport argon2 in 0.13s · pure Python · py.typed · requires Python >=3.8
Known vulns0(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

API stability5/5Version 25.1.0 keeps the established `PasswordHasher.hash()`, `verify()`, and `check_needs_rehash()` path intact. The release widens the rehash input to bytes and adjusts WebAssembly compatibility instead of changing stored-hash semantics. Old encoded strings retain their parameters, so an application can verify them first and decide whether its current hasher would replace them.
Docs5/5The official documentation opens with a complete hash, verify, mismatch, and rehash example, then separates high-level use, parameter profiles, low-level functions, installation, exceptions, and the command-line benchmark. It also warns that defaults may consume 64 MB and cause swapping in restricted containers, a concrete deployment concern that many password-library quick starts omit.
Maintenance5/5PyPI published 25.1.0 on June 3, 2025, and GitHub recorded a push on August 20, 2026. The repository is not archived and currently shows 1 combined open issue and pull request. Its changelog names supported Python releases, WebAssembly changes, deprecations, and removal dates, so maintenance policy is visible even when releases are infrequent.
Ecosystem4/5The stored registry snapshot reports 17,361,474 weekly downloads, while GitHub reports 729 stars. Django exposes an Argon2 password hasher backed by this package, and higher-level policy packages can place it beside older schemes. The scope remains intentionally narrow: account flows, rate limits, sessions, tokens, and multi-scheme dispatch have to come from the application or another library.

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
Skip it if

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 user

Check 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_MEMORY

The 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 False

A 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 None

Checking 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 False

Argon2 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

PackageRegistryPick it when
bcryptPyPIChoose it when an established bcrypt database and compatibility requirements rule out an immediate Argon2 migration.
pwdlibPyPIChoose it when one policy object must verify several password-hash schemes during a gradual upgrade.
scryptPyPIChoose 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.