Technology  /  Python

🐍 Python 78 guides · updated 2026

From first variable to OOP, generators, and real projects — the language that runs everything from data pipelines to AI agents, taught the practical way.

AES-256 Encryption in Python: PyCryptodome, Fernet, and Choosing the Right Tool

AES-256 is the symmetric encryption standard used by governments, financial systems, and messaging apps. Symmetric means the same key encrypts and decrypts — simpler than asymmetric (RSA) and fast enough for large data. Python gives you two main libraries for this: pycryptodome for low-level control, and cryptography (Fernet) for a high-level safe API.

Installing the Libraries

Terminal window
pip install pycryptodome
pip install cryptography

Approach 1: PyCryptodome with CBC Mode

CBC (Cipher Block Chaining) is the most commonly discussed AES mode. Each block of plaintext is XORed with the previous ciphertext block before encryption, making the output non-deterministic even for identical inputs.

You need three things: a 32-byte key, a random 16-byte IV (Initialization Vector), and padding to fill the last block.

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
import base64
def encrypt_aes_cbc(plaintext: str, key: bytes) -> str:
"""Encrypt a string with AES-256 CBC. Returns base64-encoded ciphertext."""
iv = get_random_bytes(16) # random IV, not secret
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(plaintext.encode(), AES.block_size))
# Prepend IV to ciphertext so the decryptor can read it
return base64.b64encode(iv + ciphertext).decode()
def decrypt_aes_cbc(token: str, key: bytes) -> str:
"""Decrypt a base64-encoded AES-256 CBC ciphertext."""
raw = base64.b64decode(token)
iv = raw[:16] # extract the prepended IV
ciphertext = raw[16:]
cipher = AES.new(key, AES.MODE_CBC, iv)
return unpad(cipher.decrypt(ciphertext), AES.block_size).decode()
# Generate a random 32-byte (256-bit) key
key = get_random_bytes(32)
message = "Confidential: project launch is Q3 2026"
encrypted = encrypt_aes_cbc(message, key)
decrypted = decrypt_aes_cbc(encrypted, key)
print("Original: ", message)
print("Encrypted:", encrypted)
print("Decrypted:", decrypted)

The IV does not need to be secret, but it must be unique per encryption. Prepending it to the ciphertext is the standard practice.

Approach 2: Fernet — Simpler and Safer for Most Use Cases

Fernet is a high-level encryption recipe from the cryptography library. It handles IVs, padding, and authentication automatically. It uses AES-128 under the hood (not 256), but the authentication guarantee makes it safer against misuse than a raw CBC implementation.

from cryptography.fernet import Fernet
# Generate and save the key — losing it means losing the data
key = Fernet.generate_key()
print("Key (save this):", key.decode())
cipher = Fernet(key)
message = b"API secret: sk-prod-a1b2c3d4"
token = cipher.encrypt(message)
print("Encrypted token:", token.decode())
recovered = cipher.decrypt(token)
print("Decrypted:", recovered.decode())

Fernet tokens include an HMAC, so tampering with the ciphertext causes decryption to fail with an exception rather than returning garbage. This is called authenticated encryption, and it is a meaningful security improvement over plain CBC.

Approach 3: Password-Derived Key with PBKDF2

In practice, you rarely hand a random 32-byte key to a user. Instead, derive the key from a password using a key derivation function. PBKDF2 slows down brute-force attacks by making key derivation computationally expensive.

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from Crypto.Random import get_random_bytes
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA256
import base64
def derive_key(password: str, salt: bytes) -> bytes:
"""Derive a 32-byte key from a password using PBKDF2-SHA256."""
return PBKDF2(password, salt, dkLen=32, count=200_000, hmac_hash_module=SHA256)
def encrypt_with_password(plaintext: str, password: str) -> str:
"""Encrypt using a password. Stores salt + IV + ciphertext in one token."""
salt = get_random_bytes(16)
key = derive_key(password, salt)
iv = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(plaintext.encode(), AES.block_size))
return base64.b64encode(salt + iv + ciphertext).decode()
def decrypt_with_password(token: str, password: str) -> str:
"""Decrypt a token produced by encrypt_with_password."""
raw = base64.b64decode(token)
salt, iv, ciphertext = raw[:16], raw[16:32], raw[32:]
key = derive_key(password, salt)
cipher = AES.new(key, AES.MODE_CBC, iv)
return unpad(cipher.decrypt(ciphertext), AES.block_size).decode()
password = "correct-horse-battery-staple"
secret = "Database password: hunter2"
token = encrypt_with_password(secret, password)
result = decrypt_with_password(token, password)
print("Recovered:", result)

The count=200_000 parameter makes each key derivation take roughly 0.1 seconds on modern hardware. That is fine for legitimate users logging in once, but devastating for an attacker trying millions of password guesses.

Which Approach to Use

SituationRecommendation
General symmetric encryptionFernet — authenticated, handles edge cases
Need AES-256 specificallyPyCryptodome CBC with GCM mode (authenticated)
Encrypting with a user passwordPBKDF2 + AES (Approach 3 above)
File encryption at restFernet or AES-GCM

Security note: never reuse an IV with the same key. Never store the key alongside the encrypted data. For anything production-critical, use a secrets manager (AWS Secrets Manager, HashiCorp Vault) rather than managing keys manually.