The first practical error-correcting codes — elegant, efficient, and perfect
The Bell Labs frustration that launched error-correcting codes
Step-by-step construction and encoding
Detecting and locating single-bit errors
Generator and parity-check matrices
Extending Hamming codes for double-error detection
Why Hamming codes are mathematically special
In the late 1940s, Richard Hamming worked at Bell Labs alongside Claude Shannon. He used relay-based computers that ran programs over weekends.
The machines used parity checking. When an error was detected, the machine would stop and flash lights indicating the error. On weekdays, operators would fix the error and restart. On weekends — no operators. Hamming would arrive Monday to find his jobs had failed early Friday evening.
Hamming realized: "If the machine can detect an error, why can't it locate and correct the error?" He needed multiple parity checks, each covering a different subset of bits, so that the pattern of parity failures would point to the exact error location.
Instead of one parity bit checking all data bits, use multiple parity bits, each covering a specific overlapping subset of data bits.
1 parity bit → 1 check → 2 outcomes:
r parity bits → r checks → 2r outcomes:
In Hamming(7,4), we have 7 bit positions. Positions that are powers of 2 (1, 2, 4) are parity bits. The remaining positions (3, 5, 6, 7) hold data.
| Position | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|
| Binary | 001 | 010 | 011 | 100 | 101 | 110 | 111 |
| Role | p1 | p2 | d1 | p3 | d2 | d3 | d4 |
Checks positions with bit 0 set in binary:
1, 3, 5, 7
Checks positions with bit 1 set in binary:
2, 3, 6, 7
Checks positions with bit 2 set in binary:
4, 5, 6, 7
Each parity bit covers all positions whose binary representation has a 1 in the corresponding bit position:
| Position | Binary | p1 checks? | p2 checks? | p3 checks? |
|---|---|---|---|---|
| 1 (p1) | 001 | Yes | ||
| 2 (p2) | 010 | Yes | ||
| 3 (d1) | 011 | Yes | Yes | |
| 4 (p3) | 100 | Yes | ||
| 5 (d2) | 101 | Yes | Yes | |
| 6 (d3) | 110 | Yes | Yes | |
| 7 (d4) | 111 | Yes | Yes | Yes |
Encode data bits d1d2d3d4 = 1011
Step 1: Place data bits in positions 3, 5, 6, 7:
Step 2: Compute parity bits:
p1: XOR positions 1,3,5,7
p1 ⊕ 1 ⊕ 0 ⊕ 1 = 0
p1 = 0
p2: XOR positions 2,3,6,7
p2 ⊕ 1 ⊕ 1 ⊕ 1 = 0
p2 = 1
p3: XOR positions 4,5,6,7
p3 ⊕ 0 ⊕ 1 ⊕ 1 = 0
p3 = 0
Result:
When we receive a (possibly corrupted) codeword, we recompute each parity check. The results form the syndrome.
Example: Sent 0110011, received 0111011 (error at position 4)
s1: XOR pos 1,3,5,7
0 ⊕ 1 ⊕ 0 ⊕ 1 = 0
s2: XOR pos 2,3,6,7
1 ⊕ 1 ⊕ 1 ⊕ 1 = 0
s3: XOR pos 4,5,6,7
1 ⊕ 0 ⊕ 1 ⊕ 1 = 1
| Syndrome (s3s2s1) | Decimal | Meaning | Action |
|---|---|---|---|
| 000 | 0 | No error | Accept as-is |
| 001 | 1 | Error at position 1 | Flip bit 1 (p1) |
| 010 | 2 | Error at position 2 | Flip bit 2 (p2) |
| 011 | 3 | Error at position 3 | Flip bit 3 (d1) |
| 100 | 4 | Error at position 4 | Flip bit 4 (p3) |
| 101 | 5 | Error at position 5 | Flip bit 5 (d2) |
| 110 | 6 | Error at position 6 | Flip bit 6 (d3) |
| 111 | 7 | Error at position 7 | Flip bit 7 (d4) |
The encoding process can be expressed as matrix multiplication over GF(2):
c = m · G, where m is the 1×4 message vector and G is the 4×7 generator matrix.
Systematic form: G = [P | Ik] where P contains the parity-check coefficients and Ik is the 4×4 identity matrix.
The parity-check matrix H is used for decoding. The syndrome is computed as s = H · rT, where r is the received vector.
The columns of H are the binary representations of the numbers 1 through 7:
Col 1: 001, Col 2: 010, Col 3: 011
Col 4: 100, Col 5: 101, Col 6: 110, Col 7: 111
If there's a single error at position j, the syndrome s = H · ejT equals column j of H, which is the binary representation of j.
Encode message m = [1, 0, 1, 1] using c = m · G:
import numpy as np
G = np.array([
[1, 1, 1, 0, 0, 0, 0],
[1, 0, 0, 1, 1, 0, 0],
[0, 1, 0, 1, 0, 1, 0],
[1, 1, 0, 1, 0, 0, 1]
])
message = np.array([1, 0, 1, 1])
codeword = message @ G % 2
print(f"Codeword: {codeword}") # [0 1 1 0 0 1 1]
H = np.array([
[1, 0, 1, 0, 1, 0, 1],
[0, 1, 1, 0, 0, 1, 1],
[0, 0, 0, 1, 1, 1, 1]
])
def decode_hamming74(received):
"""Decode a Hamming(7,4) codeword using syndrome decoding."""
r = np.array(received)
syndrome = H @ r % 2
error_pos = syndrome[0] * 1 + syndrome[1] * 2 + syndrome[2] * 4
if error_pos == 0:
print("No error detected")
else:
print(f"Error at position {error_pos}, flipping bit")
r[error_pos - 1] ^= 1 # Fix the error (0-indexed)
# Extract data bits from positions 3, 5, 6, 7 (indices 2, 4, 5, 6)
data = [r[2], r[4], r[5], r[6]]
return data
# Test: introduce error at position 5
received = [0, 1, 1, 0, 1, 1, 1] # bit 5 flipped (was 0)
decoded = decode_hamming74(received)
print(f"Decoded message: {decoded}") # [1, 0, 1, 1]
| Message | Codeword | Weight | Message | Codeword | Weight | |
|---|---|---|---|---|---|---|
| 0000 | 0000000 | 0 | 1000 | 1110000 | 3 | |
| 0001 | 1101001 | 4 | 1001 | 0011001 | 3 | |
| 0010 | 0110010 | 3 | 1010 | 1000010 | 4 | |
| 0011 | 1011011 | 4 | 1011 | 0101011 | 3 | |
| 0100 | 1010100 | 3 | 1100 | 0100100 | 4 | |
| 0101 | 0111101 | 4 | 1101 | 1001101 | 3 | |
| 0110 | 1100110 | 4 | 1110 | 0010110 | 3 | |
| 0111 | 0001111 | 3 | 1111 | 1111111 | 7 |
Weight distribution: 1 word of weight 0, 7 of weight 3, 7 of weight 4, 1 of weight 7
Minimum weight: 3 (confirming dmin = 3 for this linear code)
SECDED = Single Error Correction, Double Error Detection. Add one more parity bit (overall parity) to create an [8, 4, 4] code.
| Syndrome | Overall P | Meaning |
|---|---|---|
| = 0 | OK | No error |
| ≠ 0 | Fail | 1 error → correct it |
| ≠ 0 | OK | 2 errors → detected! |
| = 0 | Fail | Error in p0 only |
Hamming(7,4) is just one member of a family. For any integer r ≥ 2, there is a Hamming(2r−1, 2r−1−r) code.
| r | Code | [n, k, d] | Rate | Redundancy |
|---|---|---|---|---|
| 2 | Hamming(3,1) | [3, 1, 3] | 0.333 | 2 bits |
| 3 | Hamming(7,4) | [7, 4, 3] | 0.571 | 3 bits |
| 4 | Hamming(15,11) | [15, 11, 3] | 0.733 | 4 bits |
| 5 | Hamming(31,26) | [31, 26, 3] | 0.839 | 5 bits |
| 6 | Hamming(63,57) | [63, 57, 3] | 0.905 | 6 bits |
| 8 | Hamming(255,247) | [255, 247, 3] | 0.969 | 8 bits |
A code is perfect if it achieves the Hamming bound with equality — the Hamming spheres of radius t around each codeword tile the entire space with no overlaps or gaps.
For Hamming(2r−1, 2r−1−r) with t = 1:
Every vector in {0,1}n is within distance 1 of exactly one codeword.
For a perfect code, the nearest-codeword decoder is unambiguous for all error patterns of weight ≤ t:
Every received vector r is within distance 1 of exactly one codeword. The decoder always makes a definite decision.
Some received vectors are NOT within distance t of any codeword. These are "uncorrectable" patterns that the decoder can detect but not fix.
The relationship between minimum distance and error correction/detection is fundamental:
class Hamming74:
"""Complete Hamming(7,4) encoder and decoder."""
G = [[1,1,1,0,0,0,0], [1,0,0,1,1,0,0],
[0,1,0,1,0,1,0], [1,1,0,1,0,0,1]]
H = [[1,0,1,0,1,0,1], [0,1,1,0,0,1,1], [0,0,0,1,1,1,1]]
@staticmethod
def encode(message):
"""Encode 4-bit message to 7-bit codeword."""
assert len(message) == 4
codeword = [0] * 7
for i in range(4):
if message[i]:
for j in range(7):
codeword[j] ^= Hamming74.G[i][j]
return codeword
@staticmethod
def decode(received):
"""Decode 7-bit received word, correcting single errors."""
r = list(received)
# Compute syndrome
syndrome = 0
for i in range(3):
s = 0
for j in range(7):
s ^= Hamming74.H[i][j] * r[j]
syndrome += s * (2 ** i)
if syndrome != 0:
r[syndrome - 1] ^= 1 # Correct the error
return [r[2], r[4], r[5], r[6]] # Extract data bits
import random
def test_hamming74():
"""Test all possible messages and single-error patterns."""
errors_corrected = 0
total_tests = 0
# Test all 16 possible 4-bit messages
for msg_int in range(16):
message = [(msg_int >> i) & 1 for i in range(3, -1, -1)]
codeword = Hamming74.encode(message)
# Test no-error case
decoded = Hamming74.decode(codeword)
assert decoded == message, f"Failed for {message}"
# Test all 7 single-error patterns
for err_pos in range(7):
corrupted = list(codeword)
corrupted[err_pos] ^= 1 # Introduce single error
decoded = Hamming74.decode(corrupted)
assert decoded == message, \
f"Failed: msg={message}, err_pos={err_pos}"
errors_corrected += 1
total_tests += 1
print(f"All tests passed! {errors_corrected} errors corrected "
f"across {total_tests} test cases.")
test_hamming74() # All tests passed! 112 errors corrected
Modern server RAM uses SECDED Hamming codes to protect against bit flips caused by cosmic rays, alpha particles, and electrical noise.
| Property | Single Parity | Repetition-3 | Hamming(7,4) | SECDED(8,4) |
|---|---|---|---|---|
| Parameters | [n+1, n, 2] | [3, 1, 3] | [7, 4, 3] | [8, 4, 4] |
| Rate | ~1.0 | 0.333 | 0.571 | 0.500 |
| Errors detected | 1 | 2 | 2 | 3 |
| Errors corrected | 0 | 1 | 1 | 1 |
| Perfect? | No | Yes (trivially) | Yes | No |
| Decode complexity | O(n) | O(n) | O(n) | O(n) |
Hamming codes are great for correction, but many applications only need fast, reliable detection of errors.
CRC (Cyclic Redundancy Check) uses polynomial arithmetic to detect burst errors with extremely high probability.
Used in Ethernet, USB, ZIP files, and virtually every network protocol.