Python A Simple Python Web Server

Serve files and write a small JSON endpoint with http.server, framing responses correctly and keeping the demo local.

Lesson 73 of 107Advanced12 min read

Python's standard library includes a basic web server in the http.server module. With one command it serves the files in a folder, and with a small class it can answer requests with your own responses, such as JSON:

Terminal
python -m http.server 8000 --bind 127.0.0.1

Open http://127.0.0.1:8000 in a browser and you'll see a listing of the current folder. This lesson explains how requests reach a handler, how an HTTP response is structured, why the body must be bytes, and why this server is for learning and local testing rather than production. The examples report the status of a workshop robot. For real web applications, the next step is a framework such as Flask.

Request handlers

HTTPServer listens for connections and passes each request to a handler class you provide. The handler defines one method per HTTP method: do_GET, do_POST, do_DELETE and so on. Python calls the matching method and makes the request details available as attributes:

AttributeContains
self.paththe path and query string, such as /status?verbose=1
self.commandthe method, such as GET
self.headersthe request headers
self.rfilethe request body stream (read Content-Length bytes)
self.wfilethe response stream you write to
Python
from http.server import BaseHTTPRequestHandler

class StatusHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/status":
            ...                     # build a response
        else:
            self.send_error(404)

SimpleHTTPRequestHandler, used by python -m http.server, is a ready-made handler that serves files from a folder. Custom handlers like this one decide everything themselves, which is a good way to see what web frameworks do behind the scenes.

Response framing

An HTTP response has three parts, sent in order: a status line, headers, then a blank line and the body. The handler methods map onto them directly:

Python
self.send_response(200)                                   # status line
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))        # headers
self.end_headers()                                        # blank line
self.wfile.write(body)                                    # body

The order matters: once end_headers() is called, the headers have been sent and can't be changed, and anything written before it would corrupt the response. Content-Type tells the client how to interpret the body; Content-Length tells it how many bytes to expect.

self.send_error(404) writes a complete error response, status and HTML body included, in one call. Status codes follow the conventions covered in HTTP requests and REST APIs: 200 for success, 404 for an unknown path, 405 for an unsupported method, 400 for a malformed request.

Encoding

self.wfile is a binary stream, so you write bytes, not str. JSON text must be encoded first:

Python
import json
body = json.dumps({"robot": "ready", "battery": "86%"}).encode("utf-8")

Content-Length must be the length of those bytes, not the number of characters. They differ as soon as the text contains non-ASCII characters:

Python
text = "Café"
print(len(text), len(text.encode("utf-8")))   # 4 5

If you send the character count, the client may cut off the end of the body or wait for bytes that never arrive. Declare the encoding in the header, charset=utf-8, so clients decode the body correctly. Reading a request body works the other way: read int(self.headers["Content-Length"]) bytes from self.rfile and decode them.

Local boundaries

http.server is intended for development and learning. The Python documentation explicitly warns that it is not recommended for production: it only implements basic security checks, handles requests simply, and lacks the features a public site needs, such as HTTPS, robust request parsing, authentication and protection against slow or malicious clients.

Bind demo servers to 127.0.0.1 (localhost), so only your own computer can connect: HTTPServer(("127.0.0.1", 8080), Handler) or python -m http.server --bind 127.0.0.1. Binding to 0.0.0.0 exposes the server, and with SimpleHTTPRequestHandler every file in the folder, to anyone on your network.

Real applications run on a WSGI or ASGI server such as Gunicorn or Uvicorn, usually behind a reverse proxy like Nginx, with a framework such as Flask, Django or FastAPI handling routing. The concepts from this lesson, handlers, status, headers and byte bodies, are exactly what those layers manage for you.

Independent practice and a worked check

Task: serve a local status route and verify a known path, an unknown path and the JSON content type. Extend the handler with a second route, /battery, and check all three paths:

Python
routes = {"/status": {"robot": "ready"}, "/battery": {"level": 86}}

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        payload = routes.get(self.path)
        if payload is None:
            self.send_error(404)
            return
        body = json.dumps(payload).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

Start it with the background-thread pattern from the example and request /status, /battery and /nope: expect two 200 JSON responses and one 404. For the worked check, the type written to the response body stream is bytes.

Reference and next practice

The http.server documentation lists handler attributes and the production warning. Continue with the Flask introduction and revisit HTTP and REST. Practise with the Python exercises and Python quiz.

A Simple Python Web Server Summary

  • Request handlers: A handler inspects the request path and writes an HTTP response.
  • Response framing: A response includes a status, headers and body bytes.
  • Encoding: JSON text must be encoded into bytes for a binary response stream.
  • Local boundaries: http.server is useful for controlled learning and local development.

