python5 min read

Tutorial: Learn Python Cryptography from Scratch (2026)

Tutorial: Learn Python Cryptography from Scratch (2026)

Published:  |  Category: Python  |  Reading time: ~15 min
Tutorial: Learn Python Cryptography from Scratch (2026)

I first needed cryptography when building an end-to-end encrypted messaging feature for a health app. Getting crypto wrong can compromise user data and destroy trust. Python's cryptography library provides high-level recipes (Fernet) for common use cases and low-level primitives for custom implementations. Understanding the fundamentals — symmetric encryption, asymmetric encryption, hashing, and digital signatures — is essential for any developer handling sensitive data.

This tutorial covers practical cryptography in Python: encrypting files with symmetric keys, hashing passwords securely, generating and verifying digital signatures, and the difference between encryption, encoding, and hashing. I'll focus on doing things correctly — using authenticated encryption, constant-time comparisons, and proper key management.

Symmetric Encryption with Fernet

Fernet provides authenticated symmetric encryption using AES-128-CBC with HMAC-SHA256 for integrity. It generates a URL-safe base64-encoded key from which both encryption and authentication keys are derived. To encrypt, call fernet.encrypt(data) which returns a token containing the IV, ciphertext, and HMAC. Decryption verifies the HMAC first, so tampered ciphertext is rejected. Use Fernet for encrypting files, database fields, or configuration secrets.

from cryptography.fernet import Fernet

# Generate a key (keep this secret!)
key = Fernet.generate_key()
cipher = Fernet(key)

# Encrypt
message = b"Sensitive data"
token = cipher.encrypt(message)
print(f"Encrypted: {token}")

# Decrypt
decrypted = cipher.decrypt(token)
print(f"Decrypted: {decrypted}")

# File encryption
with open('secret.txt', 'rb') as f:
    data = f.read()
encrypted = cipher.encrypt(data)
with open('secret.enc', 'wb') as f:
    f.write(encrypted)

Password Hashing with bcrypt and Argon2

Never store passwords in plaintext or with simple hashes like SHA-256. Password hashing algorithms are intentionally slow (key derivation functions) to resist brute-force attacks. Bcrypt is widely supported and easy to use. Argon2, the winner of the Password Hashing Competition, is more resistant to GPU and side-channel attacks. Both include a salt automatically to prevent rainbow table attacks.

import bcrypt

# Hash a password
password = b"user_password123"
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password, salt)
print(f"Hashed: {hashed}")

# Verify
if bcrypt.checkpw(password, hashed):
    print("Password matches!")

# Using Argon2
from argon2 import PasswordHasher

ph = PasswordHasher()
hash = ph.hash("user_password123")
print(f"Argon2 hash: {hash}")

# Verify
ph.verify(hash, "user_password123")

Asymmetric Encryption with RSA

Asymmetric (public-key) encryption uses a key pair: a public key for encryption and a private key for decryption. RSA is the most common algorithm. The cryptography library lets you generate keys, encrypt with the public key, and decrypt with the private key. RSA is slow for large data, so it's typically used to encrypt an AES session key, which then encrypts the actual data (hybrid encryption).

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes

# Generate RSA key pair
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
)
public_key = private_key.public_key()

# Encrypt with public key
message = b"Secret message for recipient"
ciphertext = public_key.encrypt(
    message,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

# Decrypt with private key
plaintext = private_key.decrypt(
    ciphertext,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)
print(plaintext.decode())

Digital Signatures and Verification

Digital signatures prove authenticity and integrity. The signer uses a private key to sign data, and anyone with the public key can verify the signature. This is the foundation of SSL/TLS certificates, software updates, and code signing. The cryptography library supports ECDSA and RSA signatures. I sign API payloads to prevent tampering and verify signatures from webhook callbacks.

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes

# Generate ECDSA key pair (modern alternative to RSA)
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()

# Sign data
message = b"Important document"
signature = private_key.sign(
    message,
    ec.ECDSA(hashes.SHA256())
)

# Verify signature
try:
    public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
    print("Signature is valid!")
except:
    print("Signature is INVALID!")

# Tampered message fails verification
public_key.verify(signature, b"Tampered document", ec.ECDSA(hashes.SHA256()))
# Raises InvalidSignature

Hashing and HMAC for Integrity

Cryptographic hash functions (SHA-256, SHA-3, Blake2) produce a fixed-size digest from arbitrary data. Hashes are one-way and collision-resistant. Use them for file integrity checks, data deduplication, and content addressing. HMAC (Hash-based Message Authentication Code) combines a hash with a secret key, providing both integrity and authenticity. It's used in API authentication (AWS Signature V4) and JWT tokens.

import hashlib
import hmac

# SHA-256 hash
data = b"Hello, world!"
digest = hashlib.sha256(data).hexdigest()
print(f"SHA-256: {digest}")

# File integrity
sha256 = hashlib.sha256()
with open('file.bin', 'rb') as f:
    for chunk in iter(lambda: f.read(4096), b''):
        sha256.update(chunk)
print(f"File hash: {sha256.hexdigest()}")

# HMAC for API signing
secret = b"shared-secret-key"
message = b"GET|/api/users|timestamp=1700000000"
hmac_digest = hmac.new(secret, message, hashlib.sha256).hexdigest()
print(f"HMAC: {hmac_digest}")

Key Management and Safe Storage

Cryptography is only as strong as key management. Never hardcode keys in source code. Use environment variables, vault services (HashiCorp Vault, AWS KMS), or encrypted configuration files. For local development, store keys in a .env file excluded from version control. Rotate keys regularly and keep backups. The cryptography library provides key serialization (PEM format) for storing keys on disk with password protection.

from cryptography.hazmat.primitives import serialization

# Serialize private key (PEM, password-protected)
pem_data = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.BestAvailableEncryption(b'strong-password')
)
with open('private_key.pem', 'wb') as f:
    f.write(pem_data)

# Load private key back
from cryptography.hazmat.primitives import serialization

with open('private_key.pem', 'rb') as f:
    loaded_key = serialization.load_pem_private_key(
        f.read(),
        password=b'strong-password'
    )

# Env var pattern (never hardcode!)
import os
API_SECRET = os.environ.get('API_SECRET_KEY')
if not API_SECRET:
    raise ValueError("API_SECRET_KEY not set")

Frequently Asked Questions

What is the difference between hashing and encryption?

Hashing is one-way and deterministic — you cannot recover the original input. Encryption is two-way — ciphertext can be decrypted with the key. Use hashing for passwords and integrity checks. Use encryption for confidential data.

Should I use AES-GCM or AES-CBC?

Use AES-GCM (Galois/Counter Mode) — it provides authenticated encryption (confidentiality + integrity) in one mode. AES-CBC requires a separate HMAC for authentication, which is error-prone. Fernet uses AES-CBC + HMAC correctly, but GCM is simpler.

How long should cryptographic keys be?

RSA: 2048 bits minimum (3072 recommended). ECDSA: 256 bits (P-256) is sufficient. AES: 128 bits is adequate; 256 bits for extra margin. These recommendations are from NIST in 2024.

What is forward secrecy?

Forward secrecy ensures that if a long-term private key is compromised, past session keys cannot be derived. Achieved with ephemeral Diffie-Hellman key exchange (ECDHE). Used in TLS 1.3. Important for any protocol that encrypts messages.

Originally published on Ayodhyyya. Last updated June 1, 2026.