Mastodon Mastodon Mastodon Mastodon

How to Write Secure Python Code: A Step-by-Step Guide

Photo of author

Kamil Akbari

Published:

Last updated:

Python’s simplicity makes it the language of choice for web APIs, data pipelines, and automation – but the same flexibility that speeds up development also introduces attack surface. This guide covers the security controls that matter most in production Python code, from dependency isolation to session management, with concrete code examples for each. References to OWASP Cheat Sheets and the Bandit static analysis tool provide a starting point for deeper verification.

Python developers most exposed to these risks

The techniques below are most critical for developers building web applications (Django, Flask, FastAPI), REST APIs that consume user input, data pipelines handling sensitive records, or any Python process with access to a database or external service. Scripts that run locally with no user input are lower risk, but shared services and anything internet-facing should treat every point below as mandatory.

Isolating project dependencies with virtual environments

Virtual environments isolate project dependencies and reduce the chance that package conflicts or unsafe global installs will affect other applications on the host.

  1. Dependency isolation: each environment has its own packages, separate from the system interpreter and other projects.
  2. Version control: you can pin and audit exact library versions before deployment.
  3. Reduced blast radius: a compromised or vulnerable package install is contained to the project environment instead of the whole workstation or server.

Example of creating a virtual environment:

python3 -m venv env
source env/bin/activate

These commands create and activate a virtual environment named env. From that point, Python packages are installed and run in isolation without affecting the global interpreter.

Scoping variables to prevent unauthorized data exposure

Limiting the scope of variables and functions reduces the chance that sensitive values are exposed or modified elsewhere in the codebase.

  • Avoid global variables when they hold credentials, tokens, or user data.
  • Prefer local variables contained within function scope.

Consider this example with a global variable:

secret = "my super secret password"
def print_secret():
# Accessing global variable
    print(secret)

print_secret()

In this case, the secret variable is accessible to all functions in the module, which increases the risk of accidental disclosure.

Now compare it with an example using a local variable:

def print_secret():
    secret = "my super secret password"
    print(secret)

print_secret()

Here, the secret value is protected by the scope of print_secret, making unintended reuse elsewhere less likely.

Code modularity as a security boundary

Modularity improves maintainability, but it also helps security reviews. Smaller, well-scoped modules are easier to test, reason about, and audit.

  1. Code is easier to organize and reuse safely.
  2. Testing and debugging become simpler because behavior is isolated.
  3. Security review improves because each module can be audited on its own.

Consider two examples: a bad one with mixed responsibilities and a better one split into modules.

Bad example:

def do_something():
# Too many different tasks in one function
    pass

def do_something_else():
# Dependent code that is hard to debug
    pass

Good example:

# module_a.py
def do_part_one():
    print("Part one")

# module_b.py
def do_part_two():
    print("Part two")

# main.py
from module_a import do_part_one
from module_b import do_part_two

def main():
    do_part_one()
    do_part_two()

This separation keeps each part of the code independent and easier to test without broad side effects.

Parameterized queries as the primary defense against SQL injection

Code injection remains one of the most serious threats to any application that handles user input.

  1. Use parameterized queries instead of string formatting when talking to databases.
  2. Validate and sanitize user-controlled input before it reaches business logic.

Example of vulnerable code:

def get_user(user_id):
    query = f"SELECT * FROM users WHERE id = {user_id}"
    return execute_query(query)

If user_id contains SQL fragments, they can be executed by the database.

A safer version:

def get_user(user_id):
    query = "SELECT * FROM users WHERE id = ?"
    return execute_query(query, (user_id,))

With parameterized queries, the input is handled as data rather than executable SQL syntax.

Deserialization risks: why pickle executes arbitrary code

Serialization and deserialization are common in Python applications, but they become dangerous when an attacker can tamper with the serialized data.

  • Avoid unsafe modules like pickle for untrusted input because deserialization can execute arbitrary code.
  • Prefer safer formats such as json for simple data structures.

Example of vulnerable code using the pickle module:

import pickle

def unsafe_deserialization(data):
    return pickle.loads(data)

Safer alternative using the json module:

import json

def safe_deserialization(data):
    return json.loads(data)

Least privilege: limiting what each function is permitted to do

Limiting the permissions of programs and processes to the minimum they need reduces the damage a bug or compromise can cause.

def process_user_data(user_data):
# Code here processes user data without unnecessary privileges
    pass

In this example, process_user_data handles only the data it needs and does not require elevated privileges, which limits exposure if that function is abused.

Password hashing with bcrypt and token-based authentication

Weak authentication and authorization controls often lead directly to account compromise. Passwords should be hashed with a modern password hashing function rather than stored or compared in plaintext.

import bcrypt

password = b"super secret password"
hashed = bcrypt.hashpw(password, bcrypt.gensalt())

Using bcrypt ensures that even if a credential database leaks, original passwords are harder to recover through offline cracking.

Flask session security: secure cookies, HttpOnly, and short lifetimes

Session management is a core part of application security because session tokens often become the real authentication boundary after login.

  1. Set Secure and HttpOnly flags on session cookies.
  2. Rotate the session identifier after successful authentication.
  3. Use short session lifetimes that match the risk of the application.

Example code using Flask:

from flask import Flask, session
from datetime import timedelta

app = Flask(__name__)
app.config.update(
    SESSION_COOKIE_SECURE=True, # HTTPS only
    SESSION_COOKIE_HTTPONLY=True, # Prevent access to cookies via JS
    SESSION_COOKIE_SAMESITE='Lax' # Restrict cookie sending to third-party site requests
)
app.permanent_session_lifetime = timedelta(minutes=15) # 15-minute session timeout

@app.route('/login', methods=['POST'])
def login():
# Check credentials
    session.regenerate() # Regenerate session ID after successful login
    return "Logged in successfully!"

eval() and exec(): dynamic code execution as an attack vector

The eval() and exec() functions execute arbitrary Python code. They should be avoided for user-controlled input because they effectively turn data into code.

Example of dangerous use of eval():

eval('os.system("rm -rf /")')

Code like this can execute operating system commands and should not exist in production paths that process external input.

Static analysis: automating security checks with Bandit

Manual code review misses issues at scale. Bandit is an open-source Python static analysis tool that flags common security issues – hardcoded credentials, use of pickle, SQL string formatting, and calls to eval() – by scanning your codebase’s AST. Run it in CI/CD to catch regressions before they reach production:

pip install bandit
bandit -r ./myproject/

For broader coverage of injection, broken authentication, and cryptographic failures across any language, the OWASP Top Ten and the OWASP Cheat Sheet Series provide detailed remediation guidance. The patterns in this guide – parameterized queries, scoped variables, vetted serialization, bcrypt hashing, and short-lived sessions – directly address the most commonly exploited classes of Python vulnerabilities in web applications.


Kamil Akbari

Kamil Akbari is a cybersecurity editor and author at CyberSecureFox with more than 5 years of experience in cybersecurity software development and security tooling. He focuses on AI security, CVE analysis, ransomware, malware, cloud security, and practical pentesting. His articles are based on official advisories, CVE/NVD data, CISA alerts, vendor publications, and public research reports.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.