A Simple Python Web Server Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A Simple Python Web Server in a small program

Run it, then visit http://127.0.0.1:8080/status; the browser shows {"robot": "ready"}. Any other path returns a 404 page. Stop the server with Ctrl+C.

Python
import json
from http.server import BaseHTTPRequestHandler, HTTPServer

class StatusHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/status":
            self.send_error(404)
            return
        body = json.dumps({"robot": "ready"}).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

if __name__ == "__main__":
    HTTPServer(("127.0.0.1", 8080), StatusHandler).serve_forever()
Line-by-line explanation
  1. Lines 6 to 8Unknown paths get a complete 404 response from send_error().
  2. Line 9The JSON text is encoded to bytes before sending.
  3. Lines 10 to 14Status, headers, the blank line and then the body, in that order.
  4. Lines 16 to 17The server binds to localhost only and runs until interrupted.
Example 2

Testing the server from Python

This starts the same kind of handler in a background thread and checks a known path, an unknown path and the content type.

Python
import json, threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.request import urlopen
from urllib.error import HTTPError

class StatusHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/status":
            self.send_error(404)
            return
        body = json.dumps({"robot": "ready", "note": "café"}).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)
    def log_message(self, *args):
        pass

server = HTTPServer(("127.0.0.1", 0), StatusHandler)
threading.Thread(target=server.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{server.server_address[1]}"

with urlopen(base + "/status") as response:
    print(response.status, response.headers["Content-Type"])
    print(json.loads(response.read()))
try:
    urlopen(base + "/missing")
except HTTPError as error:
    print(error.code, error.reason)
server.shutdown()
Output
200 application/json; charset=utf-8
{'robot': 'ready', 'note': 'café'}
404 Not Found
Line-by-line explanation
  1. Lines 17 to 18Overriding log_message keeps the server from printing a log line for each request.
  2. Line 20Port 0 lets the operating system choose a free port.
  3. Lines 24 to 26The standard library's urlopen acts as the client, so no extra packages are needed.

Common A Simple Python Web Server Mistakes

Writing a str to the response stream

Don't
self.wfile.write(json.dumps({"robot": "ready"}))   # TypeError: a bytes-like object is required
Do
self.wfile.write(json.dumps({"robot": "ready"}).encode("utf-8"))

Exposing a folder to the whole network

Don't
python -m http.server 8000 --bind 0.0.0.0     # anyone on the network can download your files
Do
python -m http.server 8000 --bind 127.0.0.1

A Simple Python Web Server in Real Projects

Where http.server is useful

Developers use python -m http.server to preview a static website, share files with a colleague on a trusted network for a moment, or serve test fixtures. Custom handlers make convenient fake APIs in automated tests, exactly like the background-thread server above, so client code can be tested without a real service.

For anything users depend on, the same ideas are handled by a framework and a production server. Understanding the raw request-and-response cycle makes framework features, such as routing decorators, jsonify() and response objects, much easier to reason about.

A Simple Python Web Server FAQ

How do I start a simple HTTP server in Python?

Run python -m http.server 8000 --bind 127.0.0.1 in the folder you want to serve, then open http://127.0.0.1:8000. On Python 2 the equivalent was python -m SimpleHTTPServer.

Is Python's http.server safe for production?

No. The documentation recommends against it for production because it only implements basic security checks. Use a framework such as Flask, Django or FastAPI on a production server like Gunicorn or Uvicorn.

Why must I encode the response body?

self.wfile is a byte stream, because HTTP sends bytes. Convert text with .encode("utf-8") and declare charset=utf-8 in the Content-Type header so the client decodes it the same way.

How do I read a POST body in http.server?

In do_POST, read the length from the headers and then that many bytes: length = int(self.headers["Content-Length"]), data = json.loads(self.rfile.read(length)). Validate the data before using it.

What is the difference between 127.0.0.1 and 0.0.0.0?

127.0.0.1 accepts connections only from the same computer. 0.0.0.0 listens on every network interface, so other devices can connect. Use localhost for demos and development.

A Simple Python Web Server Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choiceeasy

What type is written to the response body stream?

Multiple choicemedium

Which review concern is valid for a simple python web server?

A Simple Python Web Server Quiz

2 questions to check what you learned.

  1. What type is written to the response body stream?

    • A Python dictionary
    • bytes
    • Python ignores the distinction
  2. Which statement correctly describes encoding?

    • JSON text must be encoded into bytes for a binary response stream.
    • Only the example output matters, not the rule
    • Every operation has the same behavior