Digital Lab

Python CGI Lab

CGI scripts and Python backend logic, running live on ordinary shared hosting — no app server, no port to bind, no process to keep alive. Just Apache and a Python interpreter.

What CGI actually is

CGI — the Common Gateway Interface — is the oldest way to make a web server run a program instead of serving a static file. It dates to 1993. The "interface" part is almost insultingly simple: when a request hits a script, the web server (Apache, here) starts a brand-new process, hands it the request as environment variables plus the raw request body on stdin, and reads whatever the process prints to stdout as the response.

That's the entire protocol. No sockets to open, no framework required, no persistent process. A CGI script is just a program that knows it must:

  1. Print a Content-Type header line
  2. Print one blank line (the header/body separator — easy to forget, breaks everything if missing)
  3. Print the response body

That's hello.py → in its entirety: three print statements.

Why this still matters on shared hosting

Modern Python web apps usually run as a long-lived process — Gunicorn, uWSGI, an ASGI server — sitting behind a reverse proxy, listening on its own port. Cheap shared hosting (like this site's host, World4You) generally won't let you do that: no port binding, no persistent background process, no control over what's listening where.

What shared hosts do support — going back decades — is Apache configured to execute a script and capture its output. That's exactly the contract CGI provides, which is why it's still the practical way to run server-side Python on this kind of hosting. The tradeoff is that the Python interpreter starts cold on every single request — there's no warm process holding a database connection pool or cached state. For small scripts and low traffic, that startup cost is negligible. It would be a real problem at scale.

Statelessness — the core limitation

Because every request gets a fresh process with no memory of the last one, a CGI script by itself has no concept of "earlier" or "later." Anything that needs to persist — a visit count, a session, a queue of feedback messages — has to be written somewhere outside the process, almost always the filesystem on shared hosting (a database is the other option, where available). counter.py → demonstrates this directly: it reads a number from a text file, increments it, writes it back, with an OS-level file lock so two simultaneous requests can't corrupt the count by racing each other.

Reading a request: environment variables and stdin

Everything the browser sent arrives as environment variables — REQUEST_METHOD, QUERY_STRING, HTTP_USER_AGENT, and so on — readable with plain os.environ. A GET request's form data lives entirely in QUERY_STRING. A POST request's body has to be read from sys.stdin, exactly CONTENT_LENGTH bytes — read less and you lose data, read more (or read until EOF) and the script can hang waiting for bytes that will never arrive, since CGI doesn't close stdin for you. env.py → dumps the environment variables for whatever request loaded it; echo.py → parses a real form submission via both GET and POST.

Note: these examples parse the query string and POST body with urllib.parse rather than the stdlib cgi module. cgi.FieldStorage was the traditional tool for this, but the whole cgi module was deprecated in Python 3.11 and removed entirely in 3.13 (PEP 594) — worth knowing if you're maintaining older CGI code that imports it.

Security notes that actually matter here

CGI's simplicity cuts both ways — there's no framework quietly escaping things for you, so a few rules are worth stating plainly:

Live demos

Hello, CGI
/test/hello.py
The protocol in three lines: header, blank line, body.
Request environment
/test/env.py
What Apache actually hands the script per request.
Form echo
/test/echo.py
Parses GET query strings and POST bodies by hand.
Persistent counter
/test/counter.py
State surviving a stateless process, via a locked file.

Advanced demos

Server Status Dashboard
/test/status.py
Dynamic output with no user input — time, Python version, request metadata.
Unit Converter
/test/converter.py
Self-posting form: one script handles both GET (show form) and POST (process input).
Guestbook
/test/guestbook.py
File persistence, XSS prevention with html.escape(), concurrent write locking.
Session Login
/test/login.py
Cookie-based sessions: Set-Cookie, Location redirect, token validation.
← back to presker.at