LESSON 78 · DEEPER PYTHON28 min

Check Content with hashlib

Calculate a deterministic SHA-256 checksum for text and detect when its content changes.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Check Content with hashlib

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    Suppose you saved a document yesterday and want to check whether its content is identical today. Reading every byte yourself is possible, but a compact digest makes the comparison convenient. Python's hashlib module can calculate SHA-256, a deterministic digest of bytes. If the bytes change, the digest should change. This lesson will start with a real error caused by handing the hash function a Python string rather than bytes. Then we will choose UTF-8 encoding explicitly, calculate a digest, and compare it to the familiar SHA-256 test value for abc.

  2. 00:40

    We will wrap the operation in two small functions: one produces a hexadecimal checksum, and the other checks it against an expected value. Finally we will test a one-character change and a word containing a non-ASCII character. There are two limits we must keep visible. A matching checksum does not authenticate who supplied the expected checksum. Plain SHA-256 is also not an appropriate password storage scheme. Our goal is narrower: define the exact bytes of text and detect whether that content matches an expected digest. Before the broken run, predict what type the sha256 function expects.

  3. 01:22

    Python refuses the string directly. That is a useful failure, because text can be represented by bytes in more than one encoding. The hash algorithm does not know whether we meant UTF-8, UTF-16, or another representation. We have to decide. For this tutorial, UTF-8 is the rule: encode the text with utf-eight before passing it to sha256. This also means that two visually similar strings might produce different digests if their underlying Unicode representation differs. We are hashing bytes, not the idea of a word. The exception tells us where the type boundary sits.

  4. 02:04

    A tempting response would be to catch the TypeError and return an empty digest. That would hide a programming mistake and produce a meaningless checksum. Instead, fix the input type, then compare the result to a known vector. The value for abc is widely used as a SHA-256 test case. If our computed hex string matches it, we have stronger evidence the code path is doing what we intend. We will inspect the bytes themselves before wrapping the behavior in a helper function. The first line confirms that we now have bytes. For plain ASCII letters, UTF-8 produces the byte values we expect, so the representation looks simple.

  5. 02:47

    The second line is the known SHA-256 digest of those bytes, shown with hexdigest as lowercase hexadecimal characters. SHA-256 returns thirty-two digest bytes; each byte takes two hexadecimal characters, giving us the sixty-four- character result on the third line. The hex representation is useful for logs, published checksum text, and exact assertions. The underlying digest() method would return raw bytes instead. Both refer to the same computation, but our task contract returns the printable hex form. Now consider what happens for an accented word.

  6. 03:27

    UTF-8 may use multiple bytes for one visible character. That does not change the algorithm; it clarifies why encoding is part of the input definition. It also clarifies why the built-in hash function is the wrong replacement. Python's hash() supports internal hash- table behavior and may vary across interpreter processes. It is not a stable content fingerprint. We will now put the explicit encoding and SHA-256 operation in one reusable function, then add a comparison helper. The first function names our exact rule: take text, encode as UTF-8, hash those

  7. 04:08

    bytes with SHA-256, and return lowercase hexadecimal. The second function calculates that same digest and compares it with the expected text using hmac.compare_digest. This comparison routine avoids a simple early-exit content comparison pattern that can leak timing information. It does not turn the expected digest into a trusted statement about the publisher. It only compares two values. With abc, the result matches the known digest; with abd, one letter differs, so the comparison is false. We could use this pattern to check that downloaded bytes match a digest obtained

  8. 04:49

    from a trusted source, but the source of that expected digest matters. If an attacker replaces both the download and the digest beside it, a matching comparison tells us nothing about origin. For authentication, use an appropriate keyed MAC or digital signature. There is also no way to use this fast, plain SHA-256 call as a safe password database design merely by adding compare_digest. Password storage requires a dedicated slow scheme, a salt, and proper parameters. Keeping those distinct use cases prevents us from overclaiming what this little helper can establish. The accented character becomes two UTF-8 bytes, shown by the escaped byte values in

  9. 05:32

    the first line. The SHA-256 hexadecimal result still has sixty-four characters, regardless of whether the input text is short or long. A word matches its own expected digest; removing the accent changes the bytes and the digest, so the last result is false. This is why an application must document its encoding and, when relevant, Unicode normalization policy. Two strings that look alike to a person can differ at the byte level. In the lesson task, implement both functions yourself and test the standard abc vector before trying your own message.

  10. 06:13

    Change a single character, a newline, or the capitalization, then predict whether the comparison should succeed. If it fails unexpectedly, print the encoded bytes and inspect what you actually hashed. Remember the boundary: a content checksum is a reproducible fingerprint of chosen bytes. It is useful for detecting accidental change or checking content against a digest from a trusted place. It is not a proof of who created the content, and it is not a password hashing scheme. Those distinctions let us use the tool accurately without asking it to do a job it was never designed to do.

