hashlib provides a common interface to many secure hash and message digest algorithms, including the SHA-2 family (sha256, sha512), SHA-3, and BLAKE2. It also provides scrypt and pbkdf2_hmac for password hashing. Hashes are one-way: you cannot recover the input from the digest.
One-way fingerprints of data
What you will learn: what a cryptographic hash is, how to compute SHA-256, and how to hash files for integrity checking.
How to read this tab: Remember hashlib works on bytes — encode strings first. The same input always gives the same hash.
A hash function takes any data and produces a fixed-size "fingerprint" — the same input always gives the same fingerprint, but you cannot work backwards from the fingerprint to the input. Hashes verify that data has not changed, and (with special slow functions) store passwords safely.
hashlib needs bytes, not strings. hashlib.sha256("text") raises TypeError. Encode first: hashlib.sha256("text".encode("utf-8")). The encoding affects the result, so be consistent — UTF-8 is the standard choice.
Computing a hash
The core pattern: create a hash object, feed it bytes, then read the digest. Note that hashlib works on bytes, not str — you must encode text first.
import hashlib
# SHA-256 — the most common general-purpose secure hash
text = "The Codex"
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
print(digest) # 64 hex characters — always the same for this input
# e.g. '6c3f...e91a'
# The same input ALWAYS gives the same hash
print(hashlib.sha256(b"hello").hexdigest()
== hashlib.sha256(b"hello").hexdigest()) # True
# A tiny change gives a completely different hash (avalanche effect)
print(hashlib.sha256(b"hello").hexdigest()[:8]) # '2cf24dba'
print(hashlib.sha256(b"Hello").hexdigest()[:8]) # ' 185f8db3' — totally different
# You must pass bytes, not str
# hashlib.sha256("hello") # TypeError: Unicode-objects must be encoded
hashlib.sha256("hello".encode()) # correct — encode to bytes firsthashlib.sha256("text") raises TypeError. Encode the string first: hashlib.sha256("text".encode("utf-8")). Hashes operate on raw bytes, so text must be converted to a byte representation, and the encoding affects the result.
Hash files in chunks, never all at once. Use iter(lambda: f.read(8192), b"") with h.update() to process a large file in fixed-size pieces, keeping memory constant. Python 3.11+ adds hashlib.file_digest() as a built-in shortcut for exactly this.
Hashing files for integrity
import hashlib
# Hash a file in chunks — never load a large file fully into memory
def hash_file(path, algorithm="sha256"):
h = hashlib.new(algorithm)
with open(path, "rb") as f: # binary mode!
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk) # feed data incrementally
return h.hexdigest()
# print(hash_file("video.mp4")) # verify a download matches its published hash
# Python 3.11+ has a built-in helper for exactly this:
# with open("file.bin", "rb") as f:
# digest = hashlib.file_digest(f, "sha256").hexdigest()
# update() can be called repeatedly — order matters
h = hashlib.sha256()
h.update(b"Hello, ")
h.update(b"world!")
print(h.hexdigest() == hashlib.sha256(b"Hello, world!").hexdigest()) # True✅ Beginner tab complete
- I can compute a SHA-256 hash with hexdigest()
- I know hashlib needs bytes, so I encode strings first
- I can hash a file in chunks for integrity verification
- I know a hash is one-way — you cannot reverse it
Password hashing and algorithm choice
What you will learn: why passwords need slow salted hashing (pbkdf2_hmac/scrypt), never plain SHA-256, and how to choose an algorithm.
How to read this tab: The most important rule: never store passwords with plain sha256 — use pbkdf2_hmac with a per-password salt.
Never use plain sha256 for passwords. SHA-256 is fast — billions of guesses per second on a GPU. Passwords need the opposite: a deliberately slow, salted function like pbkdf2_hmac (with a high iteration count) or scrypt. A unique random salt per password defeats rainbow tables.
Password hashing — the right way
Storing passwords requires a deliberately slow hash with a unique salt per password. Never use plain SHA-256 for passwords — it is too fast, making brute-force attacks cheap. Use pbkdf2_hmac or scrypt.
import hashlib, secrets
def hash_password(password):
# A unique random salt per password defeats rainbow tables
salt = secrets.token_bytes(16)
# pbkdf2 with many iterations is deliberately slow
key = hashlib.pbkdf2_hmac(
"sha256",
password.encode("utf-8"),
salt,
600_000, # iteration count — higher = slower = safer
)
return salt + key # store salt alongside the hash
def verify_password(stored, password):
salt = stored[:16] # the salt we prepended
key = stored[16:]
new_key = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 600_000)
return secrets.compare_digest(key, new_key) # constant-time compare
stored = hash_password("correct horse battery staple")
print(verify_password(stored, "correct horse battery staple")) # True
print(verify_password(stored, "wrong password")) # False
# scrypt — memory-hard, even stronger against custom hardware attacks
key = hashlib.scrypt(
b"my password", salt=secrets.token_bytes(16),
n=2**14, r=8, p=1, dklen=32)SHA-256 is designed to be fast — billions of guesses per second on a GPU. For passwords you want the opposite: a slow, salted function like pbkdf2_hmac or scrypt. For production, a dedicated library implementing Argon2 or bcrypt is often preferable, but pbkdf2_hmac is solid and built in.
sha256 for integrity, pbkdf2/scrypt for passwords, never md5/sha1 for security. SHA-256 and BLAKE2 are the right general-purpose choices. MD5 and SHA-1 have known collision attacks and survive only as fast checksums for detecting accidental corruption.
Choosing an algorithm
| Algorithm | Use for | Notes |
|---|---|---|
sha256 / sha512 | General integrity, fingerprints | Fast, secure, the default choice |
blake2b / blake2s | High-speed hashing | Faster than SHA-2, equally secure |
sha3_256 | Integrity (different design) | SHA-3 family, distinct from SHA-2 |
pbkdf2_hmac / scrypt | Passwords | Deliberately slow + salted |
md5 / sha1 | Legacy / non-security checksums only | Broken — never use for security |
✅ Intermediate tab complete
- I never use plain sha256 for passwords
- I use pbkdf2_hmac or scrypt with a unique random salt per password
- I store the salt alongside the hash and compare with compare_digest
- I know md5 and sha1 are broken and only for non-security checksums
Salts, peppers, HMAC, and BLAKE2
What you will learn: how salts and peppers work, HMAC for authenticated hashing, BLAKE2 keyed hashing, and the CPU-hard vs memory-hard distinction.
How to read this tab: Read when implementing authentication or message authentication — the pbkdf2 vs scrypt vs Argon2 trade-off matters.
Salts, peppers, HMAC, and BLAKE2 keyed hashing
A salt is a unique random value stored alongside each password hash; it ensures two users with the same password get different hashes and defeats precomputed rainbow tables. A pepper is a secret value (not stored in the database, kept in application config) mixed into the hash, adding a layer that survives a database-only breach. For message authentication — verifying both integrity and authenticity — HMAC combines a hash with a secret key.
import hashlib, hmac, secrets
# HMAC — authenticated hash, proves the data came from someone with the key
key = secrets.token_bytes(32)
message = b"transfer $100 to account 12345"
signature = hmac.new(key, message, hashlib.sha256).hexdigest()
# The recipient recomputes HMAC with the shared key and compares
print(hmac.compare_digest(
signature,
hmac.new(key, message, hashlib.sha256).hexdigest())) # True
# BLAKE2 supports keyed hashing natively (a built-in MAC)
mac = hashlib.blake2b(message, key=key).hexdigest()
# Also supports 'person' and 'salt' parameters for domain separation
h = hashlib.blake2b(b"data", salt=b"16-byte-salt----",
person=b"app-v1__________")
# Available algorithms on this platform
print(hashlib.algorithms_guaranteed) # always available everywhere
print(hashlib.algorithms_available) # platform-dependent extras
# usedforsecurity flag (3.9+) — mark a hash as non-security
# Lets MD5 work in FIPS-restricted builds for checksums only
h = hashlib.md5(b"data", usedforsecurity=False) # checksum, not security
# Length matters: sha256 -> 32 bytes, sha512 -> 64 bytes
print(hashlib.sha256().digest_size) # 32
print(hashlib.sha512().digest_size) # 64The choice between password-hashing functions involves a deliberate cost trade-off. pbkdf2_hmac is CPU-hard: its iteration count sets how many hash rounds an attacker must perform per guess. scrypt is additionally memory-hard, requiring large amounts of RAM per guess, which defeats the cheap parallelism of GPUs and custom ASICs. The modern recommendation for new systems is Argon2 (not in the standard library — available via the argon2-cffi package), which is memory-hard and won the Password Hashing Competition; but scrypt and a high-iteration pbkdf2_hmac remain strong, standards-based choices that ship with Python.
✅ Expert tab complete
- I know a salt is per-password and stored; a pepper is secret and global
- I can use hmac for authenticated message hashing
- I know pbkdf2 is CPU-hard while scrypt is also memory-hard
- I know Argon2 (third-party) is the modern recommendation