DEV Community

Cover image for How to Hash Passwords in Python and Encrypt Sensitive Data the Right Way
Developer Service
Developer Service

Posted on • Originally published at developer-service.blog

How to Hash Passwords in Python and Encrypt Sensitive Data the Right Way

You’re building your first serious application, a chat app, a password manager, maybe even an e-commerce platform. Everything looks solid until someone asks:

“How are user passwords stored?”

That’s when it hits you: plain text.

It’s a mistake many developers make early on. But here’s the good news: Python makes secure password handling surprisingly straightforward.

In this guide, you’ll learn how to implement cryptography correctly and avoid one of the most common (and dangerous) beginner mistakes.


Why This Matters to You

Before we dive into the code, let’s talk about why cryptography matters to you as a developer.

Every day, your applications handle sensitive data, passwords, API keys, personal details, possible payment information. Without proper encryption, that data is essentially left unprotected.

And the stakes are high. A data breach isn’t just a PR nightmare; it can mean broken user trust, legal consequences, regulatory fines, and real harm to real people. In 2025, the average cost of a data breach reached $4.4 million. But beyond the numbers, there’s a more important reality: users are trusting you with their private information.

The good news? You don’t need a PhD in mathematics to handle cryptography correctly. You just need to know which tools to use and when to use them. Python’s cryptography ecosystem makes strong, modern security accessible to developers at any experience level.


Getting Started

The cryptography library is your go-to tool for secure cryptographic operations in Python.

It’s actively maintained by security professionals, widely trusted in production systems, and most importantly, designed to be difficult to misuse.

pip install cryptography
Enter fullscreen mode Exit fullscreen mode

With it installed, let’s take a look at what you can actually build.


Hashing Passwords: Your First Line of Defense

Let's start with the most common use case: storing passwords. You should never store passwords in plain text.

Here's what you need to understand: hashing is a one-way function. You can turn a password into a hash, but you can't reverse it back. When a user logs in, you hash their input and compare it to the stored hash. If they match, the password is correct. This means even if someone steals your database, they can't get the actual passwords.

But not all hashing is created equal. You need PBKDF2, bcrypt, or Argon2, algorithms specifically designed for passwords.

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import os

def hash_password(password: str) -> tuple[bytes, bytes]:
    salt = os.urandom(16)  # Random salt for uniqueness

    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=480000,  # OWASP recommended minimum
    )

    hashed = kdf.derive(password.encode())
    return salt, hashed

# Store both salt and hash with the user record
salt, hashed = hash_password("MySecurePassword123!")
print(f"Salt: {salt.hex()}")
print(f"Hashed: {hashed.hex()}")
Enter fullscreen mode Exit fullscreen mode

Here's what makes this secure: First, the salt ensures that even if two users have identical passwords, their hashes will be completely different. Without salt, attackers could use rainbow tables, precomputed databases of password hashes, to crack passwords instantly. The salt makes every hash unique, forcing attackers to start from scratch for each password.

Second, PBKDF2 is intentionally slow with 480,000 iterations. This might seem counterintuitive, but it's brilliant. Each login takes a fraction of a second longer, unnoticeable to users. But for attackers trying billions of password combinations? It becomes computationally infeasible. They can't try millions of passwords per second anymore.

Finally, you store both the salt and the hash with each user record in your database. When a user logs in, you retrieve their salt, hash their input password with it, and compare the result. No plaintext passwords ever touch your database.

Running the example produces an output similar to this:

Salt: 88550cd4b4fac3e5153f9167733a82c0
Hashed: 6555686a7ce17e8fb81de3cb2f88809d81b410e572b039ece7526efd8e864b1c
Enter fullscreen mode Exit fullscreen mode

Encrypting Data: Keeping Secrets Safe

Sometimes you need to encrypt data and decrypt it later, think API keys, sensitive user data, or confidential files. Unlike hashing, encryption is reversible.

You need to be able to decrypt and use that API key, or show users their saved credit card info. For this, use Fernet, a symmetric encryption system where the same key locks and unlocks your data.

