Home DSA LZW Compression: Dictionary-Based Encoding and Decoding Explained
Intermediate 3 min · July 14, 2026

LZW Compression: Dictionary-Based Encoding and Decoding Explained

Learn LZW compression: dictionary-based encoding and decoding with Python code, real-world incidents, debugging guide, and interview questions.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Basic understanding of data compression concepts
  • Familiarity with hash tables or dictionaries
  • Python programming experience
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • LZW is a lossless compression algorithm that builds a dictionary of substrings during encoding.
  • It replaces repeated substrings with single codes, achieving compression.
  • The dictionary is adaptive and does not need to be stored separately.
  • Decoding reconstructs the dictionary on the fly.
  • Used in GIF, TIFF, and Unix compress.
✦ Definition~90s read
What is LZW Compression?

LZW compression is a lossless dictionary-based algorithm that replaces repeated substrings with codes, adapting to the data as it processes.

Imagine you have a long list of words and you want to shorten it.
Plain-English First

Imagine you have a long list of words and you want to shorten it. You create a shorthand code for each unique word. When you see the word again, you just write the code. LZW does this automatically, learning new codes as it goes, so you don't need a separate codebook.

Have you ever wondered how GIF images or Unix 'compress' achieve such small file sizes? The secret lies in LZW (Lempel-Ziv-Welch) compression, a dictionary-based lossless algorithm that adapts to the data it compresses. Unlike fixed-dictionary methods, LZW builds its dictionary on the fly, encoding repeated patterns as single codes. This makes it incredibly effective for text and images with repetitive content.

In this tutorial, you'll learn the inner workings of LZW compression and decompression, with production-ready Python code. We'll walk through a real-world bug that caused data corruption, provide a debugging guide for production issues, and cover common mistakes. By the end, you'll be able to implement LZW and understand its trade-offs.

LZW is a cornerstone of data compression, and mastering it will deepen your understanding of adaptive algorithms. Let's dive in!

How LZW Compression Works

LZW compression builds a dictionary of substrings encountered during encoding. Initially, the dictionary contains all single characters (e.g., 0-255 for bytes). The encoder reads input character by character, building a current string. As long as the current string is in the dictionary, it continues reading. When the next character would make a string not in the dictionary, it outputs the code for the current string, adds the new string (current + next char) to the dictionary, and resets the current string to the next character.

This adaptive approach allows LZW to learn longer and longer patterns, achieving high compression ratios on repetitive data. The dictionary grows until a predefined maximum size (e.g., 4096 entries for 12-bit codes). After that, the encoder may stop adding new entries or reset the dictionary.

Let's look at a simple example with a string 'ABABABA'. Initial dictionary: A=0, B=1. Encoder steps: - Read A (current='A', in dict). Read B: 'AB' not in dict -> output 0, add 'AB'=2, current='B'. - Read A: 'BA' not in dict -> output 1, add 'BA'=3, current='A'. - Read B: 'AB' in dict -> current='AB'. Read A: 'ABA' not in dict -> output 2, add 'ABA'=4, current='A'. - Read B: 'AB' in dict -> current='AB'. Read A: 'ABA' in dict -> current='ABA'. End of input -> output 4. Output codes: 0,1,2,4. Original length 7, compressed to 4 codes (assuming fixed-width codes).

lzw_compress.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def lzw_compress(data: bytes, max_dict_size: int = 4096) -> list:
    # Build initial dictionary of single bytes
    dictionary = {bytes([i]): i for i in range(256)}
    next_code = 256
    current = b''
    result = []
    for byte in data:
        new_current = current + bytes([byte])
        if new_current in dictionary:
            current = new_current
        else:
            result.append(dictionary[current])
            if next_code < max_dict_size:
                dictionary[new_current] = next_code
                next_code += 1
            current = bytes([byte])
    if current:
        result.append(dictionary[current])
    return result
💡Dictionary Size Matters
📊 Production Insight
In production, always define a maximum dictionary size to prevent unbounded memory growth. Reset the dictionary when full to maintain compression efficiency.
🎯 Key Takeaway
LZW compression builds an adaptive dictionary of substrings, replacing repeated patterns with shorter codes.

