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

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())
# 5d41402abc4b2a76b9719d911017c592

Hash 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
# 5d41402abc4b2a76b9719d911017c592

2 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())
# b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9

This 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 memory

The 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: True

3 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: True

For 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:
# ef92b778bafe771e

First 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:

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

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.