I use Fernet for everything from encrypting database backups to protecting API tokens. It's become my go-to because it just works, and it's nearly impossible to mess up.

from cryptography.fernet import Fernet

key = Fernet.generate_key()  # Store this securely!
cipher = Fernet(key)

# Encrypt and decrypt
message = "Sensitive data here"
encrypted = cipher.encrypt(message.encode())
decrypted = cipher.decrypt(encrypted)  # Returns original message

print(encrypted.decode())
print(decrypted.decode())
print(key.decode())
Enter fullscreen mode Exit fullscreen mode

Fernet is beautifully simple and handles the complexity for you, it uses AES encryption, includes authentication to prevent tampering, and adds timestamps to prevent replay attacks. It's what cryptographers call "authenticated encryption," meaning it both hides your data and proves it hasn't been modified.

But here's the critical part: that key is everything. If you lose it, your encrypted data is gone forever. There's no password recovery, no backdoor, no way to get it back. If someone steals it, they can decrypt everything you've ever encrypted with it.

This is why key management is crucial. Store your encryption keys in environment variables, use a secrets management service like AWS Secrets Manager, or at minimum, keep them in a secure configuration file that's never committed to version control. Never hardcode them in your source code.

Running the example produces an output similar to this:

Encrypted:  gAAAAABpnsxPJzrJfStXQvhlYDCBb6y42Qteqm7gcjtsM0KqyNZG5PG7KZxOz8OKXqItL4kGYxdnPq2IJXWS5ClrTWkcaFJm1hG10tnK5LqyqqRP3-NYY1A=
Decrypted:  Sensitive data here
Key:  WrhgfbdekrQcL1TpWo2Nh8lBR7LuqMKruj9Q5XaOc30=
Enter fullscreen mode Exit fullscreen mode

Public-Private Key Encryption: The Magic of Two Keys

Asymmetric encryption uses two keys: a public key (that anyone can have) and a private key (that only you keep). It's like a mailbox, anyone can drop letters in (encrypt with your public key), but only you can open it (decrypt with your private key).

This solves a huge problem: how do two parties communicate securely without meeting first to exchange a secret key? With asymmetric encryption, you can publish your public key anywhere, on your website, in an email, even on a billboard, and anyone can use it to send you encrypted messages that only you can read.

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

# Generate keys
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()

# Encrypt with public key, decrypt with private key
message = b"Secret message"
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("Ciphertext: ", ciphertext)
print("Plaintext: ", plaintext.decode())
Enter fullscreen mode Exit fullscreen mode

This is the foundation for secure communication, digital signatures, SSL/TLS certificates, and SSH keys. You'll use this when different parties need to communicate securely without sharing a secret key beforehand.

Running the example produces an output similar to this:

Ciphertext:  b'\x17\xdeS\xa7*\xa2p\xa6\xd23&{/64\x89\xb8/\x94\x15f\x9b\x80\x15\x86\xaf\xc6\x9ek\xc3\x90\xfb\xcdk^\xd8\xe4J\xb8g\r\n\x99\xacY2)\x92\xf7%\x94\xb9\x98]\x81\xa6{\x98\xcciA\x16\xe4L^B\xf9\x0fR:=\x90\x06\xa9\xf1\x9d__C\x0ca7\xbdK\xacB\xb6C\xc3H{9yq\x1f\xc3\x1fH\xdf\xbe\xbe\x9b\xcci%7\xe0\xa1\xea\xcf\xcf?]\x8c\x1b\xa6\xbf\xb9|\x11\x95\x9f\xf0\x15S=NXs6\xfe\x94\xf2\xf4\xaczt\x89\xcc\x07\xcf\xc7\x82\x9a\xecM~/C\xef\xbc\x12\xf7\xfbJ\xfc\xd2Am\xcd\xe9\xc2}1\xea\xd1\xfe\x07p\xfe\x9e\xec\x95l\xa6\x8a\xcc\xeaZQ\xc5\x81\xe1\x9f\xab_\x08\x9e\xf9\xebo\xb7\xda\x98\xc8\xc3*\xd6k\xe1\x8c=\xbcX \x9a\x12\x9f\x0c\xa5\x8f\xe5\xdb\xf5\x89?S\xa7*\x8e@\xe1\x10mq\x95l\xb9M6\xb7\xc8`\xd8\xe8\xf0\xea\xf5\x11\x93\xc5\xa7/c\x98\x9a\xd9\x8fe\xaf\x19\xc2\xad\xc2=p'
Plaintext:  Secret message
Enter fullscreen mode Exit fullscreen mode

