String Compression in Python: Run-Length Encoding and zlib

Quick answer: For a simple run-length encoding, group adjacent equal characters and store each character with its consecutive count. This reduces repeated runs but is not the same as general-purpose byte compression such as zlib.

Python string compression infographic showing adjacent runs, groupby, count-symbol decoding, zlib, measurement, and edge cases
Run-length encoding is useful for repeated neighbors, not a universal replacement for byte compression.

String compression in Python usually means replacing repeated characters with a shorter representation. A common beginner-friendly technique is run-length encoding. It reads a string from left to right, counts adjacent repeated characters, and writes each character with its count. For example, "aaabbc" can become "a3b2c1".

This method is useful for learning loops, indexes, grouping, input checks, and reversible transforms. It is not a universal compression system like gzip. Run-length encoding works best when the text has long repeated runs, such as simple masks, game boards, bitmap-like data, or generated labels. It can make normal prose longer because most letters do not repeat many times in a row. For an implementation-focused walkthrough of the same compression method, Run Length Encoding in Python Guide develops the value-and-count representation step by step.

The official itertools.groupby documentation explains the grouped iterator used later in this guide. The Python re documentation is useful when validating encoded text with a pattern.

Before writing code, decide whether the compressed form must always be shorter. Some interview questions return the original string when compression does not save space. Other lessons always return the encoded form so the result is easier to inspect. Also decide whether counts may have more than one digit. Real examples need to handle "aaaaaaaaaaaa" as "a12", not as a single-digit count.

Compress A String With A Loop

The clearest approach keeps the current character and a count. When the next character changes, append the finished run to a list and start counting the new character.

def compress_text(text):
    if not text:
        return ""

    parts = []
    current = text[0]
    count = 1

    for char in text[1:]:
        if char == current:
            count += 1
        else:
            parts.append(current + str(count))
            current = char
            count = 1

    parts.append(current + str(count))
    return "".join(parts)

print(compress_text("aaabbcccc"))

This prints a3b2c4. The final append after the loop matters because the last run has no later character to trigger the change branch.

Building a list and joining it is better than repeatedly extending one string inside the loop. Strings are immutable, so list collection keeps the example efficient and readable.

Return The Original When It Is Shorter

Many coding challenges ask for compression only when it saves space. In that case, compute the encoded form, compare lengths, and return whichever string is shorter.

def compress_text(text):
    if not text:
        return ""

    parts = []
    current = text[0]
    count = 1

    for char in text[1:]:
        if char == current:
            count += 1
        else:
            parts.append(current + str(count))
            current = char
            count = 1

    parts.append(current + str(count))
    return "".join(parts)

def compress_if_shorter(text):
    encoded = compress_text(text)
    if len(encoded) < len(text):
        return encoded
    return text

print(compress_if_shorter("abcdef"))
print(compress_if_shorter("aaaaabbbbcc"))

The first call returns the original text because a1b1c1d1e1f1 would be longer. The second call returns a compressed value because repeated runs create a clear saving.

This policy is useful in user-facing code. It avoids surprising output when the source text has no useful repetition, while still keeping the compression helper reusable.

Python Pool infographic showing repeated characters, run counts, encoded pairs, and compact string
Run-length encoding is simple and effective when adjacent characters repeat in long runs.

Decompress Run-Length Encoded Text

A reversible format is easier to test. To decompress, read a character, collect the digits after it, convert those digits to a count, and repeat the character.

def decompress_text(encoded):
    output = []
    index = 0

    while index < len(encoded):
        char = encoded[index]
        index += 1

        digits = []
        while index < len(encoded) and encoded[index].isdigit():
            digits.append(encoded[index])
            index += 1

        if not digits:
            raise ValueError("missing count")
        output.append(char * int("".join(digits)))

    return "".join(output)

print(decompress_text("a3b2c4"))

This prints aaabbcccc. The inner loop allows counts like 12 or 104, so the decoder is not limited to short runs.

The function raises ValueError if a character has no count after it. That makes malformed input fail clearly instead of producing a silent wrong result.

Use itertools.groupby For Compact Code

itertools.groupby() groups adjacent equal values. It can shorten the compression function while keeping the same run-length encoding idea.

from itertools import groupby

def compress_with_groupby(text):
    parts = []
    for char, group in groupby(text):
        count = sum(1 for _ in group)
        parts.append(char + str(count))
    return "".join(parts)

print(compress_with_groupby("zzzzaaaabb"))

This prints z4a4b2. The grouping is adjacent only, so "aaba" becomes a2b1a1, not a3b1.

Use this version when the team already understands iterators. For a first explanation, the manual loop is often easier because every state change is visible.

Python Pool infographic mapping text encoding through zlib.compress, compressed bytes, and decompression
zlib works on bytes, so encode text first and decode the decompressed bytes afterward.

Validate Encoded Input Before Decoding

If encoded text comes from a file, form, or another program, validate it before decoding. A regular expression can confirm that the string is made from repeated character-and-count pairs.

