Python Security Best Practices

· 5 min read · Updated March 15, 2026 · intermediate
python security best-practices cryptography

Security is not an afterthought — it is a design decision that affects every layer of your application. This guide covers practical security patterns for Python developers, from handling user input to managing secrets.

Validate All Input

Never trust user input. Every piece of data that enters your application from an external source is a potential attack vector.

Use a Validation Library

Pydantic is the standard for data validation in Python:

from pydantic import BaseModel, EmailStr, Field, validator

class UserRegistration(BaseModel):
    username: str = Field(min_length=3, max_length=30)
    email: EmailStr
    password: str = Field(min_length=8)
    
    @validator("username")
    def username_alphanumeric(cls, v):
        assert v.isalnum(), "must be alphanumeric"
        return v.lower()

# Rejects invalid email, weak passwords, bad usernames
user = UserRegistration(
    username="john123",
    email="john@example.com",
    password="secret123"
)

Sanitize SQL Queries

Never interpolate user input directly into SQL. Use parameterized queries:

# Bad — vulnerable to SQL injection
query = f"SELECT * FROM users WHERE username =  + username + "

# Good — safe from injection
cursor.execute(
    "SELECT * FROM users WHERE username = %s",
    (username,)
)

With SQLAlchemy ORM:

# Use the ORM which handles escaping automatically
user = session.query(User).filter(User.username == username).first()

Handle Secrets Properly

Never hardcode secrets in your source code. Use environment variables or a secrets manager.

Environment Variables

import os

# Access secrets from environment
DATABASE_PASSWORD = os.environ.get("DATABASE_PASSWORD")
API_KEY = os.environ.get("API_KEY")

if not API_KEY:
    raise ValueError("API_KEY environment variable is required")

Python-Dotenv for Development

# .env file (add to .gitignore)
# DATABASE_PASSWORD=secret123
# API_KEY=xxx

from dotenv import load_dotenv

load_dotenv()  # Loads .env file into environment

Avoid Accidental Secret Leaks

Be careful with logging and error messages:

# Bad — leaks credentials in logs
logger.info(f"Connecting with password: {password}")

# Good — no sensitive data in logs
logger.info("Connecting to database")

Password Handling

Never store passwords in plain text. Use strong hashing algorithms.

Using Passlib

from passlib.hash import bcrypt

# Hash a password for storage
hashed = bcrypt.hash("user_password")
# $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyY1KKE9YFdm

# Verify a password
bcrypt.verify("user_password", hashed)  # True
bcrypt.verify("wrong_password", hashed)  # False

With Bcrypt Module Directly

import bcrypt

# Hashing
password = b"secret_password"
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password, salt)

# Verifying
bcrypt.checkpw(password, hashed)  # True

Secure File Handling

File operations can expose your system to path traversal attacks.

Prevent Path Traversal

import os
from pathlib import Path

def safe_read_file(filename: str, base_dir: Path):
    # Resolve the full path
    full_path = (base_dir / filename).resolve()
    
    # Ensure it stays within the base directory
    if not str(full_path).startswith(str(base_dir)):
        raise ValueError("Invalid file path")
    
    return full_path.read_text()

# Usage
BASE_DIR = Path("/var/app/files")
content = safe_read_file("document.txt", BASE_DIR)
# Trying "../../etc/passwd" would be blocked

Limit File Uploads

from pathlib import Path
import mimetypes

ALLOWED_EXTENSIONS = {".txt", ".pdf", ".png", ".jpg"}
MAX_FILE_SIZE = 5 * 1024 * 1024  # 5 MB

def secure_upload(uploaded_file):
    # Check file size
    if uploaded_file.size > MAX_FILE_SIZE:
        raise ValueError("File too large")
    
    # Check extension
    ext = Path(uploaded_file.filename).suffix.lower()
    if ext not in ALLOWED_EXTENSIONS:
        raise ValueError("File type not allowed")
    
    # Verify MIME type
    mime_type, _ = mimetypes.guess_type(uploaded_file.filename)
    if mime_type not in ["text/plain", "application/pdf", "image/png", "image/jpeg"]:
        raise ValueError("Invalid file type")
    
    # Save with a generated filename, never use user-provided names
    import uuid
    safe_name = f"{uuid.uuid4()}{ext}"
    # ... save to storage

HTTPS and TLS

Always use encrypted connections in production.

SSL Context for Requests

