How does Django store a user's password, and what role does the order of the PASSWORD_HASHERS setting play?
answer
- never the password itself
- four fields joined by dollar signs
- the prefix picks the verifier
- first entry wins for new hashes
- iteration count raised in 6.1
basics
~10 sDjango stores algorithm$parameters$salt$hash, by default pbkdf2_sha256 with 1,500,000 iterations in 6.1. The first PASSWORD_HASHERS entry hashes new passwords; the others only verify existing hashes, chosen by the stored prefix.
solid answer
~40 s`set_password()` calls `make_password()`, which asks the preferred hasher to encode the password with a fresh random salt. With the default settings the `password` column holds `pbkdf2_sha256$1500000$<salt>$<base64 hash>`; Django 6.1 raised the PBKDF2 default from 1,200,000 to 1,500,000 iterations. `PASSWORD_HASHERS` lists the hashers the project trusts: the **first** entry is used for every new hash and is the target of upgrades, and the rest exist only so existing hashes can still be verified. Verification does not try them in turn; `identify_hasher()` reads the algorithm name before the first `$` and loads that hasher. If it is not listed, the check simply fails. To prefer Argon2 you install `argon2-cffi` and put `Argon2PasswordHasher` first, keeping the PBKDF2 hashers below it.
code
python · 8 lines# settings.py - prefer Argon2id, keep the defaults so existing hashes still verify
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
'django.contrib.auth.hashers.ScryptPasswordHasher',
]go deeper
Recall that Django stores algorithm, parameters, salt and hash in one string, and that set_password() hashes but does not save.
Explain that the first PASSWORD_HASHERS entry hashes new passwords, the prefix selects the verifier, and a missing hasher makes those logins fail.
Plan hasher changes safely: add the new hasher first, keep the old ones listed until no hashes use them, and watch the 6.1 iteration bump roll through on login.
Weigh Argon2's memory hardness against the extra native dependency and the server cost per login, and set a policy for when work factors are raised.
## What ends up in the password column Django never stores a password or a reversible form of it. `AbstractBaseUser.set_password(raw)` calls `django.contrib.auth.hashers.make_password()`, which: 1. picks the **preferred hasher**, the first class in `PASSWORD_HASHERS`; 2. generates a random salt with at least 128 bits of entropy; 3. calls the hasher's `encode()`, which returns a self-describing string. For the default `PBKDF2PasswordHasher` in Django 6.1 that string is: ```text pbkdf2_sha256$1500000$<salt>$<base64 of the derived key> ``` - **algorithm** - `pbkdf2_sha256`, the hasher's `algorithm` attribute; - **work factor** - the iteration count used for this particular hash; - **salt** - random per password, so equal passwords hash differently; - **hash** - the derived key. Because each hash records its own algorithm and parameters, Django can verify old hashes after the defaults change. `set_password()` does not save; the caller must call `save()`. `set_unusable_password()` stores `!` followed by 40 random characters, which no hasher will ever match. ## The default PASSWORD_HASHERS | Position | Hasher | Algorithm prefix | Extra dependency | |---|---|---|---| | 1 (preferred) | `PBKDF2PasswordHasher` | `pbkdf2_sha256` | none | | 2 | `PBKDF2SHA1PasswordHasher` | `pbkdf2_sha1` | none | | 3 | `Argon2PasswordHasher` | `argon2` | `argon2-cffi` | | 4 | `BCryptSHA256PasswordHasher` | `bcrypt_sha256` | `bcrypt` | | 5 | `ScryptPasswordHasher` | `scrypt` | none (uses the standard library) | The comment above the setting in `global_settings.py` states the rule: the first hasher is the preferred algorithm, and passwords using the others are converted automatically on login. ## How verification picks a hasher `check_password()` calls `verify_password()`, which calls `identify_hasher(encoded)`. That function reads the text before the first `$` and asks `get_hasher()` for the matching class **among the hashers listed in the setting**. So: - the list is not tried in order; the prefix selects exactly one hasher; - a hash whose algorithm is missing from the list cannot be verified: `get_hasher()` raises `ValueError`, `verify_password()` treats it as a failed check (after running the default hasher once so the timing looks normal), and the user cannot sign in; - removing an entry from the list is therefore a breaking change for every user still stored with it. After a successful check, if the stored hash used a different algorithm from the preferred one, or the preferred hasher's `must_update()` says its parameters are stale, the password is re-hashed and saved. ## Choosing the preferred hasher - **PBKDF2-SHA256** is the default because it needs nothing beyond Python. Django raises the iteration count in new releases (1,200,000 in 6.0, 1,500,000 in 6.1), and existing hashes are upgraded as users log in. - **Argon2** is the algorithm the Django docs point to as the Password Hashing Competition's recommendation. Install it with `python -m pip install django[argon2]` and list `Argon2PasswordHasher` first; Django uses the Argon2id variant with `time_cost=2`, `memory_cost=102400` and `parallelism=8` by default. - **bcrypt** is supported through `BCryptSHA256PasswordHasher`, which pre-hashes with SHA-256 to avoid bcrypt's input length limit. - **scrypt** is memory-hard and needs no extra package. To tune a work factor, subclass the hasher, change the attribute (for example `iterations`), and list your subclass first. ## Removed hashers Django 5.1 removed `SHA1PasswordHasher`, `UnsaltedSHA1PasswordHasher` and `UnsaltedMD5PasswordHasher`. Salted `MD5PasswordHasher` still exists for importing legacy data and for fast test suites, but it is not in the default list and should never be first in production.
- Why does Django run a hasher even when the stored hash cannot be identified or the user does not exist?To keep response times similar. `verify_password()` hashes a random string with the default hasher when the stored value is unusable or its algorithm is unknown, and `ModelBackend` hashes the submitted password when no user matches. Without that, a fast failure would reveal which accounts exist or which ones have unusable passwords.
- How do you make a test suite that creates many users faster without touching production settings?Override `PASSWORD_HASHERS` in the test settings with `['django.contrib.auth.hashers.MD5PasswordHasher']`. Tests that create users or log in then skip the deliberately slow key derivation. The weak hasher never reaches production because only the test settings module lists it.
saying these in an interview costs you the question
- Django encrypts passwords and can decrypt them for admins
- Django tries every hasher in PASSWORD_HASHERS until one matches
- Removing an old hasher from the list forces users to re-hash on login
- Argon2 is Django's default hasher out of the box
- set_password() saves the user to the database immediately