security/password-hashing-why-a-website-never-stores-your-password-and-what-a-s.md
Password hashing: why a website never stores your password, and what a salt changes
Websites should never store your password, only a hash made from it. But two users with the same password get the same hash, and a stolen table can be checked against a list of common passwords in one pass.
This article shows what a salt changes, why a fast hash is the wrong tool for passwords, and what to use instead. You can run the attacker yourself on a small stolen database, with salt off and on.

When you sign up for a website and choose a password, the site should never keep that password. It should keep something else, made from it. This article shows what that something is, why two identical passwords should not give two identical records, and how you can see the difference yourself.
A fingerprint, not a copy
A hash function is a recipe that turns any text into a fixed-length string of letters and digits, called a hash (or digest). Three properties matter here. The same input always gives the same hash. A tiny change in the input gives a completely different hash. And there is no recipe for going backwards from the hash to the input.
Python has several hash functions built in. This one is called SHA-256:
import hashlib
print(hashlib.sha256(b"password").hexdigest())
print(hashlib.sha256(b"Password").hexdigest())
Real output:
5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
e7cf3ef4f17c3999a94f2c6f612e8a888e5b1026878e4e19398b23bd38ec221a
One capital letter, and every character of the result changed. So a website can store the hash. When you log in, it hashes what you typed and compares the two hashes. If they match, you knew the password, and the site never needed to keep the password itself.
Why a stolen database is still a problem
Databases do get stolen. Suppose an attacker has the table of hashes. They cannot run the recipe backwards, but they do not need to. They can guess: hash a list of common passwords (123456, qwerty, sunshine...) and look for matches. The list is called a wordlist, and a match means the password is found.
Worse, the attacker can do this once and reuse it. Hash every word in the list a single time, then check every user in the table against that one set of results. And if two users picked the same password, they have the same hash, so the table itself shows who matches whom, before any guessing happens.
Salt: make every hash different
A salt is a random value, different for every user, stored right next to the hash. It is mixed into the password before hashing. It is not a secret. Its job is only to make sure that the same password gives a different hash for each person.
import hashlib
salt = "a3f91c07" # random, one per user
print(hashlib.sha256((salt + "sunshine").encode()).hexdigest()[:16])
Real output:
50bde24e44ba62c8
Without the salt, sunshine starts a941a4c4fd0c01cd. With it, 50bde24e44ba62c8. A different salt would give yet another result. Now the attacker's one-time shortcut is gone: a guess must be hashed again with each user's own salt.
Try it: a stolen database
Run the attacker with salt off. They hash each of the 12 words once (12 hashes) and three of the four users fall at once. Turn salt on and run it again. Every word must now be hashed separately for each user, so the work is 12 × 4 = 48 hashes, and the repeated sunshine no longer shows up in the table. With only 12 words that is still easy. With a list of a billion words, it is the difference between a billion hashes and four billion, and it grows with every extra user.
Why fast is the problem
SHA-256 was designed to be fast, which is good for checking files and bad for passwords: a fast recipe lets an attacker make guesses quickly. On the laptop used to write this article, one SHA-256 took about 0.8 microseconds, so roughly a million guesses per second from a single core, and dedicated hardware is far quicker.
Password storage should use a recipe that is deliberately slow and repeats its work. Python includes one called PBKDF2:
import hashlib
h = hashlib.pbkdf2_hmac("sha256", b"sunshine", salt.encode(), 600_000)
print(h.hex()[:16])
(Using the same salt as before.) Real output (and it took about 0.37 seconds on the same laptop):
34a34f076252d108
The 600,000 means "repeat the hash that many times". One login waits a third of a second, which nobody notices. An attacker making billions of guesses notices very much. As of 2026, the OWASP password storage cheat sheet lists Argon2id as the first choice, with scrypt, bcrypt and PBKDF2 as other options, and it gives 600,000 as the PBKDF2-HMAC-SHA256 iteration count. Argon2id is also designed to need a lot of memory, which makes cheap special hardware less useful.
So what, for your own code
If you ever build a login, do not invent a scheme. Use a well-known library for Argon2id, bcrypt or scrypt in your language. Most such libraries create the salt for you and store it inside the result string, so there is nothing extra to manage. Never store the password itself, never use a plain fast hash on its own, and never write your own recipe.
And as a user: a long, unique password is much harder to guess than a short common one, however well the site stores it. A password manager makes that easy.