LZW Decompression: Rebuilding the Dictionary

LZW decompression reconstructs the original data from the compressed codes. It starts with the same initial dictionary (single bytes). It reads codes one by one. For each code, it outputs the corresponding string from the dictionary. Then it updates the dictionary by adding the previous string plus the first character of the current string. This is the classic LZW decoding algorithm.

A special case occurs when the decoder encounters a code that is not yet in the dictionary. This happens when the encoder added a new entry and immediately used it. The decoder can handle this by using the previous decoded string plus its first character.

Example: Decoding codes [0,1,2,4] from earlier. - Code 0 -> output 'A', prev='A'. Next code 1 -> output 'B', add 'A'+'B'='AB'=2, prev='B'. - Code 2 -> output 'AB', add 'B'+'A'='BA'=3, prev='AB'. - Code 4 -> not in dict yet (next code is 4). Use prev='AB' + first char of prev='A' -> 'ABA'=4. Output 'ABA', add 'AB'+'A'='ABA'=4, prev='ABA'. Output: 'A','B','AB','ABA' -> 'ABABABA'.

lzw_decompress.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def lzw_decompress(codes: list, max_dict_size: int = 4096) -> bytes:
    # Build initial dictionary (reverse mapping)
    dictionary = {i: bytes([i]) for i in range(256)}
    next_code = 256
    result = []
    prev = dictionary[codes[0]]
    result.append(prev)
    for code in codes[1:]:
        if code in dictionary:
            current = dictionary[code]
        else:
            # Special case: code equals next_code
            current = prev + prev[:1]
        result.append(current)
        if next_code < max_dict_size:
            dictionary[next_code] = prev + current[:1]
            next_code += 1
        prev = current
    return b''.join(result)
🔥The KW Trick
📊 Production Insight
Ensure your decoder handles the special case correctly; otherwise, decompression will fail on certain inputs.
🎯 Key Takeaway
LZW decompression mirrors encoding, rebuilding the dictionary using the previous decoded string and the first character of the current string.

Handling Dictionary Overflow and Reset

In practice, the dictionary cannot grow indefinitely. A maximum size is set, typically 4096 entries (12-bit codes) or 65536 (16-bit). When the dictionary reaches its maximum, the encoder must decide what to do. Common strategies: 1. Stop adding new entries: The dictionary becomes static. Compression may degrade for new patterns. 2. Reset the dictionary: Clear all entries except the initial 256 single-byte codes. This allows the algorithm to adapt to new data but loses learned patterns. 3. LRU eviction: Remove least recently used entries, but this adds complexity.

In many implementations (e.g., GIF), the dictionary is reset when full. A special 'clear code' (often 256) is inserted into the output stream to signal the decoder to reset its dictionary. This ensures both encoder and decoder stay synchronized.

Let's implement a version with reset support.

lzw_compress_reset.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def lzw_compress_with_reset(data: bytes, max_dict_size: int = 4096, clear_code: int = 256) -> list:
    dictionary = {bytes([i]): i for i in range(256)}
    next_code = 257  # after clear code
    current = b''
    result = []
    for byte in data:
        new_current = current + bytes([byte])
        if new_current in dictionary:
            current = new_current
        else:
            result.append(dictionary[current])
            if next_code < max_dict_size:
                dictionary[new_current] = next_code
                next_code += 1
            else:
                # Dictionary full: emit clear code and reset
                result.append(clear_code)
                dictionary = {bytes([i]): i for i in range(256)}
                next_code = 257
            current = bytes([byte])
    if current:
        result.append(dictionary[current])
    return result
⚠ Synchronization is Critical
📊 Production Insight
In GIF, the clear code is 256, and the end-of-information code is 257. Ensure your implementation respects these standards for interoperability.
🎯 Key Takeaway
Dictionary reset is essential for handling overflow and maintaining compression on new data patterns.

Variable-Length Codes and Bit Packing

LZW codes are often stored as variable-length binary strings to save space. Initially, codes are 9 bits (since we have 256+2 codes). As the dictionary grows, the code width increases up to a maximum (e.g., 12 bits). This requires careful bit packing.

