Python Session 6 — Flask and Streamlit

From scripts to simple apps

Juan F. Imbet

Paris Dauphine University-PSL

Two paths to apps

  • Flask (micro web framework)
    • Full control over HTTP, URLs, templates, sessions, cookies
    • Perfect for APIs, custom dashboards, teaching core web concepts
    • Typical stack: Flask + Jinja2 + SQLAlchemy + Bootstrap
  • Streamlit (data app framework)
    • Focus on data + UI with minimal web boilerplate
    • Perfect for quick prototypes, EDA dashboards, teaching ML results
    • You think in Python scripts, Streamlit turns them into web apps

What is WSGI?

  • WSGI = Web Server Gateway Interface
    • Standard interface between Python web apps and web servers
    • Allows your Flask app to communicate with web servers (Apache, Nginx, etc.)
  • Think of it as a translator:
    • Web server receives HTTP request → passes to WSGI → your Flask app processes → returns response
  • For development:
    • Flask has built-in server (app.run())
  • For production:
    • Use production-grade WSGI servers like gunicorn or uWSGI
    • They handle multiple requests concurrently and are more robust

Flask: hello app

from flask import Flask

app = Flask(__name__)  # Flask app object

@app.get("/")
def home():
    return "Hello, Flask! 🐍"

if __name__ == "__main__":
    # For local development only (not production)
    app.run(debug=True)
  • Save as app.py

  • Run with Flask CLI (recommended):

    export FLASK_APP=app
    export FLASK_ENV=development  # enables debug mode
    flask run
  • Visit: http://127.0.0.1:5000/

Flask: routes and params

from flask import request

@app.get("/greet/<name>")
def greet(name):
    excited = request.args.get("excited", "0") == "1"
    suffix = "!!!" if excited else "."
    return f"Hi {name}{suffix}"
  • Path parameter: <name> appears in the URL, becomes a function argument
  • Query parameter: ?excited=1 read via request.args
  • Example URL:
    • http://localhost:5000/greet/Ana
    • http://localhost:5000/greet/Ana?excited=1
  • You can restrict types: /user/<int:user_id>, /price/<float:x>

Flask: templates (Jinja2)

from flask import render_template

@app.get("/user/<name>")
def user_profile(name):
    hobbies = ["coding", "running", "music"]
    return render_template("user.html", name=name, hobbies=hobbies)

templates/user.html

<!doctype html>
<html lang="en">
  <body>
    <h1>Hello {{ name }}!</h1>
    <ul>
      {% for h in hobbies %}
        <li>{{ h }}</li>
      {% endfor %}
    </ul>
  </body>
</html>
  • Jinja2 = template engine used by Flask
  • { ... } for variables, {% ... %} for control flow
  • Goal: Python = logic, Jinja = presentation

Flask: forms and JSON

from flask import request, jsonify, render_template

@app.get("/form")
def show_form():
    return render_template("sum_form.html")

@app.post("/sum")
def compute_sum():
    a = int(request.form["a"])
    b = int(request.form["b"])
    total = a + b
    return render_template("sum_result.html", total=total)

@app.post("/api/sum")
def api_sum():
    data = request.get_json()
    a = data["a"]
    b = data["b"]
    return jsonify({"result": a + b})
  • request.form → form fields sent via POST (Content-Type: application/x-www-form-urlencoded)
  • request.get_json() / request.json → JSON body for APIs
  • jsonify converts dict → JSON response with correct headers

Flask: calling APIs with requests

Server code (test.py):

@app.post("/api/sum")
def api_sum():
    data = request.get_json()
    a = data["a"]
    b = data["b"]
    return jsonify({"result": a + b})

Client code (api_test.py):

import requests

data = {"a": 5, "b": 7}

response = requests.post("http://127.0.0.1:5000/api/sum", json=data)
print(response.json())
  • Use requests.post() with json= parameter to send JSON data
  • Server must use request.get_json() to parse the JSON body
  • Response is also JSON, parse with response.json()

Flask: request lifecycle

  • Browser → HTTP request → Web server → Flask app → view function → response
  • The WSGI layer sits between web server and Flask, handling the protocol translation
  • Important globals:
    • request: current HTTP request (method, headers, body, args…)
    • session: per-user, signed cookie storage
    • g: request-scoped storage (DB connection, current user…)
  • Hooks:
from flask import g, request

@app.before_request
def before():
    g.request_path = request.path

