// python · lab

Server-side rendering in pure Python, part 1: from data to HTML

Build a real server-side-rendered page in ~45 lines of Python — standard library only, no framework. You'll serve HTML built from data, filter it with a query parameter, and prove it's SSR with view-source. Part 1 of 2: here the data is a fixed list; part 2 swaps it for PostgreSQL.

mindmap — quick refresh SSR lab — data → HTML, on the server the pieces http.server — the loop, handled for you a template string with {slots} render(): data → rows → filled template the flow per request parse path + query string filter the data build HTML text send 200 + Content-Type + body proving it's SSR view-source: data is IN the page works with JavaScript disabled curl sees the same content where this leads real template engines add escaping + loops Django/Flask industrialize exactly this

Prerequisites: Python 3 installed, plus the ideas from How HTTP servers work and SSR vs CSR. Time: ~30 minutes.

Server-side rendering sounds like a framework feature. It isn't — it's just building a string of HTML from data, on the server, per request. Today you'll do it with nothing but Python's standard library, so there's nowhere for the magic to hide.

What "rendering" literally is

Before the server, understand the core move: a template with holes, data, and a merge:

That's the entire trick. Real template engines (Django's, Jinja2) add loops, inheritance, and — critically — escaping (more on that at the end), but the mental model stays: template + data → HTML string.

The server, complete

Save this as server.py — it's the whole lab, ~45 lines, standard library only:

# server.py — server-side rendering with only the standard library
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs

STUDENTS = [
    {"id": 1, "name": "Asha",  "course": "AI"},
    {"id": 2, "name": "Ravi",  "course": "AI"},
    {"id": 3, "name": "Meena", "course": "Data Science"},
]

PAGE = """<!doctype html>
<html>
<head><title>Students</title></head>
<body>
  <h1>Students — {course}</h1>
  <ul>
{rows}
  </ul>
</body>
</html>"""

def render(course):
    wanted = [s for s in STUDENTS if course in ("all", s["course"])]
    rows = "\n".join(
        f'    <li>{s["id"]}{s["name"]} ({s["course"]})</li>' for s in wanted
    )
    return PAGE.format(course=course, rows=rows)

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        url = urlparse(self.path)
        if url.path != "/students":
            self.send_error(404, "try /students")
            return
        course = parse_qs(url.query).get("course", ["all"])[0]
        body = render(course).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

if __name__ == "__main__":
    print("serving on http://127.0.0.1:8000/students")
    HTTPServer(("127.0.0.1", 8000), Handler).serve_forever()

Run it and visit the page:

python3 server.py
# → http://127.0.0.1:8000/students

Read it as the server loop

Map the code onto the loop from How HTTP servers work — every stage is here:

  • accept + parsehttp.server does both and calls do_GET with self.path filled in.
  • route — our humble if url.path != "/students" is the routing table (with send_error(404) as the fall-through).
  • handlerrender(course): filter the data, build rows, fill the template. This is the dynamic lane from the static-vs-dynamic simulator, and the SSR lane from the race.
  • respond — status, Content-Type, Content-Length, body. The exact anatomy from the HTTP guide, typed by hand.

Notice ?course=AI — visit http://127.0.0.1:8000/students?course=AI and the server renders a different page from the same template. That per-request variation is what makes it dynamic.

Prove it's server-side

Three experiments, in order of persuasiveness:

  1. View-source (Ctrl+U / Cmd+Option+U): Asha and Ravi are right there in the HTML. The browser received finished content.
  2. Disable JavaScript in DevTools and reload: the page is identical. There is no JS to depend on.
  3. curl it — a program with no rendering engine at all sees the full content:
curl "http://127.0.0.1:8000/students?course=AI"

Now do experiment 1 on a CSR app (any big dashboard-style site): the source shows a nearly-empty <div>. You can see the strategy difference you learned in SSR vs CSR.

One warning before you go: escaping

Our render() pastes data into HTML raw. If a student's name were <script>alert(1)</script>, we'd be injecting code into every visitor's browser — the classic XSS vulnerability. The stdlib fix is one function: pass every value through html.escape() from the html module. Real template engines escape by default, and that — not convenience — is the serious reason to use one in production.

Exercises

  1. Add a column: give each student a year, show it in the row.
  2. Add a route: /courses should render the list of distinct courses (hint: another template string + a second if in do_GET).
  3. Escape it: import html and wrap every interpolated value in html.escape(). Then add a student named <b>Bold</b> and confirm it displays as text instead of becoming markup.
  4. Break the loop (from the servers guide): add time.sleep(5) inside render() and open the page in two tabs at once. Which tab suffers, and why? (HTTPServer is single-threaded; try ThreadingHTTPServer and watch the difference.)

What's next

One honest limitation remains: STUDENTS is a list frozen into the source code — changing the data means editing the program and restarting it. Real applications render from a database. That's exactly part 2: same server, same template, but the rows come live from PostgreSQL.

References

  • MDN Web Docs. (n.d.). Introduction to the server side. Mozilla. Retrieved August 16, 2026, from https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Server-side/First_steps/Introduction
  • OWASP Foundation. (n.d.). Cross site scripting prevention cheat sheet. Retrieved August 16, 2026, from https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
  • Python Software Foundation. (n.d.). http.server — HTTP servers (Python documentation). Retrieved August 16, 2026, from https://docs.python.org/3/library/http.server.html
  • Python Software Foundation. (n.d.). html — HyperText Markup Language support (Python documentation). Retrieved August 16, 2026, from https://docs.python.org/3/library/html.html