Skip to content

How to Encode and Decode Messages in Python

Learn Python encryption techniques with practical examples for Atbash, Caesar, XOR ciphers, and secure Fernet-based encoding for message protection.

Encoding and decoding messages, also known as encryption and decryption, is a fundamental concept in computer science and cybersecurity. Encryption transforms readable text (plaintext) into an unreadable format (ciphertext), while decryption reverses the process to recover the original message.

In this guide, you'll learn multiple methods to encode and decode messages in Python, from simple substitution ciphers to more practical encryption techniques.

Understanding the Atbash Cipher

The simplest substitution cipher we'll implement is the Atbash cipher, which replaces each letter with its reverse in the alphabet:

A ↔ Z    N ↔ M
B ↔ Y    O ↔ L
C ↔ X    P ↔ K
D ↔ W    Q ↔ J
...      ...
M ↔ N    Z ↔ A

A key property of the Atbash cipher is that encryption and decryption use the same operation, applying the cipher twice returns the original text.

The cleanest and most efficient approach uses Python's built-in string translation methods:

def encrypt(message):
    """Encrypt a message using the Atbash cipher."""
    table = str.maketrans(
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
        "zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA"
    )
    return message.translate(table)

decrypt = encrypt

original = "Hello World!"
encrypted = encrypt(original)
print(f"Original:  {original}")
print(f"Encrypted: {encrypted}")

decrypted = decrypt(encrypted)
print(f"Decrypted: {decrypted}")

Output:

Original:  Hello World!
Encrypted: Svool Dliow!
Decrypted: Hello World!

How it works:

  1. str.maketrans() creates a translation table mapping each character to its replacement.
  2. translate() applies the mapping to every character in the string.
  3. Characters not in the table (numbers, spaces, punctuation) are left unchanged.

Method 2: Using a Dictionary Mapping

A dictionary-based approach offers more readability and is easy to customize:

def create_cipher():
    """Create the Atbash cipher mapping."""
    cipher = {}
    for i in range(26):
        cipher[chr(ord('a') + i)] = chr(ord('z') - i)
        cipher[chr(ord('A') + i)] = chr(ord('Z') - i)
    return cipher

def encode(message, cipher):
    """Encode a message using the given cipher dictionary."""
    return ''.join(cipher.get(char, char) for char in message)


cipher = create_cipher()

original = "TutorialReference!"
encoded = encode(original, cipher)
decoded = encode(encoded, cipher)  # Apply again to decode

print(f"Original:  {original}")
print(f"Encoded:   {encoded}")
print(f"Decoded:   {decoded}")

Output:

Original:  TutorialReference!
Encoded:   GfglirzoIvuvivmxv!
Decoded:   TutorialReference!

The cipher.get(char, char) returns the mapped character if it exists, or the original character if it's not in the mapping (preserving spaces, numbers, and punctuation).

Method 3: Caesar Cipher (Shift-Based)

The Caesar cipher shifts each letter by a fixed number of positions. Unlike Atbash, the encryption and decryption use different shift values:

def caesar_encrypt(message, shift):
    """Encrypt using Caesar cipher with a given shift."""
    result = []
    for char in message:
        if char.isalpha():
            base = ord('A') if char.isupper() else ord('a')
            shifted = (ord(char) - base + shift) % 26 + base
            result.append(chr(shifted))
        else:
            result.append(char)
    return ''.join(result)

def caesar_decrypt(message, shift):
    """Decrypt by shifting in the opposite direction."""
    return caesar_encrypt(message, -shift)


shift = 3
original = "Hello World! 123"

encrypted = caesar_encrypt(original, shift)
decrypted = caesar_decrypt(encrypted, shift)

print(f"Original:  {original}")
print(f"Encrypted (shift {shift}): {encrypted}")
print(f"Decrypted: {decrypted}")

Output:

Original:  Hello World! 123
Encrypted (shift 3): Khoor Zruog! 123
Decrypted: Hello World! 123

How it works:

  1. Each letter is shifted forward by shift positions in the alphabet.
  2. The modulo operator (% 26) wraps around so that shifting past Z cycles back to A.
  3. Decryption shifts in the opposite direction (-shift).

