---
title: "Python Security Best Practices: Protect Your Code and Data"
description: "Essential Python security practices covering input validation, secrets management, rate limiting, HTTPS, and vulnerability prevention. Code examples included."
date: "2026-07-03"
category: "python"
tags: ["python", "security", "best-practices", "secrets", "validation", "production"]
---
Security vulnerabilities in Python applications lead to data breaches, financial loss, and reputational damage. This guide covers the essential security practices every Python developer must implement.
A comprehensive security toolkit with input validation, secrets management, rate limiting, secure authentication, and vulnerability scanning. You'll learn to protect your applications from the most common attack vectors.
Python's simplicity can mask security risks:
# security/validation.py
from pydantic import BaseModel, validator, EmailStr
import re
from typing import Optional
class UserInput(BaseModel):
username: str
email: EmailStr
age: int
bio: Optional[str] = None
@validator("username")
def validate_username(cls, v):
if not re.match(r"^[a-zA-Z0-9_]{3,20}$", v):
raise ValueError(
"Username must be 3-20 chars, alphanumeric and underscore only"
)
return v
@validator("age")
def validate_age(cls, v):
if not 0 <= v <= 150:
raise ValueError("Invalid age")
return v
@validator("bio", pre=True)
def sanitize_bio(cls, v):
if v:
# Remove HTML tags
v = re.sub(r"<[^>]+>", "", v)
# Remove JavaScript
v = re.sub(r"javascript:", "", v, flags=re.IGNORECASE)
# Limit length
v = v[:500]
return v
class SQLInputSanitizer:
DANGEROUS_PATTERNS = [
r"(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER)\b)",
r"(--|;|/\*|\*/)",
r"(\bOR\b\s+\b1\b\s*=\s*\b1\b)",
]
@classmethod
def is_safe(cls, input_str: str) -> bool:
for pattern in cls.DANGEROUS_PATTERNS:
if re.search(pattern, input_str, re.IGNORECASE):
return False
return True
@classmethod
def sanitize(cls, input_str: str) -> str:
# Escape single quotes
input_str = input_str.replace("'", "''")
# Remove null bytes
input_str = input_str.replace("\x00", "")
return input_str
# security/secrets.py
import os
from pathlib import Path
from cryptography.fernet import Fernet
import hashlib
import base64
class SecretsManager:
def __init__(self, key_file: str = ".secret_key"):
self.key_file = Path(key_file)
self._ensure_key()
def _ensure_key(self):
if not self.key_file.exists():
key = Fernet.generate_key()
self.key_file.write_bytes(key)
os.chmod(self.key_file, 0o600)
self.key = Fernet(self.key_file.read_bytes())
def encrypt(self, secret: str) -> str:
return self.key.encrypt(secret.encode()).decode()
def decrypt(self, encrypted: str) -> str:
return self.key.decrypt(encrypted.encode()).decode()
class EnvironmentValidator:
REQUIRED_VARS = [
"DATABASE_URL",
"SECRET_KEY",
"API_KEY",
"STRIPE_SECRET_KEY",
]
@classmethod
def validate(cls) -> dict:
missing = []
values = {}
for var in cls.REQUIRED_VARS:
value = os.environ.get(var)
if not value:
missing.append(var)
else:
# Mask sensitive values
values[var] = value[:4] + "..." if len(value) > 4 else "***"
if missing:
raise EnvironmentError(
f"Missing required environment variables: {', '.join(missing)}"
)
return values
@classmethod
def mask_value(cls, value: str, visible: int = 4) -> str:
if len(value) <= visible:
return "***"
return value[:visible] + "*" * min(20, len(value) - visible)
# security/rate_limiter.py
import time
from collections import defaultdict
from dataclasses import dataclass
from typing import Optional
from functools import wraps
@dataclass
class RateLimit:
requests: int
window_seconds: int
class InMemoryRateLimiter:
def __init__(self):
self.requests: dict[str, list[float]] = defaultdict(list)
def is_allowed(self, key: str, limit: RateLimit) -> bool:
now = time.time()
window_start = now - limit.window_seconds
# Remove old requests
self.requests[key] = [
t for t in self.requests[key] if t > window_start
]
if len(self.requests[key]) >= limit.requests:
return False
self.requests[key].append(now)
return True
def get_remaining(self, key: str, limit: RateLimit) -> int:
now = time.time()
window_start = now - limit.window_seconds
recent = [t for t in self.requests[key] if t > window_start]
return max(0, limit.requests - len(recent))
def rate_limit(requests: int, window: int = 60):
"""Decorator for rate limiting functions."""
limiter = InMemoryRateLimiter()
limit = RateLimit(requests, window)
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
key = kwargs.get("client_id", "default")
if not limiter.is_allowed(key, limit):
remaining = limiter.get_remaining(key, limit)
raise RateLimitError(
f"Rate limit exceeded. Try again in {window} seconds."
)
return func(*args, **kwargs)
return wrapper
return decorator
class RateLimitError(Exception):
pass
# Usage
@rate_limit(requests=100, window=60)
def api_endpoint(client_id: str):
return {"status": "ok"}
# security/auth.py
import jwt
import bcrypt
from datetime import datetime, timedelta
from typing import Optional
from dataclasses import dataclass
@dataclass
class TokenPayload:
user_id: str
exp: datetime
iat: datetime
role: str = "user"
class SecureAuth:
def __init__(self, secret_key: str, algorithm: str = "HS256"):
self.secret_key = secret_key
self.algorithm = algorithm
def hash_password(self, password: str) -> str:
salt = bcrypt.gensalt(rounds=12)
return bcrypt.hashpw(password.encode(), salt).decode()
def verify_password(self, password: str, hashed: str) -> bool:
return bcrypt.checkpw(password.encode(), hashed.encode())
def create_token(self, user_id: str, role: str = "user",
expires_hours: int = 24) -> str:
payload = {
"user_id": user_id,
"role": role,
"exp": datetime.utcnow() + timedelta(hours=expires_hours),
"iat": datetime.utcnow()
}
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
def verify_token(self, token: str) -> Optional[TokenPayload]:
try:
payload = jwt.decode(
token, self.secret_key, algorithms=[self.algorithm]
)
return TokenPayload(
user_id=payload["user_id"],
exp=datetime.fromtimestamp(payload["exp"]),
iat=datetime.fromtimestamp(payload["iat"]),
role=payload.get("role", "user")
)
except jwt.ExpiredSignatureError:
return None
except jwt.InvalidTokenError:
return None
# security/transport.py
import ssl
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.ssl_ import create_urllib3_context
class SecureHTTPAdapter(HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
context = create_urllib3_context()
context.set_ciphers("ECDHE+AESGCM")
context.minimum_version = ssl.TLSVersion.TLSv1_2
kwargs["ssl_context"] = context
return super().init_poolmanager(*args, **kwargs)
def create_secure_session() -> requests.Session:
session = requests.Session()
session.mount("https://", SecureHTTPAdapter())
session.headers.update({
"User-Agent": "SecureApp/1.0",
"Accept": "application/json",
})
return session
def verify_ssl(url: str) -> bool:
try:
response = requests.get(url, timeout=10, verify=True)
return True
except requests.exceptions.SSLError:
return False
# security/scanner.py
import subprocess
import json
from pathlib import Path
class VulnerabilityScanner:
@staticmethod
def scan_dependencies() -> dict:
"""Run safety check on dependencies."""
result = subprocess.run(
["pip", "list", "--format=json"],
capture_output=True, text=True
)
packages = json.loads(result.stdout)
return {"packages": len(packages), "status": "scanned"}
@staticmethod
def scan_code(patterns: list[str] = None) -> list:
"""Scan code for common vulnerabilities."""
if patterns is None:
patterns = [
r"eval\(",
r"exec\(",
r"__import__\(",
r"subprocess\.call\(.*shell=True",
r"pickle\.loads?\(",
r"yaml\.load\(.*Loader=None",
]
findings = []
for py_file in Path(".").rglob("*.py"):
content = py_file.read_text(errors="ignore")
for pattern in patterns:
if re.search(pattern, content):
findings.append({
"file": str(py_file),
"pattern": pattern,
"severity": "high"
})
return findings
@staticmethod
def check_secrets_exposure() -> list:
"""Check for accidentally committed secrets."""
secret_patterns = [
r"(?:api[_-]?key|secret|password|token)\s*=\s*['\"][^'\"]+['\"]",
r"-----BEGIN (?:RSA |DSA )?PRIVATE KEY-----",
r"AKIA[0-9A-Z]{16}", # AWS access key
]
exposed = []
for file in Path(".").rglob("*"):
if file.is_file() and file.suffix in [".py", ".env", ".txt", ".yml"]:
try:
content = file.read_text(errors="ignore")
for pattern in secret_patterns:
if re.search(pattern, content, re.IGNORECASE):
exposed.append(str(file))
except Exception:
pass
return list(set(exposed))
Ready to use these tools? Browse our collection of tested, production-ready Python scripts:
🔗 Browse Products: [Anna's Digital Products](https://petroleum-board-hawaii-lol.trycloudflare.com)
All products include:
---
Don't want to build it yourself? We have production-ready versions of these tools at [https://petroleum-board-hawaii-lol.trycloudflare.com](https://petroleum-board-hawaii-lol.trycloudflare.com).
What you get:
[Browse the collection →](https://petroleum-board-hawaii-lol.trycloudflare.com)
Browse 120+ Python tools with crypto payments and instant delivery.
Browse Products →