import re

pattern = re.compile(r"(?:[A-Za-z]\d+)+\Z")

def is_encoded_text(text):
    return bool(pattern.fullmatch(text))

print(is_encoded_text("a3b2c4"))
print(is_encoded_text("abc"))

This pattern accepts letters followed by one or more digits. Adjust the character range if your format needs spaces, punctuation, Unicode text, or escape rules.

Validation and decoding should agree on the same format. If the pattern allows a character, the decoder must know how to handle that character without ambiguity.

Python Pool infographic comparing repetitive text, noisy text, compressed size, and round-trip fidelity
Measure compressed size and CPU cost because short or noisy text may not become smaller.

Compress A File-Friendly String

For small text files, read the text, compress it, and write the encoded result. This example uses explicit UTF-8 encoding so behavior is consistent across systems.

from pathlib import Path
from tempfile import TemporaryDirectory

def compress_text(text):
    if not text:
        return ""

    parts = []
    current = text[0]
    count = 1

    for char in text[1:]:
        if char == current:
            count += 1
        else:
            parts.append(current + str(count))
            current = char
            count = 1

    parts.append(current + str(count))
    return "".join(parts)

def compress_file(source_path, target_path):
    text = Path(source_path).read_text(encoding="utf-8")
    encoded = compress_text(text)
    Path(target_path).write_text(encoded, encoding="utf-8")
    return len(text), len(encoded)

with TemporaryDirectory() as folder:
    source = Path(folder) / "input.txt"
    target = Path(folder) / "output.rle"
    source.write_text("aaabbcccc", encoding="utf-8")
    original_size, encoded_size = compress_file(source, target)

print(original_size, encoded_size)

This is a demonstration, not a replacement for standard archive formats. For production storage, compare against built-in and platform tools that handle binary data, metadata, and broad file types.

The main testing rule is to check round trips. If decompress_text(compress_text(value)) equals the original for empty strings, single characters, long runs, and mixed text, the pair is likely behaving as intended.

String compression in Python is most useful as a focused exercise in stateful loops and format design. Start with the manual loop, add the shorter-return policy if the compressed value is user-facing, and include decompression when the result must be read back later.

Run-Length Encode Adjacent Runs

Run-length encoding replaces a run such as aaaa with a symbol and its count. It is useful when repeated values are adjacent, but it can make short or high-entropy text larger because counts and separators add overhead. Define the output format before writing a decoder.

from itertools import groupby

def encode_runs(text):
    parts = []
    for character, group in groupby(text):
        count = sum(1 for _ in group)
        parts.append(f"{count}{character}")
    return "".join(parts)

print(encode_runs("aaabbc"))  # 3a2b1c
Python Pool infographic testing encoding, Unicode, headers, empty data, decompression errors, and validation
Check encoding, metadata, empty input, corruption, decompression errors, and exact round-trip equality.

Decode With A Clear Contract

A decoder must know where the count ends and the symbol begins. A format that allows digits in the original text needs escaping or a structured representation, such as pairs of integers and characters. Test empty input, one-character runs, large counts, and symbols that look like delimiters.

Choose The Right Compression Tool

Run-length encoding is an algorithmic teaching example and can work well for repeated pixels, tokens, or telemetry. For general bytes, compare a standard compressor such as zlib and measure the compressed size and decompression cost on representative data. Compression does not remove the need for an encoding and integrity policy.

Frequently Asked Questions

How do I compress a string in Python?

For a teaching-level run-length encoding, group adjacent equal characters and store each character with its consecutive count; for general bytes, compare zlib.

What is run-length encoding?

Run-length encoding replaces adjacent repetitions such as aaaa with a symbol and count such as 4a.

Does run-length encoding always make a string smaller?

No. Short or high-entropy strings can become larger because counts and separators add overhead.

How should I test a string compression algorithm?

Test empty input, one-character runs, digits and delimiters, large counts, round-trip decoding, and compressed size on representative data.

Subscribe
Notify of
guest
4 Comments
Oldest
Newest Most Voted
James
James
4 years ago

It says “‘i’ is not defined” for the Simple Loop compression program when trying to compress 1 character.

Pratik Kinage
Admin
4 years ago
Reply to  James

Can you try using a different code? Practically it’s hard to compress 1 character unless you have zero-width space characters. Let me know if you still need help with anything.

Regards,
Pratik

bob
bob
3 years ago

how can i make my for loop do it but in a way where it does say 1 for example abbc compressed would be ab2c

Pratik Kinage
Admin
3 years ago
Reply to  bob

Following code will do –

new_string = ""
string = "pythooonnnpool"
count = 1
for i in range(len(string)-1):
if string[i] == string[i+1]:
count = count + 1
else:
new_string = new_string + string[i] + (str(count) if count > 1 else "")
count = 1
new_string = new_string + string[i+1] + (str(count) if count > 1 else "")
print(new_string)

Last edited 3 years ago by Pratik Kinage