Understand the concept

A checksum gives a compact fingerprint of some bytes. If you download or save content and calculate its SHA-256 digest again later, a changed digest is strong evidence the bytes changed. hashlib supplies sha256 and many other digest algorithms. Its input must be bytes, so text first needs an explicit encoding such as UTF-8. The hexadecimal representation from hexdigest is convenient for logs and examples.

The task makes checksum_text(text) and checksum_matches(text, expected_hex). The first encodes the text using UTF-8, hashes those bytes, and returns the lowercase hexadecimal digest. The second compares the calculated digest with an expected string using hmac.compare_digest. Try the standard test vector abc, change one character, and hash non-ASCII text. Even a one-character change should produce a very different digest.

This exercise verifies content equality under a supplied expected digest. It does not establish who supplied that digest. An attacker who can replace both a file and its published checksum can make them match. For authentication use an authenticated source, a digital signature, or a keyed message authentication code as appropriate. Although compare_digest avoids simple content-dependent comparison timing, this lesson's plain SHA-256 result is not a MAC.

Do not use a plain SHA-256 digest to store passwords. Password storage calls for a deliberately expensive password hashing scheme with salt and parameters, such as scrypt or PBKDF2 when properly configured. A checksum is useful for deterministic content checks, deduplication candidates, and reproducible fixtures. Define the exact bytes first: differing newline conventions, Unicode normalization, or encodings will produce different digests even when text appears similar on screen.

  • hashlib.sha256 accepts bytes, not str
  • UTF-8 encoding determines the bytes of text
  • hexdigest produces a printable lowercase digest
  • A checksum alone does not authenticate the expected value

See it step by step

Read the code, predict the output, then compare it with the result.

01. Hash the exact bytes

PYTHON
import hashlib
print(hashlib.sha256(b"abc").hexdigest())
EXPECTED OUTPUT
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad

This standard SHA-256 vector makes it easy to check the digest calculation against a known answer.

02. Encode Unicode text first

PYTHON
import hashlib
message = "café"
print(message.encode("utf-8"))
print(len(hashlib.sha256(message.encode("utf-8")).hexdigest()))
EXPECTED OUTPUT
b'caf\xc3\xa9'
64

The accented character occupies two UTF-8 bytes; SHA-256's hex digest has 64 characters.

03. Detect a changed message

PYTHON
import hashlib
import hmac
def digest(text):
    return hashlib.sha256(text.encode("utf-8")).hexdigest()
print(hmac.compare_digest(digest("abc"), digest("abc")))
print(hmac.compare_digest(digest("abc"), digest("abd")))
EXPECTED OUTPUT
True
False

The same text produces the same digest, while changed bytes produce a different result.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Convert text to bytes before hashing

A SHA-256 digest summarizes bytes, not abstract Python text. If you pass a str directly to hashlib.sha256, Python raises TypeError. That failure usefully forces a decision: which encoding defines the bytes of this content? UTF-8 is a common explicit choice. The familiar abc test vector gives us a digest with a published expected value, so it is a good starting point for checking the mechanics rather than trusting a code path merely because it ran.

Inspect the encoded bytes and then hash them. hexdigest produces a 64-character lowercase hexadecimal representation of the 32-byte SHA-256 result. The hexadecimal text is convenient for logs and assertions, while digest() would return the raw bytes. This operation is deterministic: identical bytes give identical digests. It is not reversible in the sense of decoding the original message from the digest.

PYTHON
import hashlib

