How-tos
The Best Practices for Writing Secure Python Code
Learn essential developer practices for writing secure Python code—from input validation and parameterized queries to password hashing, dependency security, and least privilege. These habits form the baseline for production-ready applications.
June 2026 · 8 min read · 1 views · 0 hearts
Advertisement
The Best Practices for Writing Secure Code
You can write the most elegant Python code in the world, but if a single SQL injection or XSS vulnerability brings your app down, none of that good architecture matters. Security isn’t a feature—it’s a baseline. Here’s how to make it part of your daily development workflow without losing your mind.
Validate Everything, Trust Nothing
This is the golden rule. Never assume user input is safe. That includes form fields, URL parameters, API payloads, file uploads—even seemingly harmless strings like usernames or search terms.
The fix? Validate on arrival. Use type checking (isinstance()), length limits, and allowlists for expected characters. Python’s re module is your friend for whitelist-based regex (e.g., only alphanumeric characters + space). Reject anything that doesn’t fit, don’t try to “clean” it.
- Always validate input type, length, format, and range.
- Reject early—fail fast with clear error messages.
- Sanitize for output separately (e.g., HTML-escape before rendering).
Parameterize Your Database Queries
SQL injection is one of the oldest tricks in the book, and it’s still terrifyingly common. The mistake is simple: concatenating user input into a query string.
# BAD — never do this
query = f"SELECT * FROM users WHERE username = '{user_input}'"
cursor.execute(query)
# GOOD — parameterized query
cursor.execute("SELECT * FROM users WHERE username = %s", (user_input,))
The database driver handles escaping for you. In most modern ORMs (like SQLAlchemy), this is built in—but never assume. Always use parameterized queries or an ORM that enforces them.
Escape Output Everywhere
Your app might safely store user data, but the moment you render it back to a browser, you’re opening a door to cross-site scripting (XSS). The problem is injecting raw HTML, JavaScript, or CSS into the page.
- Use a templating engine that auto-escapes (Jinja2, Django templates, Mako).
- If you must render raw HTML (e.g., from a rich text editor), use a dedicated sanitizer like
bleach. - Escape attributes:
src,href,onclick—attackers can embed JS in any attribute. - For JSON responses, ensure no unescaped
</script>leaks into the page.
Use Environment Variables for Secrets
Don’t hardcode API keys, database passwords, or encryption salts. Ever. Not in config files. Not in your source code. A GitHub leak is one commit away.
Instead, load secrets from environment variables at runtime. Python’s os.getenv() is straightforward. Tools like python-dotenv let you keep a .env file locally (which you add to .gitignore immediately).
import os
DB_PASSWORD = os.getenv("DB_PASSWORD")
if not DB_PASSWORD:
raise RuntimeError("Missing DB_PASSWORD environment variable")
For production, use a secret manager (AWS Secrets Manager, HashiCorp Vault) or container orchestration secrets (Kubernetes Secrets, Docker secrets).
Hash Passwords Properly
If you store passwords in plaintext, you’re not just writing bad code—you’re inviting disaster. Even hashing with MD5 or SHA-1 is trivial to crack. Use a dedicated password hashing library:
from passlib.hash import pbkdf2_sha256
hashed = pbkdf2_sha256.hash(user_password)
Or bcrypt, argon2, or scrypt. These are slow by design and include a random salt. Python’s bcrypt library or passlib are battle-tested.
- Never write your own hashing algorithm.
- Always salt passwords automatically (the library handles this).
- Never use reversible encryption for passwords.
Secure Your Dependencies
Python’s ecosystem is rich, but that also means supply-chain attacks are real. Malicious packages have been uploaded to PyPI with typosquatted names or hidden backdoors.
- Pin your dependencies in
requirements.txtorpoetry.lock(exact versions). - Regularly run
pip auditorsafety checkto find known vulnerabilities. - Use a virtual environment—don’t install packages globally.
- Consider using a package manager with integrity verification (Poetry, Pipenv).
Log Sensibly, Not Excessively
Logging is essential for debugging and auditing, but it’s also a common data leak. Don’t log passwords, session tokens, credit card numbers, or personal data unless absolutely necessary.
- Mask or truncate sensitive fields (e.g.,
"password=***"). - Log to a secure, centralized system (not a world-readable file).
- Set appropriate log levels—
DEBUGin production can expose internals.
Handle Errors Gracefully
Default Python error messages can reveal stack traces, file paths, and database structure. That’s gold for an attacker.
try:
result = risky_operation()
except Exception as e:
# Log the real error for your team
logger.error(f"Operation failed: {e}")
# Return a generic message to the user
return {"error": "Something went wrong"}, 500
Never expose internal error details in production APIs or UI.
Keep Dependencies Updated
Vulnerabilities are discovered daily. Keep your environment fresh. Use automated tools: - Dependabot (GitHub) or Renovate for pull requests. - pip-audit or Trivy for container scanning. - Set up CI checks that fail on high-severity CVEs.
This isn't optional—it’s ongoing maintenance.
Think in Terms of Least Privilege
Your code doesn’t need root access to run. Your database user doesn’t need DROP TABLE permissions. Your API token shouldn’t have admin scope.
- Run your app with a dedicated non-root user.
- Grant database users only the permissions they need (SELECT, INSERT, UPDATE—not all privileges).
- Use scoped API keys (read-only vs. write access).
- Avoid running automated tasks as yourself—use service accounts.
Test for Security Early
Don’t wait until the security audit a week before launch. Integrate basic security testing into your development loop:
- Static analysis:
banditscans your code for common vulnerabilities (hardcoded passwords, SQL injection patterns). - Linting:
flake8with security plugins. - Unit tests for input validation, authentication logic, and authorization checks.
- Fuzzing for endpoints that accept complex input.
Even one quick run of bandit -r your_project/ can catch obvious issues.
Conclusion
Secure code isn’t about paranoia—it’s about discipline. Every validation check, every parameterized query, every escaped output is a small win against real threats. Make these practices habits, not afterthoughts. Your future self (and your users) will thank you.
Advertisement
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.