Encryption python projects reach for most often is handled through the third-party cryptography package, since Python's standard library deliberately doesn't ship a full encryption API — hashlib covers hashing, but symmetric and asymmetric encryption require an external, well-vetted library. That's actually a feature, not a gap: cryptographic implementations are easy to get subtly wrong, and steering developers toward one maintained, audited library instead of a dozen half-finished stdlib primitives reduces the number of ways a project ends up with home-rolled, broken crypto in production.
What library should you actually use for encryption in Python?
Use the cryptography package (pip install cryptography), maintained by the Python Cryptographic Authority. It provides two tiers: a high-level "recipes" layer, including Fernet, which bundles AES encryption, authentication, and safe defaults into a single simple API, and a lower-level "hazmat" layer for when you need direct control over specific algorithms and modes — the name is a deliberate warning that this layer is easy to misuse if you don't already understand the cryptographic primitives involved. For the overwhelming majority of application use cases — encrypting a config value, a token, a small blob of sensitive data at rest — Fernet is the right starting point precisely because it removes the decisions (which mode, how to generate the IV, whether to add authentication) that are also the most common source of mistakes.
How does symmetric encryption work in Python with Fernet?
Fernet generates a random key, and that same key both encrypts and decrypts, which is why key management and storage — not the encryption call itself — is usually the harder engineering problem. Under the hood, Fernet uses AES in CBC mode with a 128-bit key alongside HMAC for authentication, meaning it already provides integrity checking, so a tampered ciphertext raises an error at decryption time rather than silently returning garbage plaintext. Generating a key, encrypting a message, and decrypting it back is a handful of lines: generate the key once with Fernet.generate_key(), store it somewhere the application can retrieve at runtime (a secrets manager, not a hardcoded string in source), and reuse the same key object for both the encrypt and decrypt calls. The practical mistakes to avoid are hardcoding the key in source code (it will end up in git history even after rotation), generating a new key per encryption operation instead of per application/tenant (which makes decryption impossible without tracking every key), and logging the key or the plaintext alongside the ciphertext during debugging.
When should you reach for the lower-level hazmat layer instead?
Reach for hazmat only when Fernet's defaults genuinely don't fit your requirements — for example, when you need AES-GCM specifically for interoperability with another system, need a different key size, or need to encrypt very large streaming data without Fernet's framing overhead. If you do use hazmat directly, the same rules apply as in any language: use AES in GCM mode (an authenticated mode) rather than raw CBC or ECB, generate a fresh, cryptographically random nonce for every encryption operation using os.urandom() (never reuse a nonce with the same key under GCM — doing so catastrophically breaks its security guarantees), and never implement your own padding, mode, or key-derivation scheme from scratch. If you're not confident about the answer to "which mode, which padding, how is the nonce generated," that's a strong signal you should be using the recipes layer, not hazmat.
How does asymmetric encryption work in Python, and when do you need it?
Asymmetric encryption (RSA, or elliptic-curve schemes like ECDH for key exchange) is for cases where the encrypting party and decrypting party shouldn't share the same secret key — most commonly, encrypting data with a recipient's public key so only their corresponding private key can decrypt it, without ever transmitting a shared secret over the wire. The cryptography package's hazmat layer supports RSA with OAEP padding (padding.OAEP), which is the modern, secure padding choice — avoid PKCS#1 v1.5 padding for new encryption code, since it's vulnerable to padding-oracle style attacks in certain implementations. In practice, most applications use asymmetric encryption not to encrypt bulk data directly (RSA is slow and has strict size limits on what it can encrypt in one operation) but to encrypt a randomly generated symmetric key, which then encrypts the actual payload — the same hybrid approach TLS itself uses.
What are the most common encryption mistakes in Python code?
The recurring mistakes are: using random instead of secrets or os.urandom for anything cryptographic (Python's random module is explicitly documented as unsuitable for security purposes); hardcoding encryption keys directly in source files or environment-variable defaults committed to a repo; reusing nonces or IVs across multiple encryption operations with the same key; and reaching for hashlib.md5 or hashlib.sha1 for anything security-sensitive, since both are cryptographically broken for collision resistance. A static analysis pass that specifically flags these patterns — weak randomness sources, legacy hash functions, hardcoded key-shaped string literals — catches the majority of encryption mistakes in Python code well before they reach a security review.
How Safeguard Helps
Safeguard's static analysis flags exactly these patterns in Python codebases — calls to insecure random sources feeding cryptographic operations, legacy hash algorithms used in security-sensitive contexts, and hardcoded secrets or key material committed to source — directly in pull requests. See the SAST/DAST product page for how this static coverage works across Python and other languages, and check out Safeguard Academy for deeper secure-coding guides.
FAQ
Is Fernet secure enough for production use?
Yes — Fernet uses AES-128-CBC with HMAC authentication and is designed specifically to remove the common mistakes (mode selection, IV generation, missing authentication) that cause most real-world encryption bugs. It's appropriate for the large majority of application-level encryption needs.
Can I use Python's built-in hashlib for encryption?
No — hashlib only provides one-way hash functions (SHA-256, SHA-3, and legacy MD5/SHA-1), which cannot be reversed to recover original data. Encryption, which is reversible with the correct key, requires a library like cryptography.
Why shouldn't I use Python's random module for encryption keys?
Python's random module uses a Mersenne Twister PRNG that is fast and well-distributed for simulations but not unpredictable enough for security purposes — its output can potentially be predicted by an attacker. Use secrets or os.urandom for anything cryptographic.
What's the difference between encoding and encryption in Python?
Encoding (like Base64) is a reversible transformation for compatibility, not security — anyone can decode it without a key. Encryption requires a secret key to reverse, and it's a common and serious mistake to confuse Base64 encoding with actual encryption when handling sensitive data.