PythonMastery
Python programs · Useful programs · intermediate

Run-length encoding in Python

Compress repeated characters with run-length encoding, so AAABBC becomes 3A2B1C, and decode it back. Also the itertools.groupby version.

Run-length encoding (RLE) stores each run of the same character as a count and the character: AAABBC becomes 3A2B1C. It is one of the simplest ways to compress data, and it works well when the data has long runs, such as rows of pixels in a simple image.

The program

python
import re


def rle_encode(text):
    if not text:
        return ""
    parts = []
    current = text[0]
    count = 1
    for ch in text[1:]:
        if ch == current:
            count += 1
        else:
            parts.append(f"{count}{current}")
            current = ch
            count = 1
    parts.append(f"{count}{current}")  # the last run has no character after it
    return "".join(parts)


def rle_decode(code):
    return "".join(ch * int(count) for count, ch in re.findall(r"(\d+)(\D)", code))


for text in ["AAABBBCCDDDDEE", "WWWWWWWWWWWWBWWW", "abc", ""]:
    code = rle_encode(text)
    print(f"{text!r} -> {code!r} -> {rle_decode(code)!r}")
output
'AAABBBCCDDDDEE' -> '3A3B2C4D2E' -> 'AAABBBCCDDDDEE'
'WWWWWWWWWWWWBWWW' -> '12W1B3W' -> 'WWWWWWWWWWWWBWWW'
'abc' -> '1a1b1c' -> 'abc'
'' -> '' -> ''

How it works

This format cannot store digits in the text itself: "111" encodes to "31", and rle_decode("31") gives "", because the pattern finds no letter after the digits. The original text is lost. Real formats store the count in a separate byte to avoid this.

Another way: itertools.groupby

itertools.groupby() splits a string into runs of the same character for you:

python
from itertools import groupby


def rle_encode(text):
    return "".join(f"{len(list(run))}{ch}" for ch, run in groupby(text))


print(rle_encode("AAABBBCCDDDDEE"))
output
3A3B2C4D2E

Learn the idea: Working with Strings, Loops: for, while, and the Pythonic Patterns, Regular Expressions