106. Hashlib Module
Cryptographic hashing with hashlib
106. Hashlib Module
Rohan saves a JSON report every 5 seconds. After a week, he has 100,000+ files. Many snapshots are identical — RAM barely changed between runs. He wants to skip saving a report if nothing changed.
He computes a content hash to detect duplicates:
import hashlib, json
def snapshot_hash(snapshot: dict) -> str:
# Serialize to a canonical string, then hash it
content = json.dumps(snapshot, sort_keys=True)
return hashlib.sha256(content.encode()).hexdigest()Two notes:
content.encode()— hashlib requires bytes, not a string..encode()converts using UTF-8..hexdigest()— returns a 64-character hex string like"a3f2c1..."..digest()returns raw binary bytes — not useful for storing or comparing.
He stores the last hash and skips saving if unchanged:
last_hash = None
def maybe_save(snapshot, report_dir):
global last_hash
h = snapshot_hash(snapshot)
if h == last_hash:
return # nothing changed
last_hash = h
# ... save the reportSHA-256 is a one-way function — you cannot reverse it. Two different inputs that produce the same hash (a “collision”) have never been found for SHA-256. This is why it’s used to store passwords: store the hash, never the password. On login, hash the attempt and compare.
In production, never hash passwords with SHA-256 alone. Use
bcryptorargon2which are deliberately slow to resist brute-force. SHA-256 is for content fingerprinting, not password storage.
💡 Fun fact: SHA-256 is part of the SHA-2 family designed by the NSA and published in 2001. It produces a 256-bit (32-byte) digest — displayed as 64 hexadecimal characters. SHA-256 is used everywhere: Git uses it to identify commits and blobs, Bitcoin uses it twice to validate every transaction, and TLS certificates use it to sign web traffic.
⚠️ Watch out: .encode() without arguments defaults to UTF-8, which is almost always what you want. But if the string contains non-ASCII characters (e.g., accented letters, emojis), make sure both the hasher and the verifier use the same encoding. If you hash "café".encode("utf-8") but later compare against "café".encode("latin-1"), they produce different hashes and verification will silently fail.
🤔 Think about it: SHA-256 hashing is deterministic — same input always gives same output. But salt is added before hashing passwords. Why does adding a salt help against attackers even if they know the salt? What attack does salting prevent that hashing alone does not?
Learning objectives
- Encode strings to bytes with .encode() before passing to hashlib
- Use .hexdigest() to get a human-readable hex string (not .digest() for bytes)
- Implement salted password hashing and verification
- Understand that SHA-256 hex digests are always 64 characters
Key concepts
- hashlib.sha256() — creates a hash object
- .encode() — converts str to bytes
- .hexdigest() — returns hex string output
- .digest() — returns raw bytes output
- Salt — string added to prevent rainbow table attacks
Try it
Concept detail
hashlib — Cryptographic Hashing
The hashlib module provides secure hash functions (SHA-256, SHA-512, MD5, etc.). Hashing is one-way: you can verify a hash, but cannot reverse it to get the input.
The bytes Requirement
All hashlib functions require bytes, not str. Always call .encode() first:
import hashlib
# Wrong — raises TypeError:
hashlib.sha256("hello")
# Correct:
hashlib.sha256("hello".encode()) # defaults to UTF-8
hashlib.sha256("hello".encode("utf-8")) # explicit encodingdigest() vs hexdigest()
h = hashlib.sha256("hello".encode())
h.digest() # b'\x2c\xf2...' raw bytes (32 bytes for SHA-256)
h.hexdigest() # '2cf24dba...' hex string (64 chars for SHA-256)Use .hexdigest() when you need to store or compare hashes as strings. Use .digest() only when you need raw bytes (rare — e.g., HMAC computations).
Salted Password Hashing Pattern
def hash_password(password: str, salt: str) -> str:
combined = (password + salt).encode()
return hashlib.sha256(combined).hexdigest()
stored = hash_password("myPassword", "random_salt_123")
# Later:
is_valid = hash_password("attempt", "random_salt_123") == storedWhen NOT to Use SHA-256 for Passwords
SHA-256 is fast — attackers can test billions of passwords per second. For user passwords, use bcrypt or argon2 (deliberately slow). Use SHA-256 for: file checksums, content deduplication, API request signing.
Solution
import hashlib
def hash_password(password: str, salt: str = "pyforge_salt") -> str:
"""Hash a password with a salt and return the hex digest string."""
combined = (password + salt).encode() # encode to bytes first
hash_obj = hashlib.sha256(combined)
return hash_obj.hexdigest() # hex string, not raw bytes
def verify_password(attempt: str, stored_hash: str, salt: str = "pyforge_salt") -> bool:
"""Return True if attempt hashes to the same value as stored_hash."""
return hash_password(attempt, salt) == stored_hash
def is_strong_password(password: str) -> bool:
"""Return True if password is at least 8 characters."""
return len(password) >= 8Tests
KNOWN_PASSWORD = "securePass99"
KNOWN_SALT = "pyforge_salt"
# Pre-computed: sha256(("securePass99pyforge_salt").encode()).hexdigest()
KNOWN_HASH = hashlib.sha256((KNOWN_PASSWORD + KNOWN_SALT).encode()).hexdigest()
def test_hash_password_returns_string():
result = hash_password(KNOWN_PASSWORD)
assert isinstance(result, str), f"hash_password should return str, got {type(result)}"
def test_hash_password_not_bytes():
result = hash_password(KNOWN_PASSWORD)
assert not isinstance(result, bytes), "hash_password must return str not bytes (use .hexdigest())"
def test_hash_password_correct_length():
result = hash_password(KNOWN_PASSWORD)
assert len(result) == 64, f"SHA-256 hex digest should be 64 chars, got {len(result)}"
def test_hash_password_known_value():
result = hash_password(KNOWN_PASSWORD, KNOWN_SALT)
assert result == KNOWN_HASH, f"Hash mismatch for known input"
def test_hash_password_deterministic():
result1 = hash_password("hello123")
result2 = hash_password("hello123")
assert result1 == result2, "Same input should always produce same hash"
def test_hash_password_different_inputs():
h1 = hash_password("password1")
h2 = hash_password("password2")
assert h1 != h2, "Different passwords should produce different hashes"
def test_verify_password_correct():
stored = hash_password(KNOWN_PASSWORD)
assert verify_password(KNOWN_PASSWORD, stored) is True
def test_verify_password_wrong():
stored = hash_password(KNOWN_PASSWORD)
assert verify_password("wrongPassword", stored) is False