What you'll learn
- Quick answer
- Detecting Corruption Is a Different Problem From Fixing It
- CRC32: Detection, For Real, With a Real Flipped Bit
- Hamming(7,4): Real Code That Actually Fixes the Bit, Not Just Detects It
- The Actual Reason This Works: Hamming Distance
- A Checksum Is Not a Security Mechanism
- Where These Actually Show Up
- FAQ
Quick Answer
A checksum like CRC32 can only tell you that data changed, not where or how to fix it. An error-correcting code like a Hamming code adds enough redundant structure that, within a limit (one bit per block for Hamming), it can pinpoint exactly which bit flipped and correct it without needing the original data resent. Both are demonstrated here with real executed Python, not descriptions.
Detecting Corruption Is a Different Problem From Fixing It
These two things get talked about as if they're the same idea, and they're genuinely not. A checksum answers one yes/no question: did this data change since the checksum was computed? It cannot tell you where the change happened or what the original value was — just that something doesn't match anymore. An error-correcting code goes further: given a bounded amount of corruption, it can pinpoint exactly which bit (or bits) flipped and flip them back, recovering the original data without needing to ask anyone to resend it.
The simplest possible checksum is a parity bit: count the 1-bits in a block of data, and append a single extra bit that makes the total count even (or odd, by convention). Flip any single bit in the result and the parity breaks — you've detected something, but with only one parity bit you have no information about which of the original bits changed. That's the ceiling of what plain detection can offer; correction needs structurally more information, which is exactly what the rest of this post builds up to with real, executed code.
CRC32: Detection, For Real, With a Real Flipped Bit
CRC32 is the checksum behind ZIP file integrity checks, Ethernet frame validation, and plenty of file-transfer protocols. It treats the data as a giant binary number and divides it by a fixed constant (using a special kind of division over binary polynomials, not regular arithmetic), keeping the remainder as the 32-bit checksum. The useful property is that almost any accidental corruption — a flipped bit, a dropped byte, random noise — changes that remainder, so comparing checksums before and after catches it.
Here's real code, not a description of what CRC32 would do, run against a sample string with exactly one bit flipped:
import zlib
original = b"TRANSFER:AMT=500000:ACCT=1183920"
crc_original = zlib.crc32(original)
data_bytes = bytearray(original)
data_bytes[10] ^= (1 << 3) # flip exactly one bit, byte index 10
corrupted = bytes(data_bytes)
crc_corrupted = zlib.crc32(corrupted)
print(original, "->", "%08x" % crc_original)
print(corrupted, "->", "%08x" % crc_corrupted)
print("Match?", crc_original == crc_corrupted)The actual output from running this:
b'TRANSFER:AMT=500000:ACCT=1183920' -> 54ff1246
b'TRANSFER:AET=500000:ACCT=1183920' -> edbc3446
Match? FalseOne flipped bit turned AMT into AET in the data (a single bit flip in the letter M happens to land on a different printable letter, which is itself a good illustration of how a tiny physical fault — a bad RAM cell, a noisy cable — can silently corrupt readable-looking data), and the CRC32 values are completely different: 54ff1246 versus edbc3446. That mismatch is the entire mechanism — CRC32 never has to understand what the data means, it just needs the checksum to change whenever the input does. For good measure, implementing CRC32 by hand bit-by-bit (the actual polynomial division, not calling a library) and comparing it to Python's built-in zlib.crc32 on the uncorrupted string produces an identical 54ff1246 from both, confirming the library isn't doing anything mysterious.
But notice what CRC32 cannot tell you from that output: which byte changed, which bit within that byte flipped, or what the correct value should have been. All it knows is "corrupted, somehow." That's the hard limit of detection-only schemes, and it's why systems that use CRC32 (ZIP, Ethernet, TCP) respond to a mismatch by discarding the data and requesting a resend, rather than attempting any repair.
Hamming(7,4): Real Code That Actually Fixes the Bit, Not Just Detects It
A Hamming code is the cleanest teaching example of actual error correction because the mechanism is small enough to run by hand. Hamming(7,4) takes 4 data bits and adds 3 carefully chosen parity bits, producing a 7-bit codeword, arranged so that each parity bit covers a different, overlapping subset of the data bits. The overlap is the whole trick: if exactly one bit in the 7-bit codeword flips, the specific pattern of which parity checks now fail (and which still pass) uniquely identifies which one of the 7 positions flipped — not just that something is wrong, but precisely where.
def hamming74_encode(bits4):
d1, d2, d3, d4 = bits4
p1 = d1 ^ d2 ^ d4
p2 = d1 ^ d3 ^ d4
p3 = d2 ^ d3 ^ d4
return [p1, p2, d1, p3, d2, d3, d4] # positions 1-7
def hamming74_decode(codeword):
p1, p2, d1, p3, d2, d3, d4 = codeword
c1 = p1 ^ d1 ^ d2 ^ d4
c2 = p2 ^ d1 ^ d3 ^ d4
c3 = p3 ^ d2 ^ d3 ^ d4
syndrome = c1 * 1 + c2 * 2 + c3 * 4 # 0 = no error, else = the flipped position
corrected = codeword.copy()
if syndrome != 0:
corrected[syndrome - 1] ^= 1
return corrected, syndrome, [corrected[2], corrected[4], corrected[5], corrected[6]]Running this for real: encode the 4-bit value [1, 0, 1, 1], which produces the 7-bit codeword [0, 1, 1, 0, 0, 1, 1]. Now flip exactly one bit — position index 4 (the fifth bit, a data bit) — and decode:
Original codeword: [0, 1, 1, 0, 0, 1, 1]
Corrupted codeword: [0, 1, 1, 0, 1, 1, 1] (bit at index 4 flipped)
Computed syndrome: 5
Corrected codeword: [0, 1, 1, 0, 0, 1, 1]
Decoded data bits: [1, 0, 1, 1]
Matches original? TrueThe syndrome calculation alone (no comparison to the original needed) correctly computed 5, which is exactly the 1-indexed position that was flipped, and corrected it blind. Running the same test across all 7 possible single-bit-flip positions (flip position 0, decode, check; flip position 1, decode, check; ... through position 6) and comparing the decoded 4-bit output to the original [1, 0, 1, 1] in every case: all 7 corrected correctly. That's the actual, executed proof that Hamming(7,4) can correct any single-bit error in its codeword — not an explanation of how it should work, the real syndrome values and real corrected output.
The catch, and it's a real structural limit, not an implementation detail: Hamming(7,4) can only reliably correct a single bit error per 7-bit block. If two bits flip in the same codeword, the syndrome calculation produces a result that looks like a different single-bit error, and the "correction" confidently flips the wrong bit, making things worse while reporting success. This is why ECC RAM (which uses extended Hamming codes, essentially this mechanism plus one more overall parity bit) is specifically described as single-error-correcting, double-error-detecting (SECDED) — it can fix one flipped bit per word, but can only detect, not fix, two.
The Actual Reason This Works: Hamming Distance
The mechanism above isn't a trick specific to 7 and 4 — it's an instance of a general principle called Hamming distance: the number of bit positions in which two codewords of equal length differ. A parity-bit scheme (one extra bit, as described earlier) has a minimum Hamming distance of 2 between any two valid codewords, which is enough to notice that something is wrong (any single flip lands you on an invalid codeword) but not enough to know which of two neighbouring valid codewords you started from, since a single flip could have come from either direction.
Hamming(7,4)'s 3 extra parity bits are specifically chosen so that every pair of valid 7-bit codewords differs in at least 3 positions, not 2. That extra distance is exactly what buys correction instead of just detection: if the minimum distance between valid codewords is 3, then a single bit flip moves you only 1 step away from the correct codeword, but at least 2 steps away from every other valid codeword — so there's always exactly one valid codeword that's "closest" to what you received, and that closest one is unambiguously the right answer to decode to. This is the actual mathematical reason the syndrome calculation in the real code above worked for every single-bit flip across all 7 positions: the code was built, by construction, to guarantee an unambiguous closest match whenever exactly one bit is wrong. It also directly explains the earlier limit — a double-bit flip can land you exactly as close to a different, wrong codeword, which is why Hamming(7,4) can correct one error but only detect, never fix, two.
A Checksum Is Not a Security Mechanism
One confusion worth clearing up directly: CRC32 and similar checksums protect against accidental corruption — noisy cables, flaky storage, a dropped packet — not against someone deliberately tampering with the data. CRC32 is a simple, fast, linear function, which makes it cheap to compute on every network frame, but that same simplicity means it's trivial to construct a different payload that produces the exact same CRC32 value on purpose. That's a fundamentally different threat model from what a cryptographic hash function like SHA-256 is built for, where the explicit design goal is making it computationally infeasible to find two different inputs producing the same output, even for an attacker trying hard to do exactly that.
This is why file-integrity checks for untrusted downloads use SHA-256 checksums, not CRC32, while CRC32 remains the right tool inside ZIP files and Ethernet frames, where the only realistic failure mode is random physical noise, not an adversary. Reaching for the wrong one — CRC32 where tamper-resistance matters, or a slow cryptographic hash where you just need to catch accidental bit rot on every network packet in real time — is a real, recurring design mistake, not a pedantic distinction.
Where These Actually Show Up
CRC32-style checksums are everywhere data moves or sits on unreliable media: ZIP and PNG file formats embed them to catch storage or transfer corruption, Ethernet frames carry one so a switch can discard a corrupted frame instead of forwarding garbage, and TCP uses a similar (simpler) checksum on every segment. All of these are detect-and-discard-or-retransmit, not detect-and-fix, because retransmission is cheap when you have a live connection to ask again.
Error-correcting codes earn their extra complexity specifically where retransmission is expensive, slow, or impossible: ECC RAM corrects single-bit memory errors caused by cosmic rays and electrical noise without the CPU ever knowing anything happened; deep-space probes use far more sophisticated codes than Hamming (convolutional and turbo codes) because asking Voyager to resend a packet costs a round-trip measured in hours; and QR codes use Reed-Solomon coding specifically so a partially damaged, dirty, or obscured code can still scan correctly — Priodemy's own piece on how QR codes work covers that specific application of error correction to 2D barcodes in detail. Reed-Solomon and Hamming codes solve the same underlying problem (recover the original from partial corruption) with different mathematical machinery suited to different kinds of errors — Hamming is built for scattered single-bit flips, Reed-Solomon is built to tolerate whole bursts of consecutive corrupted symbols, which is exactly the kind of damage a scratched or smudged QR code produces.