import requests

# Verify certificates (always do this in production)
response = requests.get(
    "https://api.example.com/data",
    verify=True  # Or path to CA bundle
)

# For systems with custom certificates
response = requests.get(
    "https://internal-api/",
    verify="/path/to/ca-bundle.crt"
)

Flask with HTTPS

from flask import Flask

app = Flask(__name__)

# Force HTTPS in production
@app.before_request
def require_https():
    if not request.is_secure:
        url = request.url.replace("http://", "https://")
        return redirect(url, 301)

Authentication Patterns

Implement authentication carefully. Use established libraries when possible.

JWT Token Handling

import jwt
import os
from datetime import datetime, timedelta

SECRET_KEY = os.environ.get("JWT_SECRET")

def create_token(user_id: int) -> str:
    payload = {
        "user_id": user_id,
        "exp": datetime.utcnow() + timedelta(hours=24)
    }
    return jwt.encode(payload, SECRET_KEY, algorithm="HS256")

def verify_token(token: str) -> dict:
    try:
        return jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
    except jwt.ExpiredSignatureError:
        raise ValueError("Token expired")
    except jwt.InvalidTokenError:
        raise ValueError("Invalid token")

Rate Limiting

Protect against brute-force attacks with rate limiting:

from flask import Flask, request, jsonify
from flask_limiter import Limiter

app = Flask(__name__)
limiter = Limiter(app, key_func=get_remote_address)

@app.route("/login", methods=["POST"])
@limiter.limit("5 per minute")  # 5 attempts per minute
def login():
    # ... login logic

Dependency Security

Keep your dependencies secure. Vulnerabilities in libraries affect your application.

Check for Vulnerabilities

# Using pip-audit
pip-audit

# Using safety
safety check

Pin Dependencies

# pyproject.toml
[project]
dependencies = [
    "flask==3.0.0",
    "requests>=2.31.0,<3.0.0",
]

Use tools like Dependabot or Renovate to stay updated on security patches.

Cross-Site Scripting (XSS) Prevention

When rendering user content, always escape HTML.

In Jinja2 Templates

Jinja2 auto-escapes by default:

<!-- Safe — automatically escaped -->
<p>{{ user_comment }}</p>

<!-- Unsafe — only use when content is trusted -->
<p>{{ user_comment | safe }}</p>

In JSON Responses

from flask import jsonify
import json

# Automatically escapes HTML in string values
@app.route("/api/user")
def get_user():
    return jsonify({
        "username": "<script>alert(xss)</script>"
    })
# Output: {"username":"&lt;script&gt;alert(&#x27;xss&#x27;)&lt;/script&gt;"}

Security Headers

Add security headers to protect against common attacks.

With Flask

from flask import Flask

app = Flask(__name__)

@app.after_request
def add_security_headers(response):
    # Prevent clickjacking
    response.headers["X-Frame-Options"] = "DENY"
    
    # Prevent XSS
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    
    # Content Security Policy
    response.headers["Content-Security-Policy"] = "default-src self"
    
    # Referrer policy
    response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
    
    return response

CSRF Protection

Protect forms against cross-site request forgery.

With Flask-WTF

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired

class LoginForm(FlaskForm):
    username = StringField("username", validators=[DataRequired()])
    password = PasswordField("password", validators=[DataRequired()])

@app.route("/login", methods=["GET", "POST"])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # CSRF token automatically validated
        # ... login logic
    return render_template("login.html", form=form)

Logging Security Events

Monitor for suspicious activity.

import logging

logger = logging.getLogger("security")

def log_failed_login(username: str, ip_address: str):
    logger.warning(
        "Failed login attempt",
        extra={
            "username": username,
            "ip_address": ip_address,
            "event": "failed_login"
        }
    )

def log_suspicious_activity(details: dict):
    logger.error(
        "Suspicious activity detected",
        extra={**details, "event": "suspicious"}
    )

Security Checklist

Before deploying:

  • All user input is validated
  • Passwords are hashed with bcrypt or argon2
  • Secrets come from environment variables
  • HTTPS is enforced in production
  • Dependencies are audited for vulnerabilities
  • Security headers are configured
  • Rate limiting is enabled on authentication endpoints
  • File uploads are restricted and validated
  • SQL queries use parameterization
  • Logs do not contain sensitive data

Security is an ongoing process. Review and update your security practices regularly.

See Also