Mistakes I've Made (So You Don't Have To)

Let me save you some pain by sharing the errors I've made:

Don't roll your own crypto: Use established libraries like cryptography. The experts have spent decades finding and fixing vulnerabilities. Your clever modification will almost certainly make things less secure, not more.

Never hardcode keys: I've seen developers commit encryption keys to GitHub, hardcode them in mobile apps (where they're easily extracted), and even print them in error messages. Use environment variables: SECRET_KEY = os.environ.get('SECRET_KEY'). Better yet, use a secrets manager.

Use strong algorithms: MD5 and SHA1 are cryptographically broken. I still see them in legacy code, and it makes me cringe. Stick with SHA-256 or better, and use at least 480,000 iterations for PBKDF2. This number increases over time as computers get faster, check OWASP's current recommendations.

Always use a salt: Every password needs its own random salt, generated at account creation. It's that important.

Store keys separately from data: If someone gets your database dump, they shouldn't automatically get your encryption keys too. That's not encryption, that's theater. Use environment variables, key management services, or at minimum, a separate secured configuration system.


Building Secure APIs in Production?

If you're building real-world Python applications, security doesn't stop at encryption, your data also needs to be validated before it ever reaches your database.

That's where tools like Pydantic become essential for protecting your application from malformed or malicious input.

In my book Practical Pydantic, I walk through how to:

  • Validate incoming API requests
  • Enforce strict data contracts
  • Prevent injection-style attacks through schema validation
  • Build production-ready FastAPI applications safely

👉 Check it out here: https://leanpub.com/practical-pydantic/c/Dqfo5O4I7sb1


Where to Go From Here

You now have the foundation to protect your users' data, but cryptography is a vast field. As you grow more comfortable with these basics, here's what to explore next:

JWT (JSON Web Tokens) are everywhere in modern web applications. They're a standardized way to create tokens that can be verified without database lookups. Once you understand Fernet, JWTs will make perfect sense.

TLS/SSL certificates with Let's Encrypt let you secure your web traffic. HTTPS isn't optional anymore, browsers actively warn users about unencrypted sites. Fortunately, Let's Encrypt makes it free and automated.

OAuth 2.0 for third-party authentication lets users log in with Google, GitHub, or other providers. You leverage their security infrastructure while reducing your own attack surface.

Hardware Security Modules (HSMs) and cloud key management services like AWS KMS provide a secure place to store and use encryption keys at enterprise scale. When your side project becomes a company, this is where you graduate to.

Full source code of the examples on GitHub: https://github.com/nunombispo/cryptography-python-article


Start Building Secure Applications Today

Cryptography isn't just for security experts anymore.

You have both the power and the responsibility to protect your users' data, and Python makes it accessible. Start with proper password hashing, add encryption for sensitive data, and always use HTTPS. Each step makes your application more secure.

Remember: the best time to implement security was before you launched. The second best time is now.

Your users are counting on you to keep their data safe. With the tools and knowledge you have now, you're ready to build applications that earn their trust.


Follow me on Twitter: https://twitter.com/DevAsService

Follow me on Instagram: https://www.instagram.com/devasservice/

Follow me on TikTok: https://www.tiktok.com/@devasservice

Follow me on YouTube: https://www.youtube.com/@DevAsService

Top comments (0)