Hashing with hashlib
Reviewed & published by Brayan K
Hashing turns any data into a short, fixed-size fingerprint called a digest, and hashlib is Python's standard module of one-way hash functions for producing it.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Verify a download, detect a changed file, deduplicate data, or build the foundation for password storage — all from a few lines using the standard library.
What You'll Learn in This Lesson
- Hashing bytes with sha256 and md5
- Using .update() and .hexdigest()
- Why strings must be .encode()d first
- Hashing large files in chunks
- Why hashing is NOT encryption
- Hashing passwords safely with salt + pbkdf2_hmac
1 Your First Digest: sha256 and hexdigest
Feed bytes to a hash constructor and call .hexdigest() to get the fingerprint as a hex string. The same input always yields the same digest:
import hashlib
digest = hashlib.sha256(b"hello").hexdigest()
print(digest)
# 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
# md5 is faster but cryptographically broken — fine for checksums,
# never for security:
print(hashlib.md5(b"hello").hexdigest())
# 5d41402abc4b2a76b9719d911017c592Hash functions work on bytes, not text. To hash a normal string you must encode it first — almost always with UTF-8:
import hashlib
text = "hello"
# hashlib.sha256(text) -> TypeError: Unicode-objects must be encoded
digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
print(digest)
# 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 (same as b"hello")import hashlib
# Hash bytes directly
print(hashlib.sha256(b"hello").hexdigest()[:16]) # 2cf24dba5fb0a30e
# Hash a string by encoding it first (same result as b"hello")
text = "hello"
print(hashlib.sha256(text.encode("utf-8")).hexdigest()[:16]) # 2cf24dba5fb0a30e
# md5 for a quick (non-secure) checksum
print(hashlib.md5(b"hello").hexdigest()) # 5d41402abc4b2a76b9719d911017c592
# ✅ Expected output:
# 2cf24dba5fb0a30e
# 2cf24dba5fb0a30e
# 5d41402abc4b2a76b9719d911017c5922 Feeding Data with .update() and Hashing Files in Chunks
You do not have to hand the hasher everything at once. Create an empty hash object and feed it with .update() as many times as you like — the result is identical to hashing the concatenation in one go:
import hashlib
h = hashlib.sha256()
h.update(b"hello")
h.update(b" world") # feed in pieces
print(h.hexdigest())
# b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9
# Identical to hashing it all at once:
print(hashlib.sha256(b"hello world").hexdigest())
# b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9This is exactly how you hash a large file without loading it all into memory — read it in fixed-size chunks and update as you go:
import hashlib
def hash_file(path, chunk_size=8192):
h = hashlib.sha256()
with open(path, "rb") as f: # "rb" = read bytes
for chunk in iter(lambda: f.read(chunk_size), b""):
h.update(chunk) # one chunk at a time
return h.hexdigest()
# print(hash_file("video.mp4")) # works on a multi-gigabyte file
# using only a few kilobytes of memoryThe iter(callable, sentinel) trick keeps calling f.read(chunk_size) until it returns the empty bytes b"" at end of file. Memory use stays flat no matter how big the file is.
import hashlib
# Simulate "file" data and hash it in 4-byte chunks
data = b"the quick brown fox"
def hash_in_chunks(blob, size=4):
h = hashlib.sha256()
for i in range(0, len(blob), size):
h.update(blob[i:i + size])
return h.hexdigest()
chunked = hash_in_chunks(data)
at_once = hashlib.sha256(data).hexdigest()
print(chunked[:16]) # 9ecb36561341d18e
print(at_once[:16]) # 9ecb36561341d18e
print("match:", chunked == at_once) # match: True
# ✅ Expected output:
# 9ecb36561341d18e
# 9ecb36561341d18e
# match: True3 Hashing Is Not Encryption — and Passwords Need a KDF
A digest is a one-way fingerprint: there is no key and no way to recover the original input. That is the opposite of encryption, which is designed to be reversed. So hashing is great for verifying data but useless for storing data you need back:
import hashlib
# Verify a download matches a published checksum
expected = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
downloaded = b"hello"
actual = hashlib.sha256(downloaded).hexdigest()
print("file is intact:", actual == expected) # file is intact: TrueFor passwords, a bare sha256 is dangerous: it is fast (so guessable in bulk) and unsalted (so identical passwords share a digest). The standard fix is a salt plus a slow key-derivation function. hashlib ships one — pbkdf2_hmac:
import hashlib, secrets
password = "correct horse battery staple"
salt = secrets.token_bytes(16) # a unique random salt PER user
# Derive a slow, salted key. 200_000 rounds makes brute force expensive.
key = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 200_000)
# Store BOTH the salt and the key. To check a login, repeat the derivation
# with the stored salt and compare with secrets.compare_digest.
print(len(key)) # 32 (a 256-bit derived key)import hashlib
# A fixed salt here ONLY so the output is reproducible in this demo.
# Real code must use a unique random salt per user (secrets.token_bytes).
salt = b"somesalt"
key = hashlib.pbkdf2_hmac("sha256", b"secret", salt, 100_000)
print(key.hex()[:16]) # 972bbb1b2f7c1d73
print("key length:", len(key)) # key length: 32
# Same password + same salt -> same key (that is how login checks work)
again = hashlib.pbkdf2_hmac("sha256", b"secret", salt, 100_000)
print("verifies:", key == again) # verifies: True
# Expected output:
# 972bbb1b2f7c1d73
# key length: 32
# verifies: True🎯 Your Turn: Fill in the Blanks
Complete the program so it hashes a string correctly. Replace each ___ and match the expected output.
# 🎯 YOUR TURN: complete the string hash
import hashlib
text = "password123"
# 1. Turn the string into bytes before hashing:
data = text.___("utf-8")
# 2. Produce the hex digest with sha256:
digest = hashlib.sha256(data).___()
print(digest[:16]) # ef92b778bafe771e
# Expected output:
# ef92b778bafe771eFirst blank: encode — text.encode("utf-8") converts the string to bytes.
Second blank: hexdigest — returns the digest as a hex string.
Common Errors to Avoid
❌ Hashing a str without encoding it
hashlib.sha256("hello") # TypeError: Unicode-objects must be encoded✅ Encode first: hashlib.sha256("hello".encode("utf-8")).
❌ Treating a hash like reversible encryption
# There is no "unhash" — you cannot get "hello" back from the digest✅ Hashing is one-way. If you need the data back, use encryption (e.g. the cryptography library), not hashlib.
❌ Storing a plain sha256 of a password
stored = hashlib.sha256(pw.encode()).hexdigest() # fast + unsalted = unsafe✅ Use hashlib.pbkdf2_hmac with a unique salt and many rounds, or bcrypt/argon2.
🎯 Mini Challenge: Change Detector
Build a fingerprint function and use it to detect when content changes — the same idea behind file integrity checks and caching.
import hashlib
def fingerprint(text):
return hashlib.sha256(text.encode("utf-8")).hexdigest()
v1 = fingerprint("report v1")
v1_again = fingerprint("report v1")
v2 = fingerprint("report v2")
print(v1[:12]) # a5e10341fc2f
print("unchanged:", v1 == v1_again) # unchanged: True
print("changed:", v1 != v2) # changed: True
# ✅ Expected output:
# a5e10341fc2f
# unchanged: True
# changed: True📚 Keep Learning
Go deeper with the official Python documentation:
- hashlib — Secure hashes and message digests
- hashlib.pbkdf2_hmac — key derivation for passwords
- secrets.compare_digest — constant-time comparison
Frequently Asked Questions
Lesson complete — you can fingerprint any data!
You can produce digests with sha256 and md5, feed data with .update(), hash files in chunks, explain why hashing is one-way (not encryption), and store passwords properly with a salt and pbkdf2_hmac.
Practice quiz
What is a hash digest?
- A reversible encryption of the input
- The original input compressed
- A short, fixed-size fingerprint of the input that cannot be reversed
- A random number
Answer: A short, fixed-size fingerprint of the input that cannot be reversed. Hashing turns any input into a short fixed-size digest; the same input always gives the same digest and you cannot reverse it.
Why must you .encode() a string before hashing it with hashlib?
- Because hash functions operate on bytes, not text
- To make it shorter
- To add a salt
- To make it case-insensitive
Answer: Because hash functions operate on bytes, not text. Hash functions work on bytes, so a str must be encoded (usually UTF-8) first or you get a TypeError.
Is hashing the same as encryption?
- Yes, they are interchangeable
- Yes, but hashing is faster
- No — encryption is one-way
- No — hashing is one-way and has no key; encryption is reversible
Answer: No — hashing is one-way and has no key; encryption is reversible. Hashing is one-way by design (no key, no way back); encryption is reversible with the right key.
What does hashlib.md5(b"hello").hexdigest() return?
- 2cf24dba5fb0a30e
- 5d41402abc4b2a76b9719d911017c592
- The text hello
- An error
Answer: 5d41402abc4b2a76b9719d911017c592. md5 of b"hello" is 5d41402abc4b2a76b9719d911017c592 — fine for checksums, never for security.
Calling h.update(b"hello") then h.update(b" world") on a sha256 object gives the same digest as which one-shot call?
- hashlib.sha256(b"hello world")
- hashlib.sha256(b"hello")
- hashlib.sha256(b" world")
- None — update changes the result
Answer: hashlib.sha256(b"hello world"). Feeding data with repeated .update() is identical to hashing the concatenation b"hello world" at once.
Why hash a large file in fixed-size chunks instead of all at once?
- It produces a different, safer digest
- Because update() only accepts small inputs
- To keep memory use flat regardless of file size
- To make the hash reversible
Answer: To keep memory use flat regardless of file size. Reading in chunks and calling update keeps memory use flat even for multi-gigabyte files.
Why is a bare sha256 unsafe for storing passwords?
- It is too slow
- It is fast and unsalted, so identical passwords share a digest and guessing is cheap
- It cannot hash text
- It produces a different digest each time
Answer: It is fast and unsalted, so identical passwords share a digest and guessing is cheap. sha256 is fast (easy to brute force) and unsalted (identical passwords match), so it is unsafe for passwords.
Which hashlib function is designed for password storage?
- hashlib.md5
- hashlib.sha256
- hashlib.new
- hashlib.pbkdf2_hmac
Answer: hashlib.pbkdf2_hmac. pbkdf2_hmac is a slow, salted key-derivation function suitable for passwords (bcrypt/argon2 are also good).
What role does a salt play in password hashing?
- It makes hashing faster
- A unique random value per user so identical passwords get different digests
- It encrypts the password
- It compresses the password
Answer: A unique random value per user so identical passwords get different digests. A unique random salt per user ensures identical passwords do not produce the same stored digest.
What does hashlib.sha256("hello") (a str, not bytes) raise?
- A ValueError
- Nothing — it works fine
- A TypeError: Unicode-objects must be encoded
- A KeyError
Answer: A TypeError: Unicode-objects must be encoded. Passing a str raises TypeError: Unicode-objects must be encoded before hashing — encode it first.
Continue this course
- Previous: Running Commands with subprocess
- Next: Testing with unittest — Write TestCase classes with asserts, setUp/tearDown, and assertRaises
- Quick reference: Python cheat sheet
Frequently asked questions
What is hashing and what is hashlib for?
Hashing turns any input — a word, a file, gigabytes of data — into a short, fixed-size string called a digest. The same input always produces the same digest, but you cannot reverse the digest back into the input. hashlib is Python's standard module of these one-way hash functions (sha256, sha512, md5, and more). You use it to verify that a download was not corrupted, detect whether a file changed, deduplicate data, and (with extra care) store passwords.
Is hashing the same as encryption?
No, and confusing them is a common and dangerous mistake. Encryption is reversible: with the right key you can turn the scrambled text back into the original. Hashing is one-way by design — there is no key and no way to recover the input from the digest. Use encryption when you need to get data back later; use hashing when you only need to verify or fingerprint data, never to decrypt it.
Why do I have to .encode() a string before hashing it?
Hash functions operate on bytes, not text. A Python str is a sequence of characters, so you must first turn it into bytes by choosing an encoding, almost always UTF-8: 'hello'.encode('utf-8'). If you forget, hashlib raises a TypeError telling you Unicode objects must be encoded. Bytes literals like b'hello' are already bytes and can be hashed directly.
Can I use sha256 to store passwords?
Not directly. A plain sha256 of a password is fast to compute, which means an attacker can try billions of guesses per second, and identical passwords produce identical digests. Real password storage needs a unique random salt per user plus a deliberately slow key-derivation function. hashlib provides one: hashlib.pbkdf2_hmac. Even better are purpose-built libraries like bcrypt or argon2. Never store a bare sha256 of a password.