For simplicity, many tutorials use fixed-width codes (e.g., 12 bits), but that wastes space for small dictionaries. In production, you'll need to implement a bitstream that writes codes with varying bit lengths.

Here's a simple bit packing approach: start with 9 bits, and when the next code exceeds the current maximum representable value, increase the bit width by 1. The decoder must track the same bit width changes.

Example: Initial max code = 257 (9 bits). When next_code reaches 512, switch to 10 bits, etc.

lzw_bitpacking.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def write_codes(codes: list, max_bits: int = 12) -> bytes:
    # Simplified: pack codes into bytes using variable bit length
    # Start with 9 bits, increase as needed
    bit_width = 9
    max_code = (1 << bit_width) - 1
    buffer = 0
    bits_in_buffer = 0
    output = []
    for code in codes:
        # Increase bit width if necessary
        while code > max_code and bit_width < max_bits:
            bit_width += 1
            max_code = (1 << bit_width) - 1
        buffer = (buffer << bit_width) | code
        bits_in_buffer += bit_width
        while bits_in_buffer >= 8:
            bits_in_buffer -= 8
            output.append((buffer >> bits_in_buffer) & 0xFF)
    if bits_in_buffer > 0:
        output.append((buffer << (8 - bits_in_buffer)) & 0xFF)
    return bytes(output)
💡Bit Packing Complexity
📊 Production Insight
Many LZW implementations (like GIF) use a fixed maximum of 12 bits. Stick to standards to avoid interoperability issues.
🎯 Key Takeaway
Variable-length codes improve compression but add complexity. Ensure encoder and decoder agree on when to increase bit width.

Real-World Applications and Trade-offs

LZW is used in
  • GIF images: Compresses graphics with limited colors.
  • TIFF images: Optional compression method.
  • Unix compress: The 'compress' utility uses LZW.
  • PDF: Can use LZW for text streams.
Trade-offs
  • Advantages: Simple, fast, good compression for repetitive data, no need to store dictionary.
  • Disadvantages: Patented (expired now), not as good as modern algorithms like DEFLATE (used in PNG, gzip), dictionary size limits compression on large files.

LZW is a great educational tool for understanding adaptive dictionary compression. For new projects, consider using DEFLATE or LZMA, but LZW's simplicity makes it a classic.

🔥Patent History
📊 Production Insight
When implementing LZW for a new system, consider whether a modern algorithm like DEFLATE would be more appropriate.
🎯 Key Takeaway
LZW is a foundational algorithm still used in legacy formats. Modern alternatives offer better compression but are more complex.

Performance Analysis and Optimization

LZW compression time is O(n) where n is input length, assuming dictionary lookup is O(1) (hash table). Decompression is also O(n). Memory is O(dictionary size).

Optimizations
  • Use a trie for dictionary to speed up prefix searches.
  • Pre-allocate dictionary to avoid resizing overhead.
  • Use integer codes instead of byte strings for keys.
  • For large files, reset dictionary periodically to limit memory.

Example: Using a trie for dictionary can improve speed for long matches.

lzw_trie.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class TrieNode:
    def __init__(self):
        self.children = {}
        self.code = None

def lzw_compress_trie(data: bytes, max_dict_size: int = 4096) -> list:
    root = TrieNode()
    for i in range(256):
        node = TrieNode()
        node.code = i
        root.children[i] = node
    next_code = 256
    current = root
    result = []
    for byte in data:
        if byte in current.children:
            current = current.children[byte]
        else:
            result.append(current.code)
            if next_code < max_dict_size:
                new_node = TrieNode()
                new_node.code = next_code
                current.children[byte] = new_node
                next_code += 1
            current = root.children[byte]
    if current is not root:
        result.append(current.code)
    return result
💡Trie vs Hash Table
📊 Production Insight
Profile your implementation: dictionary lookups are the bottleneck. Use efficient data structures and avoid excessive memory allocation.
🎯 Key Takeaway
LZW is linear time and can be optimized with data structures like tries.
● Production incidentPOST-MORTEMseverity: high

The GIF That Wouldn't Load: A Dictionary Reset Bug

