Protecting files is one of the most practical things you can do when working with sensitive data. Whether you are storing personal documents, application backups, private notes, API keys, customer records, or project archives, file encryption adds a strong layer of protection. If someone gets access to your files without permission, encryption makes the content unreadable unless they also have the correct key or password.
Python is a great language for this task because it is easy to read, flexible, and supported by powerful cryptography libraries. In this article, we will build a complete understanding of file encryption and decryption in Python, starting from the basic concept and moving toward real-world examples. You will learn how to encrypt and decrypt files, how to use passwords safely, how to handle binary files, and what mistakes to avoid when building your own secure workflow.
Encryption is not just for large companies or security experts. It is useful for freelancers, developers, students, and anyone who wants to keep files private. A simple Python script can save you from accidental leaks, unauthorized access, and unnecessary risk.
What file encryption really means
File encryption is the process of converting readable file content into unreadable data called ciphertext. To turn that ciphertext back into the original file, you need a decryption key. Without that key, the encrypted file looks like random bytes.
This means that if you encrypt a PDF, image, ZIP archive, CSV, or text document, the file on disk is no longer human-readable. Even if someone opens it, they will not understand the content. Only your decryption process can restore the original file.
At a high level, encryption works like this:
Read the file bytes.
Apply an encryption algorithm using a key.
Save the encrypted bytes to a new file.
Later, read the encrypted file.
Use the matching key to decrypt it.
Recover the original file bytes.
The important part is that encryption should be done with a trusted library and a strong algorithm. You should avoid inventing your own encryption method. Security is one area where “simple” often means “unsafe.”
Why use Python for encryption and decryption
Python is widely used for automation, web applications, data processing, and scripting, so it often becomes the natural choice for file-handling tasks. It can read bytes, process files of many types, and work with modern security libraries with only a few lines of code.
Here are a few reasons Python is a strong choice for file encryption:
Python makes it easy to build small utilities for encrypting backups, protecting exported reports, or securing configuration files. It is also ideal for learning because the code is easier to understand than in many lower-level languages.
Python libraries such as cryptography provide reliable, well-tested encryption primitives. Instead of writing complex logic by hand, you can use a secure package that handles important details correctly.
Python also works well for automation. You can encrypt files before uploading them, decrypt files only when needed, and integrate secure workflows into desktop tools, scripts, or backend services.
Choosing the right encryption approach
Before writing code, it is helpful to understand the two main ways encryption is usually handled.
Symmetric encryption
Symmetric encryption uses the same key to encrypt and decrypt the data. This is the most common approach for file encryption because it is fast and practical. If you know the key, you can decrypt the file.
A strong symmetric algorithm is a good fit for protecting files on your machine or transferring data securely between trusted systems.
Password-based encryption
Password-based encryption starts with a password that a human can remember. That password is converted into a cryptographic key using a key derivation function such as PBKDF2, scrypt, or Argon2. This is useful when you want to protect a file with a password instead of storing a key file directly.
This is often the most user-friendly model because users can remember a password more easily than a long key.
Asymmetric encryption
Asymmetric encryption uses a public key and a private key. It is very powerful and useful for secure communication, but for direct file encryption it is usually more complicated than necessary. In many practical cases, symmetric encryption is simpler and faster.
For most file encryption scripts in Python, symmetric encryption is the best starting point.
Installing the library
A secure and common library for encryption in Python is cryptography. You can install it with pip:
pip install cryptography
After installation, you can use Fernet, which provides a simple and safe symmetric encryption interface for many common use cases.
A simple file encryption example in Python
Let’s start with a basic example that encrypts a file and then decrypts it again.
Encrypt a file
from cryptography.fernet import Fernet
def generate_key():
key = Fernet.generate_key()
with open("secret.key", "wb") as key_file:
key_file.write(key)
print("Key generated and saved to secret.key")
def load_key():
return open("secret.key", "rb").read()
def encrypt_file(file_path, encrypted_file_path, key):
fernet = Fernet(key)
with open(file_path, "rb") as file:
original_data = file.read()
encrypted_data = fernet.encrypt(original_data)
with open(encrypted_file_path, "wb") as file:
file.write(encrypted_data)
print(f"File encrypted and saved to {encrypted_file_path}")
if __name__ == "__main__":
generate_key()
key = load_key()
encrypt_file("example.txt", "example.txt.enc", key)
This script does three things. First, it generates a key and saves it to a file. Second, it loads that key back. Third, it reads the original file, encrypts it, and writes the encrypted result to a new file.
Decrypt a file
from cryptography.fernet import Fernet
def load_key():
return open("secret.key", "rb").read()
def decrypt_file(encrypted_file_path, decrypted_file_path, key):
fernet = Fernet(key)
with open(encrypted_file_path, "rb") as file:
encrypted_data = file.read()
decrypted_data = fernet.decrypt(encrypted_data)
with open(decrypted_file_path, "wb") as file:
file.write(decrypted_data)
print(f"File decrypted and saved to {decrypted_file_path}")
if __name__ == "__main__":
key = load_key()
decrypt_file("example.txt.enc", "example_decrypted.txt", key)
With this code, the file is restored back to readable form.
Understanding how Fernet works
Fernet is popular because it gives you a secure high-level API. It handles encryption, authentication, and integrity checks. That last part is important. Encryption alone is not enough. You also want to know if the file was changed or corrupted.
If someone modifies the encrypted file, decryption should fail rather than silently producing broken output. That is exactly the kind of behavior you want in a secure tool.
The cryptography library is a much safer option than writing your own custom cipher logic.
Encrypting text files, PDFs, images, and ZIP files
One of the best things about file encryption is that it works on bytes, not on “document types.” That means you can encrypt almost anything:
.txt.pdf.docx.csv.png.jpg.zip.jsonbackups and archives
The code does not care about the file format. It reads the bytes and encrypts them. This is why file encryption is so flexible.
For example, if you encrypt a PDF file, the output becomes a binary blob that cannot be opened like a normal PDF until it is decrypted. The same is true for images and archives.
Encrypting a file with a password instead of a saved key
In many real-world cases, users prefer a password. Instead of keeping a generated key in a file, you can derive a cryptographic key from a password. This is more convenient for end users, though the password must be strong.
Here is a complete example using PBKDF2 and Fernet.
import os
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.fernet import Fernet
def derive_key_from_password(password: str, salt: bytes) -> bytes:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=390000,
)
key = base64.urlsafe_b64encode(kdf.derive(password.encode()))
return key
def encrypt_file_with_password(input_file: str, output_file: str, password: str):
salt = os.urandom(16)
key = derive_key_from_password(password, salt)
fernet = Fernet(key)
with open(input_file, "rb") as f:
data = f.read()
encrypted = fernet.encrypt(data)
with open(output_file, "wb") as f:
f.write(salt + encrypted)
print(f"Encrypted file saved to {output_file}")
def decrypt_file_with_password(input_file: str, output_file: str, password: str):
with open(input_file, "rb") as f:
content = f.read()
salt = content[:16]
encrypted_data = content[16:]
key = derive_key_from_password(password, salt)
fernet = Fernet(key)
decrypted = fernet.decrypt(encrypted_data)
with open(output_file, "wb") as f:
f.write(decrypted)
print(f"Decrypted file saved to {output_file}")
if __name__ == "__main__":
encrypt_file_with_password("document.txt", "document.txt.enc", "MyStrongPassword123!")
decrypt_file_with_password("document.txt.enc", "document_restored.txt", "MyStrongPassword123!")
Why the salt matters
The salt is random data added before key derivation. It protects against certain attacks and makes it much harder for attackers to use precomputed tables. The salt does not need to be secret, but it must be stored so the same password can be used again for decryption.
In the example above, the salt is placed at the beginning of the encrypted file. During decryption, the code reads the first 16 bytes, uses them to derive the same key, and then decrypts the rest of the file.
A cleaner reusable class for file encryption
Once the basic idea works, you may want to wrap the logic in a reusable class. This makes your code easier to maintain and use in other scripts.
import os
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.fernet import Fernet
class FileEncryptor:
def __init__(self, password: str):
self.password = password
def _derive_key(self, salt: bytes) -> bytes:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=390000,
)
return base64.urlsafe_b64encode(kdf.derive(self.password.encode()))
def encrypt(self, input_path: str, output_path: str):
salt = os.urandom(16)
key = self._derive_key(salt)
fernet = Fernet(key)
with open(input_path, "rb") as f:
data = f.read()
encrypted_data = fernet.encrypt(data)
with open(output_path, "wb") as f:
f.write(salt + encrypted_data)
def decrypt(self, input_path: str, output_path: str):
with open(input_path, "rb") as f:
content = f.read()
salt = content[:16]
encrypted_data = content[16:]
key = self._derive_key(salt)
fernet = Fernet(key)
decrypted_data = fernet.decrypt(encrypted_data)
with open(output_path, "wb") as f:
f.write(decrypted_data)
if __name__ == "__main__":
encryptor = FileEncryptor("MyStrongPassword123!")
encryptor.encrypt("secret.txt", "secret.txt.enc")
encryptor.decrypt("secret.txt.enc", "secret_recovered.txt")
This version is easier to reuse in a project. You can create one object, then call encrypt() or decrypt() whenever needed.
Handling large files
The basic examples above read the whole file into memory. That is fine for small and medium files, but it may not be ideal for very large files such as multi-gigabyte backups or media archives.
For huge files, you may want a streaming approach. Some encryption libraries support chunked encryption, which lets you process the file in smaller pieces. This reduces memory usage and makes your tool more scalable.
The Fernet API is simple but not designed for true streaming encryption. If you need large-file streaming, you may need a lower-level approach or a different mode of operation. That said, for many everyday file encryption tasks, Fernet is perfectly adequate.
For a practical blog post, it is important to be honest about this limitation. A lot of beginners assume that one encryption method is automatically best for everything. In reality, the best method depends on file size, workflow, and security requirements.
Encrypting and decrypting files from the command line
A very useful pattern is to turn your encryption script into a command-line tool. That way, you can run it like this:
python file_crypto.py encrypt input.txt output.enc
python file_crypto.py decrypt output.enc output.txt
Here is a complete version using argparse.
import os
import sys
import base64
import argparse
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.fernet import Fernet
def derive_key(password: str, salt: bytes) -> bytes:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=390000,
)
return base64.urlsafe_b64encode(kdf.derive(password.encode()))
def encrypt_file(input_path: str, output_path: str, password: str):
salt = os.urandom(16)
key = derive_key(password, salt)
fernet = Fernet(key)
with open(input_path, "rb") as f:
data = f.read()
encrypted = fernet.encrypt(data)
with open(output_path, "wb") as f:
f.write(salt + encrypted)
def decrypt_file(input_path: str, output_path: str, password: str):
with open(input_path, "rb") as f:
content = f.read()
salt = content[:16]
encrypted_data = content[16:]
key = derive_key(password, salt)
fernet = Fernet(key)
decrypted = fernet.decrypt(encrypted_data)
with open(output_path, "wb") as f:
f.write(decrypted)
def main():
parser = argparse.ArgumentParser(description="Encrypt or decrypt files in Python")
parser.add_argument("action", choices=["encrypt", "decrypt"])
parser.add_argument("input_file")
parser.add_argument("output_file")
parser.add_argument("password")
args = parser.parse_args()
try:
if args.action == "encrypt":
encrypt_file(args.input_file, args.output_file, args.password)
print("Encryption completed successfully.")
else:
decrypt_file(args.input_file, args.output_file, args.password)
print("Decryption completed successfully.")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
This is a nice foundation for a real utility script. You can later add logging, file validation, and better password input handling.
Improving security with good password handling
A secure encryption tool is not only about the encryption algorithm. It is also about how you handle passwords and keys.
Here are a few important habits:
Do not hard-code passwords directly in your script unless it is only for testing. A hard-coded password is easy to leak and hard to change safely.
Do not reuse weak passwords. A short password like 123456 or password is not enough. Use something longer and harder to guess.
Do not store unprotected key files in public folders. A key file should be treated like a secret. If someone gets the key, they can decrypt the file.
Do not invent your own crypto logic. Use a well-reviewed library and standard practices. Security mistakes are often invisible until it is too late.
Adding password prompts instead of plain text arguments
Passing the password directly in the terminal is convenient, but it can sometimes be visible in command history or process listings. A better approach is to prompt the user securely.
import getpass
password = getpass.getpass("Enter password: ")
This prevents the password from being displayed on the screen while typing it.
You can combine this with the command-line example:
import getpass
password = getpass.getpass("Enter encryption password: ")
This is a small improvement, but it makes the tool feel much more professional.
Encrypting multiple files
Sometimes you may want to encrypt many files at once, such as a folder of documents or a backup directory. You can use Python’s pathlib and os tools to loop through files.
Here is an example that encrypts every file in a folder:
from pathlib import Path
from cryptography.fernet import Fernet
def generate_key():
key = Fernet.generate_key()
with open("batch.key", "wb") as f:
f.write(key)
return key
def load_key():
with open("batch.key", "rb") as f:
return f.read()
def encrypt_folder(folder_path: str):
key = load_key()
fernet = Fernet(key)
folder = Path(folder_path)
for file_path in folder.iterdir():
if file_path.is_file() and file_path.name != "batch.key":
with open(file_path, "rb") as f:
data = f.read()
encrypted = fernet.encrypt(data)
output_path = file_path.with_suffix(file_path.suffix + ".enc")
with open(output_path, "wb") as f:
f.write(encrypted)
print(f"Encrypted: {file_path.name} -> {output_path.name}")
if __name__ == "__main__":
generate_key()
encrypt_folder("my_files")
This is useful for simple local workflows. For a production app, you would probably want more careful error handling and folder structure management.
Decrypting multiple files
The reverse process works in a similar way:
from pathlib import Path
from cryptography.fernet import Fernet
def load_key():
with open("batch.key", "rb") as f:
return f.read()
def decrypt_folder(folder_path: str):
key = load_key()
fernet = Fernet(key)
folder = Path(folder_path)
for file_path in folder.iterdir():
if file_path.is_file() and file_path.suffix == ".enc":
with open(file_path, "rb") as f:
encrypted_data = f.read()
decrypted = fernet.decrypt(encrypted_data)
output_path = file_path.with_suffix("")
with open(output_path, "wb") as f:
f.write(decrypted)
print(f"Decrypted: {file_path.name} -> {output_path.name}")
if __name__ == "__main__":
decrypt_folder("my_files")
This example removes the .enc suffix and restores the original file name. In a real project, you may want to preserve the original extension more carefully.
Handling errors properly
Encryption code should fail safely. That means if something goes wrong, the script should not silently corrupt files or produce incomplete output.
Common errors include:
wrong password
missing file
invalid encrypted data
permission problems
corrupted output file
You can catch these issues and show clear messages:
from cryptography.fernet import InvalidToken
try:
decrypted_data = fernet.decrypt(encrypted_data)
except InvalidToken:
print("Decryption failed: wrong password or corrupted file.")
This is very helpful because InvalidToken usually means either the password was incorrect or the encrypted file was changed.
Example: full script for password-based file encryption
Here is a more complete version that ties everything together.
import os
import sys
import base64
import getpass
from pathlib import Path
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
def derive_key(password: str, salt: bytes) -> bytes:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=390000,
)
return base64.urlsafe_b64encode(kdf.derive(password.encode()))
def encrypt_file(input_path: str, output_path: str, password: str):
salt = os.urandom(16)
key = derive_key(password, salt)
fernet = Fernet(key)
with open(input_path, "rb") as f:
data = f.read()
encrypted = fernet.encrypt(data)
with open(output_path, "wb") as f:
f.write(salt + encrypted)
def decrypt_file(input_path: str, output_path: str, password: str):
with open(input_path, "rb") as f:
content = f.read()
if len(content) < 17:
raise ValueError("Encrypted file is too small or invalid.")
salt = content[:16]
encrypted_data = content[16:]
key = derive_key(password, salt)
fernet = Fernet(key)
try:
decrypted = fernet.decrypt(encrypted_data)
except InvalidToken:
raise ValueError("Wrong password or corrupted file.")
with open(output_path, "wb") as f:
f.write(decrypted)
def main():
if len(sys.argv) != 4:
print("Usage:")
print(" python file_crypto.py encrypt input_file output_file")
print(" python file_crypto.py decrypt input_file output_file")
sys.exit(1)
action = sys.argv[1]
input_file = sys.argv[2]
output_file = sys.argv[3]
password = getpass.getpass("Enter password: ")
try:
if action == "encrypt":
encrypt_file(input_file, output_file, password)
print("File encrypted successfully.")
elif action == "decrypt":
decrypt_file(input_file, output_file, password)
print("File decrypted successfully.")
else:
print("Invalid action. Use encrypt or decrypt.")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
This script is already good enough for many personal and internal workflows.
Real-world use cases
File encryption in Python is useful in many situations.
A developer may want to encrypt configuration files that contain secret API credentials before storing backups. A freelancer may want to protect client documents before sending them through cloud storage. A student may want to encrypt private notes on a shared computer. A company may want to encrypt archive files before moving them between internal systems.
The point is not only to keep “hackers” away. It is also to reduce accidental exposure. Sometimes the biggest risk is not an attacker but a misplaced file, a shared folder, or an unprotected backup.
Common mistakes people make
A lot of beginners run into the same problems when they first try to encrypt files.
One common mistake is using a weak password and assuming encryption will save the day. Encryption cannot fix a poor password choice.
Another mistake is losing the key file. If you use key-based encryption and delete the key, the file may be impossible to restore.
Another problem is saving encrypted output with the same name as the original file. This can overwrite data before you realize what happened. It is safer to write to a separate output file.
Some people also forget to test decryption immediately after encryption. That is risky. Always verify that a file can be decrypted successfully before trusting the workflow.
A final mistake is ignoring backups. Encryption should protect data, not become the reason you lose it. Keep secure backups of important files and keys.
When not to roll your own solution
Even though Python makes encryption easy to script, there are cases where you should use existing tools instead of building a custom script.
If you are securing enterprise data, handling regulated information, or building a public-facing product, you may need stronger architecture, audit logs, key management systems, and compliance controls. In that case, a simple script is not enough.
For personal automation, demos, internal tools, and learning projects, Python encryption scripts are excellent. For high-stakes security, use a broader security design.
Best practices to remember
A strong file encryption workflow usually follows a few practical rules.
Use a trusted library such as cryptography.
Use a strong password or random key.
Store keys carefully, and do not expose them in code.
Add a salt when deriving keys from passwords.
Check for errors and corrupted files.
Keep encrypted output separate from the original file.
Test both encryption and decryption before relying on the script.
These habits are simple, but they make a big difference.
Final thoughts
File encrypt and decrypt in Python is one of those topics that looks advanced at first, but becomes very approachable once you break it into small steps. You read the file as bytes, encrypt it with a strong key, save the result, and later decrypt it back to the original format. Python makes that process clean and understandable, especially when you use a reliable library like cryptography.
The most important lesson is that encryption is not just about hiding data. It is about protecting data correctly. That means using strong algorithms, safe password handling, proper error checks, and good file management. Once you build those habits, you can create simple tools that are genuinely useful in everyday work.
Hassan Agmir
Author · Filenewer
Writing about file tools and automation at Filenewer.
Try It Free
Process your files right now
No account needed · Fast & secure · 100% free
Browse All Tools