Method 4: Using the cryptography Library (Secure Encryption)

For real-world encryption that's actually secure, use the cryptography library with the Fernet symmetric encryption scheme:

pip install cryptography
from cryptography.fernet import Fernet

key = Fernet.generate_key()
cipher = Fernet(key)

original = "This is a secret message!"
encrypted = cipher.encrypt(original.encode())
print(f"Original:  {original}")
print(f"Encrypted: {encrypted.decode()}")

decrypted = cipher.decrypt(encrypted).decode()
print(f"Decrypted: {decrypted}")

Output:

Original:  This is a secret message!
Encrypted: gAAAAABpkKhrFlT7sA6O0DRJiYbmr1g62jmypt0A0rt89YBFSKnB7eU30zE3yuIuadkvQhP3-z1cx27exETyZn5pajwONpakiue9cJksf6K3woycjIYuuPY=
Decrypted: This is a secret message!

Method 5: XOR-Based Encoding

XOR encoding uses a key to flip bits. Applying the same key twice restores the original message:

def xor_encode(message, key):
    """Encode/decode a message using XOR with a repeating key."""
    encoded = []
    for i, char in enumerate(message):
        key_char = key[i % len(key)]
        encoded.append(chr(ord(char) ^ ord(key_char)))
    return ''.join(encoded)


key = "secret"
original = "Hello World!"

encoded = xor_encode(original, key)
print(f"Original: {original}")
print(f"Encoded:  {encoded!r}")  # repr to show non-printable chars

decoded = xor_encode(encoded, key)
print(f"Decoded:  {decoded}")

Output:

Original: Hello World!
Encoded:  ';\x00\x0f\x1e\nT$\n\x11\x1e\x01U'
Decoded:  Hello World!

Building a Complete Encoder/Decoder Tool

Here's a practical tool that supports multiple cipher methods:

def atbash(text):
    """Atbash cipher - symmetric encryption/decryption."""
    table = str.maketrans(
        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
        "zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA"
    )
    return text.translate(table)

def caesar(text, shift, decrypt=False):
    """Caesar cipher with configurable shift."""
    if decrypt:
        shift = -shift
    result = []
    for char in text:
        if char.isalpha():
            base = ord('A') if char.isupper() else ord('a')
            result.append(chr((ord(char) - base + shift) % 26 + base))
        else:
            result.append(char)
    return ''.join(result)


message = "Python is amazing!"

print(f"Original:        {message}")
print(f"Atbash encoded:  {atbash(message)}")
print(f"Atbash decoded:  {atbash(atbash(message))}")
print(f"Caesar(5) enc:   {caesar(message, 5)}")
print(f"Caesar(5) dec:   {caesar(caesar(message, 5), 5, decrypt=True)}")

Output:

Original:        Python is amazing!
Atbash encoded:  Kbgslm rh znzarmt!
Atbash decoded:  Python is amazing!
Caesar(5) enc:   Udymts nx frfensl!
Caesar(5) dec:   Python is amazing!

Quick Comparison of Methods

Method Secure Symmetric Complexity Best For
Atbash (maketrans) ❌ ✅ (same operation) Simple Learning, puzzles
Caesar cipher ❌ ❌ (need shift direction) Simple Learning, basic obfuscation
XOR encoding ❌ ✅ (same operation) Moderate Simple obfuscation
cryptography (Fernet) ✅ ✅ (same key) Library-based Production security

Conclusion

Python provides multiple ways to encode and decode messages:

  • Use str.maketrans() + translate() for the cleanest implementation of substitution ciphers. It's fast and Pythonic.
  • Use a dictionary mapping when you need a customizable cipher that's easy to modify.
  • Use the Caesar cipher to learn about shift-based encryption with separate encrypt/decrypt operations.
  • Use the cryptography library (Fernet) for any real-world encryption needs. Never rely on simple ciphers for actual security.

The substitution ciphers shown here are excellent for learning encryption concepts, building puzzles, or simple data obfuscation, but always use established cryptographic libraries when security truly matters.

Navigation