Symptom
Users saw garbled images or incomplete loading in a web application.
Assumption
The developer assumed the dictionary could grow indefinitely without resetting.
Root cause
The LZW encoder did not reset the dictionary when it reached maximum size, causing code collisions.
Fix
Implement dictionary reset when it reaches a predefined maximum size (e.g., 4096 entries).
Key lesson
  • Always define a maximum dictionary size to prevent memory overflow.
  • Reset the dictionary when full to maintain compression efficiency.
  • Test with edge cases like highly repetitive data.
  • Document the maximum code width (e.g., 12 bits) to ensure interoperability.
  • Use clear code to indicate reset in the output stream.
Production debug guideSymptom to Action3 entries
Symptom · 01
Decompressed output is corrupted or mismatched with input.
Fix
Check dictionary reset logic and ensure encoder/decoder use same maximum size.
Symptom · 02
Compression ratio is worse than expected.
Fix
Verify that dictionary is being built correctly; consider increasing dictionary size or resetting less frequently.
Symptom · 03
Memory usage spikes during compression.
Fix
Limit dictionary size and implement LRU eviction or reset.
★ Quick Debug Cheat SheetCommon LZW issues and immediate fixes.
Decompression fails with 'code not in dictionary'
Immediate action
Check that encoder and decoder use same initial dictionary and reset logic.
Commands
print(len(dictionary))
print(max_code)
Fix now
Ensure decoder handles the special case where code equals next available code (KW trick).
Compressed output is larger than input+
Immediate action
Check for too frequent dictionary resets or small dictionary size.
Commands
print(compression_ratio)
print(dictionary_size)
Fix now
Increase dictionary max size or reduce reset frequency.
Infinite loop during decompression+
Immediate action
Check that input stream is not corrupted and that codes are within valid range.
Commands
print(current_code)
print(next_code)
Fix now
Add bounds checking and break if code exceeds dictionary size.
AlgorithmTypeDictionaryCompression RatioSpeed
LZWDictionary-basedAdaptiveGood for repetitive dataFast
LZ77Sliding windowNo explicit dictionaryBetter for general dataFast
DEFLATECombinationLZ77 + HuffmanExcellentModerate
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
lzw_compress.pydef lzw_compress(data: bytes, max_dict_size: int = 4096) -> list:How LZW Compression Works
lzw_decompress.pydef lzw_decompress(codes: list, max_dict_size: int = 4096) -> bytes:LZW Decompression
lzw_compress_reset.pydef lzw_compress_with_reset(data: bytes, max_dict_size: int = 4096, clear_code: ...Handling Dictionary Overflow and Reset
lzw_bitpacking.pydef write_codes(codes: list, max_bits: int = 12) -> bytes:Variable-Length Codes and Bit Packing
lzw_trie.pyclass TrieNode:Performance Analysis and Optimization

Key takeaways

1
LZW is an adaptive dictionary-based compression algorithm that learns patterns during encoding.
2
Decompression rebuilds the dictionary using the previous decoded string and first character of current string.
3
Dictionary overflow handling (reset or stop) is critical for production use.
4
Variable-length codes improve compression but add complexity.
5
LZW is still used in legacy formats like GIF and TIFF.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain how LZW compression works with an example.
Q02SENIOR
What is the special case in LZW decompression when a code is not in the ...
Q03SENIOR
How would you implement dictionary reset in LZW?
Q04JUNIOR
What are the time and space complexities of LZW?
Q05SENIOR
How does variable-length coding improve LZW compression?
Q01 of 05JUNIOR

Explain how LZW compression works with an example.

ANSWER
LZW builds a dictionary of substrings. It reads input, finds longest match in dictionary, outputs its code, and adds the match plus next character to dictionary. Example: 'ABABABA' compresses to codes 0,1,2,4.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Is LZW lossless?
02
What is the maximum dictionary size in LZW?
03
How does LZW handle dictionary overflow?
04
Can LZW compress any data?
05
What is the difference between LZ77 and LZW?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Drawn from code that ran under real load.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Data Compression. Mark it forged?

3 min read · try the examples if you haven't

Previous
Huffman Coding Deep-Dive: Prefix Codes and Canonical Huffman
6 / 7 · Data Compression
Next
Deflate Compression: Combining LZ77 and Huffman Coding