data = "abc".encode("utf-8")
print(data)
print(hashlib.sha256(data).hexdigest())
EXPECTED OUTPUT
b'abc'
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
Follow the reasoning
  1. Point out the bytes literal before hashing.
  2. Compare the result to the known SHA-256 digest for abc.
02 / 03

Wrap the checksum and compare an expected value

checksum_text states the encoding decision once and returns the standard lowercase digest. checksum_matches computes that digest and compares it with an expected hexadecimal string using hmac.compare_digest. This comparison function avoids some simple timing differences from an ordinary early-exit string equality check. It does not make an untrusted expected value authentic, but it is a sensible habit for digest comparisons where timing might matter.

Run an unchanged message and one altered character against the same expected digest. True and False are the intended results. The built-in hash() function is not a replacement here: its output is intended for Python's in-memory hashing behavior, can vary between runs, and is not a stable cryptographic content checksum. Store or transmit the SHA-256 digest only if you also know which bytes and encoding generated it.

PYTHON
import hashlib
import hmac

def checksum_text(text):
    return hashlib.sha256(text.encode("utf-8")).hexdigest()

def checksum_matches(text, expected_hex):
    return hmac.compare_digest(checksum_text(text), expected_hex)

expected = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
print(checksum_matches("abc", expected))
print(checksum_matches("abd", expected))
EXPECTED OUTPUT
True
False
Follow the reasoning
  1. Trace both calls through the same UTF-8 conversion.
  2. Change only one character and observe that its digest no longer matches.
03 / 03

Test Unicode and state what a checksum cannot prove

A non-ASCII word illustrates why encoding must be named. In UTF-8, é occupies two bytes. Another encoding or a different Unicode normalization form could represent text differently, producing another digest. Hash the exact bytes you intend to compare, and preserve that convention in any protocol or file format. The example prints the UTF-8 bytes and checks that the digest differs when the accent is removed.

A matching checksum confirms consistency with the supplied expected digest, but it does not prove the publisher's identity. If an attacker can replace both the content and the digest, they can make the pair match again. Use an authenticated source, keyed MAC, or signature when origin matters. Plain SHA-256 is also not a password storage method; password hashing needs a salt and a deliberately costly scheme. The exercise stays within content-change detection.

PYTHON
import hashlib

def checksum_text(text):
    return hashlib.sha256(text.encode("utf-8")).hexdigest()

print("café".encode("utf-8"))
print(len(checksum_text("café")))
print(checksum_text("café") == checksum_text("cafe"))
EXPECTED OUTPUT
b'caf\xc3\xa9'
64
False
Follow the reasoning
  1. Inspect the two-byte UTF-8 encoding of é.
  2. Check digest length and changed-content result without claiming this authenticates the text.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnComplete checksum_text(text) with hashlib.sha256(text.encode('utf-8')).hexdigest(), then make checksum_matches compare the result with expected_hex using hmac.compare_digest. The prepared calls should print True and False. Keep the digest lowercase and do not use the built-in hash() function, whose values are not a stable content checksum.
Need a hint?

Encode text to UTF-8 bytes before hashlib.sha256. Call .hexdigest() on the hash object. In the second function, return hmac.compare_digest(checksum_text(text), expected_hex).

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.Why encode text before passing it to hashlib.sha256?

    02.What does matching a file and its published SHA-256 digest prove on its own?

    03.Is plain SHA-256 suitable for password storage?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Passing a string directly to SHA-256

    COMMON MISTAKE
    import hashlib
    print(hashlib.sha256('abc').hexdigest())
    FIX
    import hashlib
    print(hashlib.sha256('abc'.encode('utf-8')).hexdigest())

    What happens: TypeError: Strings must be encoded before hashing.
    Make the text-to-bytes conversion explicit and stable.

    Using hash() as a persistent checksum

    COMMON MISTAKE
    print(hash('abc'))
    FIX
    import hashlib
    print(hashlib.sha256(b'abc').hexdigest())

    What happens: The built-in hash value can differ across Python processes and is not a cryptographic content digest.
    Use a defined digest algorithm for reproducible content checks.

    Browse this lesson’s problem fixes

    Sources and further reading