🐍 Python Course · Stage 3 · Lesson 48 of 89 · hashlib — Hashing
Course Overview →

🔵 Read Beginner and Intermediate tabs. The Expert tab has the internals — worth reading once you finish Stage 3.

← secrets — Secure Random statistics — Statistics →
thecodex.expert · The Codex Family of Knowledge
Python Standard Library

hashlib — Secure Hashes and Message Digests

hashlib computes cryptographic hashes like SHA-256 — fixed-size fingerprints of data used for integrity checks, deduplication, and (with the right functions) password storage.

import hashlib docs.python.org Last verified:
Canonical Definition

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.

🟩 Beginner

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.

⏱ 25 min📄 2 sections🔶 Prerequisite: secrets module
The idea

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.

Pythonbasics.py
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 first
hashlib needs bytes, not strings

hashlib.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

Pythonfile_hash.py
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

Continue to statistics →

🔵 Intermediate

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.

⏱ 30 min📄 2 sections🔶 Prerequisite: Beginner tab

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.

Pythonpassword_hashing.py
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)
Never use plain sha256 for passwords

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

AlgorithmUse forNotes
sha256 / sha512General integrity, fingerprintsFast, secure, the default choice
blake2b / blake2sHigh-speed hashingFaster than SHA-2, equally secure
sha3_256Integrity (different design)SHA-3 family, distinct from SHA-2
pbkdf2_hmac / scryptPasswordsDeliberately slow + salted
md5 / sha1Legacy / non-security checksums onlyBroken — never use for security
Commonly confused
Hashing is not encryption. Encryption is reversible with a key; hashing is one-way. You cannot "decrypt" a hash. This is exactly why hashes are used for passwords — even if the database leaks, the original passwords are not directly recoverable.
MD5 and SHA-1 are broken. Both have known collision attacks and must never be used for security (signatures, passwords, integrity against tampering). They survive only as fast non-security checksums for detecting accidental corruption.

✅ 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

Continue to statistics →

🔴 Expert

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.

⏱ 20 min📄 1 section🔶 Prerequisite: After Stage 3

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.

Pythonhashlib_advanced.py
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)   # 64

The 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

Continue to statistics →

Sources

1
Python Standard Library — hashlib. docs.python.org/3/library/hashlib.html.
2
Python Standard Library — hashlib.pbkdf2_hmac. docs.python.org/3/library/hashlib.html#hashlib.pbkdf2_hmac.
3
Python Standard Library — hmac. docs.python.org/3/library/hmac.html.
4
Python Standard Library — hashlib.scrypt. docs.python.org/3/library/hashlib.html#hashlib.scrypt.
Source confidence: High Last verified: Primary source: docs.python.org hashlib