GeezerVisonThe World Wide Web, restored to 800 pixels of glory. |
| Home|About|Gaming cabinet|Archives|How it works|Starter sites |
GeezerVison is showing zetcode.com — Python http.server - simple HTTP servers in Python Python http.serverlast modified October 29, 2026 Python 3.15
In this article we show how to use the
The module contains server classes and request handler classes. The
The Serving files from the command line
The quickest way to use the module is to run it with the index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Home page</title> </head> <body> <p>Served by Python http.server</p> </body> </html>
We place this
The server prints a line for each request. Here, we requested the home page
and a $ python -m http.server -d public Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ... 127.0.0.1 - - [29/Oct/2026 22:22:26] "GET / HTTP/1.1" 200 - 127.0.0.1 - - [29/Oct/2026 22:22:26] code 404, message File not found 127.0.0.1 - - [29/Oct/2026 22:22:26] "GET /favicon.ico HTTP/1.1" 404 - Inspecting the response
In another terminal, we can request the page with
The $ curl -i localhost:8000/ HTTP/1.0 200 OK Server: SimpleHTTP Python/3.15.2 Date: Thu, 29 Oct 2026 21:22:28 GMT Content-type: text/html Content-Length: 155 Last-Modified: Thu, 29 Oct 2026 21:22:27 GMT <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Home page</title> </head> <body> <p>Served by Python http.server</p> </body> </html> Command line optionsThe command accepts several options. Here we bind the server to the loopback interface, choose port 9000, and use two options added in Python 3.15.
By default, the server listens on all network interfaces, so other computers
on the network can reach it. The $ python -m http.server -b 127.0.0.1 -d public -H Cache-Control no-store --content-type 'text/plain; charset=utf-8' 9000 & $ curl -I 127.0.0.1:9000/README HTTP/1.0 200 OK Server: SimpleHTTP Python/3.15.2 Date: Thu, 29 Oct 2026 21:22:29 GMT Content-type: text/plain; charset=utf-8 Content-Length: 15 Last-Modified: Thu, 29 Oct 2026 21:22:28 GMT Cache-Control: no-store Directory listingsFor a directory without an index page, the server generates a listing of its files.
The $ curl localhost:8000/
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<style type="text/css">
:root {
color-scheme: light dark;
}
</style>
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href="data.csv">data.csv</a></li>
<li><a href="images/">images/</a></li>
<li><a href="notes.txt">notes.txt</a></li>
</ul>
<hr>
</body>
</html>
A custom request handler
To generate responses in code, we subclass
hello_server.py
from http.server import BaseHTTPRequestHandler, HTTPServer
class HelloHandler(BaseHTTPRequestHandler):
def do_GET(self):
body = 'Hello there!\n'.encode('utf-8')
self.send_response(200)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
with HTTPServer(('localhost', 8000), HelloHandler) as server:
print('Serving on http://localhost:8000')
server.serve_forever()
The server answers every self.send_response(200)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
A response is written in a fixed order: the status line, the headers, an empty
line, and the body. The with HTTPServer(('localhost', 8000), HelloHandler) as server:
server.serve_forever()
The server is created with an address and the handler class. A new handler
instance is created for every request. The $ curl -i localhost:8000/ HTTP/1.0 200 OK Server: BaseHTTP Python/3.15.2 Date: Thu, 29 Oct 2026 21:22:31 GMT Content-Type: text/plain; charset=utf-8 Content-Length: 13 Hello there! Query parameters
The query_server.py
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, urlsplit
class GreetHandler(BaseHTTPRequestHandler):
def do_GET(self):
url = urlsplit(self.path)
if url.path != '/greet':
self.send_error(404, 'Not Found')
return
query = parse_qs(url.query)
name = query.get('name', ['guest'])[0]
times = int(query.get('times', ['1'])[0])
body = f'Hello {name}!\n'.encode('utf-8') * times
self.send_response(200)
self.send_header('Content-Type', 'text/plain; charset=utf-8')
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
with HTTPServer(('localhost', 8000), GreetHandler) as server:
server.serve_forever()
The server greets the user given in the url = urlsplit(self.path)
if url.path != '/greet':
self.send_error(404, 'Not Found')
return
The query = parse_qs(url.query)
name = query.get('name', ['guest'])[0]
The $ curl 'localhost:8000/greet?name=Lucy'
Hello Lucy!
$ curl 'localhost:8000/greet?name=J%C3%A1n×=2'
Hello Ján!
Hello Ján!
$ curl localhost:8000/greet
Hello guest!
$ curl -i localhost:8000/hello
HTTP/1.0 404 Not Found
Server: BaseHTTP Python/3.15.2
Date: Thu, 29 Oct 2026 21:22:32 GMT
Connection: close
Content-Type: text/html;charset=utf-8
Content-Length: 455
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<style type="text/css">
:root {
color-scheme: light dark;
}
</style>
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 404</p>
<p>Message: Not Found.</p>
<p>Error code explanation: 404 - Nothing matches the given URI.</p>
</body>
</html>
A JSON APIThe next server provides a small REST API over an in-memory dictionary of users. json_server.py
import json
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
users = {
1: {'id': 1, 'name': 'John Doe', 'occupation': 'gardener'},
2: {'id': 2, 'name': 'Roger Roe', 'occupation': 'driver'},
}
class UserHandler(BaseHTTPRequestHandler):
def send_json(self, status, data, headers=()):
body = json.dumps(data).encode('utf-8')
self.send_response(status)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
for name, value in headers:
self.send_header(name, value)
self.end_headers()
self.wfile.write(body)
def do_GET(self):
parts = self.path.strip('/').split('/')
if parts == ['users']:
self.send_json(HTTPStatus.OK, list(users.values()))
elif len(parts) == 2 and parts[0] == 'users' and parts[1].isdigit():
user = users.get(int(parts[1]))
if user is None:
self.send_json(HTTPStatus.NOT_FOUND, {'error': 'user not found'})
else:
self.send_json(HTTPStatus.OK, user)
else:
self.send_json(HTTPStatus.NOT_FOUND, {'error': 'unknown path'})
def do_POST(self):
if self.path != '/users':
self.send_json(HTTPStatus.NOT_FOUND, {'error': 'unknown path'})
return
length = int(self.headers.get('Content-Length', 0))
try:
data = json.loads(self.rfile.read(length))
user = {'id': max(users) + 1, 'name': data['name'],
'occupation': data['occupation']}
except (json.JSONDecodeError, KeyError, TypeError):
self.send_json(HTTPStatus.BAD_REQUEST, {'error': 'invalid user data'})
return
users[user['id']] = user
self.send_json(HTTPStatus.CREATED, user,
[('Location', f'/users/{user["id"]}')])
with ThreadingHTTPServer(('localhost', 8000), UserHandler) as server:
server.serve_forever()
The parts = self.path.strip('/').split('/')
The self.send_json(HTTPStatus.OK, list(users.values()))
The $ curl localhost:8000/users
[{"id": 1, "name": "John Doe", "occupation": "gardener"}, {"id": 2, "name": "Roger Roe", "occupation": "driver"}]
$ curl localhost:8000/users/2
{"id": 2, "name": "Roger Roe", "occupation": "driver"}
$ curl -i localhost:8000/users/7
HTTP/1.0 404 Not Found
Server: BaseHTTP Python/3.15.2
Date: Thu, 29 Oct 2026 21:22:33 GMT
Content-Type: application/json
Content-Length: 27
{"error": "user not found"}
Handling POST requests
The length = int(self.headers.get('Content-Length', 0))
try:
data = json.loads(self.rfile.read(length))
The self.send_json(HTTPStatus.CREATED, user,
[('Location', f'/users/{user["id"]}')])
A created resource is answered with the 201 status and a
$ curl -i -d '{"name": "Lucy Smith", "occupation": "teacher"}' localhost:8000/users
HTTP/1.0 201 Created
Server: BaseHTTP Python/3.15.2
Date: Thu, 29 Oct 2026 21:22:34 GMT
Content-Type: application/json
Content-Length: 56
Location: /users/3
{"id": 3, "name": "Lucy Smith", "occupation": "teacher"}
$ curl -d '{"name": "Tom"}' localhost:8000/users
{"error": "invalid user data"}
$ curl localhost:8000/users/3
{"id": 3, "name": "Lucy Smith", "occupation": "teacher"}
ThreadingHTTPServer
The threading_demo.py
import threading
import time
from concurrent.futures import ThreadPoolExecutor
from http.server import BaseHTTPRequestHandler, HTTPServer, ThreadingHTTPServer
from urllib.request import urlopen
class SlowHandler(BaseHTTPRequestHandler):
def do_GET(self):
time.sleep(0.5)
body = b'done\n'
self.send_response(200)
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
pass
def measure(server_class):
with server_class(('localhost', 0), SlowHandler) as server:
threading.Thread(target=server.serve_forever, daemon=True).start()
url = f'http://localhost:{server.server_port}/'
start = time.perf_counter()
with ThreadPoolExecutor(4) as pool:
replies = list(pool.map(lambda _: urlopen(url).read(), range(4)))
elapsed = time.perf_counter() - start
server.shutdown()
print(f'{server_class.__name__:20} {len(replies)} requests in {elapsed:.1f} s')
measure(HTTPServer)
measure(ThreadingHTTPServer)
Each request takes half a second. We send four requests at once and measure how long they take with each server class. with server_class(('localhost', 0), SlowHandler) as server:
threading.Thread(target=server.serve_forever, daemon=True).start()
Port 0 lets the operating system choose a free port, which we read from
def log_message(self, format, *args):
pass
Overriding $ python threading_demo.py HTTPServer 4 requests in 2.0 s ThreadingHTTPServer 4 requests in 0.5 s Serving files with extra headers
The static_server.py
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
class StaticHandler(SimpleHTTPRequestHandler):
default_content_type = 'text/plain; charset=utf-8'
def log_request(self, code='-', size='-'):
print(f'{self.command} {self.path} -> {code}', flush=True)
def log_error(self, format, *args):
pass
handler = partial(
StaticHandler,
directory='public',
extra_response_headers=[
('Cache-Control', 'max-age=60'),
('X-Content-Type-Options', 'nosniff'),
],
)
with ThreadingHTTPServer(('localhost', 8000), handler) as server:
print('Serving public/ on http://localhost:8000', flush=True)
server.serve_forever()
The server serves the handler = partial(
StaticHandler,
directory='public',
extra_response_headers=[
('Cache-Control', 'max-age=60'),
('X-Content-Type-Options', 'nosniff'),
],
)
The server creates handler instances itself, so we use
default_content_type = 'text/plain; charset=utf-8'
The $ curl -I localhost:8000/README HTTP/1.0 200 OK Server: SimpleHTTP Python/3.15.2 Date: Thu, 29 Oct 2026 21:22:35 GMT Content-type: text/plain; charset=utf-8 Content-Length: 15 Last-Modified: Thu, 29 Oct 2026 21:22:34 GMT Cache-Control: max-age=60 X-Content-Type-Options: nosniff Custom request logging
The handler of the previous example also replaces the default request log
format. Its def log_request(self, code='-', size='-'):
print(f'{self.command} {self.path} -> {code}', flush=True)
def log_error(self, format, *args):
pass
We print the method, the path, and the status code. The default
$ python static_server.py Serving public/ on http://localhost:8000 GET / -> 200 GET /README -> 200 GET /missing.html -> 404 HTTPS
Since Python 3.14, the command line server supports HTTPS with the
The $ openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout key.pem -out cert.pem -days 30 -subj /CN=localhost ----- $ python -m http.server -d public --tls-cert cert.pem --tls-key key.pem 8443 & $ curl https://localhost:8443/ curl: (60) SSL certificate problem: self-signed certificate More details here: https://curl.se/docs/sslcerts.html curl failed to verify the legitimacy of the server and therefore could not establish a secure connection to it. To learn more about this situation and how to fix it, please visit the web page mentioned above. $ curl -k https://localhost:8443/ <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Home page</title> </head> <body> <p>Served by Python http.server</p> </body> </html> Source
In this article we have worked with the AuthorMy name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming. List all Python tutorials. |
| You are visitor 01402520 — GeezerVison does not store the pages you look up. Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required. |