The http.server module defines classes for implementing HTTP servers. It includes SimpleHTTPRequestHandler for serving files from a directory, and BaseHTTPRequestHandler for building custom request handlers. It is intended for development, testing, and learning — not production deployment.
Turn any folder into a website
What you will learn: the one-command file server, and the same thing written in code with HTTPServer and SimpleHTTPRequestHandler.
How to read this tab: python -m http.server is the fastest way to share files or test a static site. Try it in a real terminal.
With a single command, http.server turns any folder into a website you can open in a browser. It is the fastest way to share files on a local network, test a static site, or see HTTP working firsthand — all built into Python, nothing to install.
--bind 0.0.0.0 exposes the server to your whole network. The default binds to localhost (only your machine). 0.0.0.0 makes it reachable by other devices — handy for sending a file to your phone, but anyone on the network can access those files while it runs.
The instant file server
The most common use needs no code at all — just a command. From any directory, run the module and it serves those files over HTTP.
# Serve the current directory on port 8000
$ python -m http.server 8000
# Then open http://localhost:8000 in a browser.
# You'll see a clickable listing of the folder's files.
# Serve on a specific address (e.g. to share on your local network)
$ python -m http.server 8000 --bind 0.0.0.0
# Serve a different directory without changing into it
$ python -m http.server 8000 --directory /path/to/files
# Press Ctrl-C to stop the server.By default the server binds to localhost (only your machine). --bind 0.0.0.0 makes it reachable by other devices on your network — handy for sending a file to your phone, but be aware anyone on the network can access those files while it runs.
The same thing, in code
from http.server import HTTPServer, SimpleHTTPRequestHandler
# Serve the current directory programmatically
server = HTTPServer(("localhost", 8000), SimpleHTTPRequestHandler)
print("Serving on http://localhost:8000")
try:
server.serve_forever() # run until interrupted
except KeyboardInterrupt:
server.shutdown()
print("\nServer stopped")
# Serve a specific directory (Python 3.7+)
from functools import partial
handler = partial(SimpleHTTPRequestHandler, directory="/path/to/public")
server = HTTPServer(("localhost", 8000), handler)
# server.serve_forever()✅ Beginner tab complete
- I can serve a directory with python -m http.server
- I know --bind 0.0.0.0 exposes it to my local network
- I can run the same server in code with HTTPServer
- I know it is for development and learning, not production
Custom handlers and request handling
What you will learn: subclassing BaseHTTPRequestHandler with do_GET/do_POST, the fixed response order, and reading request bodies.
How to read this tab: The response order is fixed: send_response, send_header, end_headers, then write the body. Never reorder it.
The response order is fixed and unforgiving. Always: send_response(code), then send_header(...) for each header, then end_headers(), then write the body to self.wfile. Skipping end_headers() or writing the body too early produces a broken response.
Building a custom request handler
To do more than serve files — return JSON, handle different URLs, accept form data — subclass BaseHTTPRequestHandler and define methods named after HTTP verbs: do_GET, do_POST, and so on.
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
# self.path is the requested URL path, e.g. "/api/status"
if self.path == "/":
self.send_response(200) # status code
self.send_header("Content-Type", "text/html")
self.end_headers() # blank line ends headers
self.wfile.write(b"<h1>Hello from Python</h1>")
elif self.path == "/api/status":
body = json.dumps({"status": "ok", "version": "1.0"}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_error(404, "Not Found") # built-in error response
def do_POST(self):
# Read the request body using the Content-Length header
length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(length)
data = json.loads(body) if body else {}
# ... process data ...
reply = json.dumps({"received": data}).encode()
self.send_response(201)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(reply)
server = HTTPServer(("localhost", 8000), MyHandler)
# server.serve_forever()Always in this sequence: send_response(code), then send_header(...) for each header, then end_headers(), then write the body to self.wfile. Skipping end_headers() or writing the body too early produces a broken response.
http.server is for development and learning only. It is single-threaded by default, has no HTTPS, no protection against malicious requests, and limited performance. For production use Flask/Django + Gunicorn (WSGI) or FastAPI + Uvicorn (ASGI).
Why it is not for production
# http.server is single-threaded by default — one request at a time.
# A slow request blocks all others. You can add threading:
from http.server import HTTPServer, SimpleHTTPRequestHandler
from socketserver import ThreadingMixIn
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
pass
# Python 3.7+ actually provides http.server.ThreadingHTTPServer directly:
from http.server import ThreadingHTTPServer # handles requests in threads
# But even threaded, http.server lacks what production needs:
# - No HTTPS/TLS out of the box
# - No protection against malformed/malicious requests at scale
# - No process management, load balancing, or graceful restarts
# - Limited performance under real load
# For production, use a real WSGI/ASGI server with a framework:
# - Flask or Django + Gunicorn/uWSGI (WSGI)
# - FastAPI + Uvicorn (ASGI, async)
# http.server is for development, testing, and learning ONLY.self.wfile as bytes, and read the request body from self.rfile. They are file-like objects over the socket, so text must be encoded before writing.✅ Intermediate tab complete
- I can subclass BaseHTTPRequestHandler with do_GET and do_POST
- I follow the order: send_response, send_header, end_headers, write body
- I can read a request body using the Content-Length header
- I know to write the body to self.wfile as bytes
The handler lifecycle and WSGI
What you will learn: how the server dispatches to do_VERB methods, the request attributes, and the path to WSGI and production servers.
How to read this tab: Read to see how http.server connects to wsgiref, Gunicorn, and the production web stack.
The handler lifecycle, BaseHTTPRequestHandler internals, and WSGI
Each incoming connection causes the server to instantiate the handler class and call its handle_one_request method, which parses the request line and headers, then dispatches to the do_VERB method matching the HTTP method. The handler exposes the parsed request through attributes: self.command (the method), self.path (the raw URL path including query string), self.headers (an email.message.Message of the headers), and the self.rfile/self.wfile streams for body I/O.
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1" # enable keep-alive (needs Content-Length)
def do_GET(self):
# Parse path and query string properly
parsed = urlparse(self.path)
path = parsed.path # "/search"
params = parse_qs(parsed.query) # {"q": ["python"]}
# Inspect request headers
user_agent = self.headers.get("User-Agent", "unknown")
body = f"path={path} params={params}".encode()
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
# Override to customise or silence the default request logging
pass # silent server
# For real applications, the standard interface is WSGI (PEP 3333),
# which decouples the app from the server. The stdlib includes a
# reference WSGI server in wsgiref:
from wsgiref.simple_server import make_server
def app(environ, start_response):
# environ holds the request; start_response sets status + headers
start_response("200 OK", [("Content-Type", "text/plain")])
return [b"Hello from a WSGI app"]
# with make_server("", 8000, app) as httpd:
# httpd.serve_forever()
# This same 'app' runs unchanged under Gunicorn, uWSGI, etc. in production.The relationship to production servers runs through WSGI (PEP 3333), the standard interface between Python web applications and servers. The stdlib's wsgiref.simple_server is a reference implementation useful for testing, and the key insight is that a WSGI app callable written against it runs unchanged under production servers like Gunicorn or uWSGI — only the server is swapped. For asynchronous applications, the analogous standard is ASGI, served by Uvicorn or Hypercorn. The progression mirrors the socket module's: http.server teaches the request/response mechanics directly, wsgiref introduces the application/server boundary, and production deployment swaps in a robust server while keeping the application code stable.
✅ Expert tab complete
- I know the server dispatches to do_VERB based on the HTTP method
- I can parse path and query string with urllib.parse
- I know WSGI (PEP 3333) decouples the app from the server
- I know a wsgiref app runs unchanged under Gunicorn/uWSGI in production