@app.after_request
def after(response):
    # e.g. add custom header
    response.headers["X-App-Name"] = "Intro-Flask"
    return response
  • Think of it as a pipeline where you can plug in logic before/after views

Flask: url_for and redirects

from flask import url_for, redirect

@app.get("/")
def index():
    # Generate URLs by function name (safer than hard-coding strings)
    return f'<a href="{url_for("dashboard")}">Go to dashboard</a>'

@app.get("/dashboard")
def dashboard():
    return "Dashboard"

@app.get("/old-dashboard")
def old_dashboard():
    # Permanent redirect to new endpoint
    return redirect(url_for("dashboard"), code=301)
  • url_for("dashboard") builds /dashboard
    • Robust when you later change URLs / add prefixes / blueprints
  • redirect() sends HTTP 302/301 to the browser

Flask: configuration & environments

import os

class Config:
    SECRET_KEY = os.environ.get("SECRET_KEY", "dev-key")
    DEBUG = False

class DevConfig(Config):
    DEBUG = True

class ProdConfig(Config):
    DEBUG = False

app = Flask(__name__)
app.config.from_object(DevConfig)
  • Access anywhere: app.config["SECRET_KEY"]
  • Typical pattern:
    • Config class (Dev / Prod / Test)
    • Read secrets from environment variables (never hard-code passwords)

Flask: application factory pattern

def create_app(config_object=DevConfig):
    app = Flask(__name__)
    app.config.from_object(config_object)
    # register blueprints, extensions…
    return app
  • Benefits:
    • Create multiple app instances with different configs
    • Easier testing (create test app with test config)
    • Better organized for larger applications
  • Usage:
# For development
app = create_app(DevConfig)

# For production
app = create_app(ProdConfig)

Streamlit: hello app

import streamlit as st

st.title("Hello Streamlit 👋")

name = st.text_input("Name", value="Juan")
age = st.slider("Age", min_value=0, max_value=100, value=30)

if name:
    st.write(f"Hi {name}, you are {age} years old.")
  • Run: streamlit run app.py
  • Streamlit watches your code → auto-reload on save
  • Everything runs top-to-bottom on each interaction

Streamlit: widgets

import streamlit as st

st.sidebar.header("Controls")

option = st.selectbox("Model", ["Logit", "Random Forest", "XGBoost"])
threshold = st.slider("Default threshold", 0.0, 1.0, 0.5, 0.01)
upload = st.file_uploader("Upload CSV", type=["csv"])

if upload:
    import pandas as pd
    df = pd.read_csv(upload)
    st.write("Preview:", df.head())

st.write("Selected model:", option)
st.write("Threshold:", threshold)
  • Widgets: text_input, selectbox, slider, checkbox, file_uploader, date_input, …
  • Immediate feedback: UI updates after every interaction

Streamlit: caching

import time
import streamlit as st
import pandas as pd

@st.cache_data
def load_data(path):
    time.sleep(2)  # simulate slow I/O
    return pd.read_csv(path)

df = load_data("data/big.csv")
st.write(df.head())
  • @st.cache_data:
    • Caches pure functions: same inputs → reuse output
    • Clears cache when code changes or when you call clear_cache()
  • Great for: downloading data, feature engineering, slow aggregations

Streamlit: session state

import streamlit as st

if "count" not in st.session_state:
    st.session_state.count = 0

st.write("Count:", st.session_state.count)

if st.button("Increment"):
    st.session_state.count += 1

if st.button("Reset"):
    st.session_state.count = 0
  • st.session_state is similar to Flask session, but kept server-side
  • Use it for:
    • Multi-step forms
    • Wizard-style flows
    • Remembering user selections

Streamlit: caching nuances

  • st.cache_data:
    • For data: reading CSVs, heavy computations, feature engineering
    • Cache key = function code + arguments
  • st.cache_resource:
    • For resources: ML models, DB connections, clients
    • Only created once per session (unless code changes)
  • Example:
@st.cache_resource
def load_model():
    # expensive model loading
    ...
    return model
  • Be careful with non-deterministic functions (randomness, time)

Streamlit: state

import streamlit as st

if "n" not in st.session_state:
    st.session_state.n = 0

st.write("n =", st.session_state.n)

col1, col2 = st.columns(2)
if col1.button("Add 1"):
    st.session_state.n += 1
if col2.button("Multiply by 2"):
    st.session_state.n *= 2
  • Use session state to maintain values across reruns
  • Useful for multi-step workflows, calculators, and interactive applications