DataLens

Serving static files

Serve CSS, images, JavaScript and downloadable files from the static folder, link them with url_for, and send files generated on the fly.

Level: Intermediate · 8 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

A page is more than HTML: it needs a stylesheet, a logo, maybe a script. These files don't change per request, so they're called static files. Flask serves them from a folder named static/ next to your app, with no route to write.

shop/
├── app.py
├── templates/
│   └── base.html
└── static/
    ├── style.css
    └── logo.svg

The static route

Every Flask app automatically has a route /static/<path:filename> that serves files from static/:

from flask import Flask

app = Flask(__name__)
client = app.test_client()

r = client.get("/static/style.css")
print(r.status_code, r.mimetype)
print(r.get_data(as_text=True))
print(client.get("/static/missing.css").status_code)
200 text/css
body { font-family: system-ui, sans-serif; color: #1A1D3A; }
h1 { color: #5669FD; }

404

Flask sets the right Content-Type from the file extension (text/css for .css, image/svg+xml for .svg), which is how the browser knows what it received. A missing file is a normal 404.

Linking static files: url_for("static")

In templates, link static files with url_for("static", filename=...), never a hard-coded path:

from flask import Flask, render_template_string

app = Flask(__name__)

PAGE = """<!doctype html>
<html>
<head><link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"></head>
<body><img src="{{ url_for('static', filename='logo.svg') }}" alt="Toko Kita"><h1>Toko Kita</h1></body>
</html>"""


@app.route("/")
def home():
    return render_template_string(PAGE)


print(app.test_client().get("/").get_data(as_text=True))
<!doctype html>
<html>
<head><link rel="stylesheet" href="/static/style.css"></head>
<body><img src="/static/logo.svg" alt="Toko Kita"><h1>Toko Kita</h1></body>
</html>

Why url_for again? Because in production the static folder is often moved: served from /assets/, or from a CDN at another domain, or with a version in the name for caching. Change one setting, and every url_for("static", ...) link follows.

Caching and cache busting

Browsers cache static files so pages load fast. The catch: after you change style.css, visitors may keep the old copy. The usual fix is cache busting: add a version to the URL, so a changed file has a new URL:

from flask import Flask, render_template_string

app = Flask(__name__)
VERSION = "2024.07.1"

with app.test_request_context():
    print(render_template_string("{{ url_for('static', filename='style.css', v=version) }}", version=VERSION))
/static/style.css?v=2024.07.1

Extra keyword arguments to url_for become query parameters. Bump VERSION on each release, and every browser fetches the new stylesheet.

Sending files from other places

Static files are public: anyone who guesses the URL gets them. For files that need checks (an invoice for the logged-in customer) or live elsewhere, use send_from_directory in your own route:

from flask import Flask, send_from_directory, abort

app = Flask(__name__)
ALLOWED = {"price-list.pdf", "catalog-2024.pdf"}


@app.route("/downloads/<name>")
def download(name):
    if name not in ALLOWED:
        abort(404)
    return send_from_directory("downloads", name, as_attachment=True)


client = app.test_client()
r = client.get("/downloads/price-list.pdf")
print(r.status_code, r.headers["Content-Disposition"])
print(client.get("/downloads/../app.py").status_code)
200 attachment; filename=price-list.pdf
404

Generated files: CSV reports

A route can also return a file it creates on the fly, such as a CSV export. Set the headers yourself:

import csv
import io
from flask import Flask, Response

app = Flask(__name__)
ROWS = [("category", "revenue"), ("Electronics", 5113000), ("Kitchen", 2610000)]


@app.route("/export.csv")
def export():
    buffer = io.StringIO()
    csv.writer(buffer).writerows(ROWS)
    return Response(buffer.getvalue(), mimetype="text/csv",
                    headers={"Content-Disposition": "attachment; filename=revenue.csv"})


r = app.test_client().get("/export.csv")
print(r.mimetype, r.headers["Content-Disposition"])
print(r.get_data(as_text=True))
text/csv attachment; filename=revenue.csv
category,revenue
Electronics,5113000
Kitchen,2610000

Response(body, mimetype=..., headers=...) gives full control over the response, when returning a string isn't enough.

Why this matters for clean code

Treat file locations like any other configuration: never hard-code them in templates. url_for("static", ...) keeps links working when infrastructure changes, send_from_directory keeps downloads safe, and generated files go through one small route with explicit headers. Each piece does one job, in one place.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

Next: Handling form data and query strings →