A Simple Python HTTP Server

Although, we aren’t gonna need it in this article but it’s always good to setup a virtual environment when working with Python. Check out my article on how to create a virtual environment for Python: /blog/python/how-to-create-a-python-virtual-environment/.

To create a simple Python HTTP Server, we are going to use two modules called http.server and socketserver. They both are part of Python’s Standard Library, we don’t need to install them.

Important! This material is made for learning purposes. DO NOT use it in production. http.server is not recommended for production. It only implements basic security checks.

 

Let’s create a file called server.py and define our Server class there.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# server.py
from socketserver import TCPServer
from http.server import SimpleHTTPRequestHandler


class Server:
    def __init__(self,
                 server_address=('127.0.0.1', 0),
                 handler=SimpleHTTPRequestHandler):
        self.server_address = server_address
        self.handler = handler

    def serve(self):
        with TCPServer(self.server_address, self.handler) as httpd:
            serving_at = httpd.server_address

            print(f'serving at http://{serving_at[0]}:{serving_at[1]}')
            httpd.serve_forever()


server = Server()
server.serve()

The preceding code-block will create a running server, listening, by default, on a port chosen by your OS and serving the current working directory.

 

How it works

On lines 8,9 the Server class initiates with two arguments - server_address and handler.

6
7
8
9
class Server:
    def __init__(self,
                 server_address=('127.0.0.1', 0),
                 handler=SimpleHTTPRequestHandler):

The server_address is a tuple taking two values. The first value is the host, which is 127.0.0.1 for localhost. The second value is the port number, which by default is 0, this allows the operating system to choose itself on which port to serve. The handler, by default, accepts SimpleHTTPRequestHandler class. SimpleHTTPRequestHandler is part of the http.server module and is responsible for handling the client requests.

You can see how the handler is used by a TCPServer instance on line 14. TCPServer itself is part of the socketserver module and instantiates with 3 arguments: server_address, RequestHandlerClass, and bind_and_activate. We’ll skip the bind_and_activate argument in this article. For the other two, you already understand what they do.

13
14
    def serve(self):
        with TCPServer(self.server_address, self.handler) as httpd:

On line 15, we call server_address from the newly created TCPServer instance to print the server information on line 17. Here, the port number is set by the OS, already. We set serving_at as the variable name here to escape confusion with the variable on line 10.

15
16
17
            serving_at = httpd.server_address

            print(f'serving at http://{serving_at[0]}:{serving_at[1]}')

On line 18, we call the serve_forever() method. It runs the server in a loop and never exits unless the program gets terminated.

18
            httpd.serve_forever()

Finally, on lines 21, 22 we instantiate the Server class and call its serve method.

21
22
server = Server()
server.serve()

 

Creating a custom handler

You can create a custom handler to be used by the TCPServer instead of SimpleHTTPRequestHandler. For simplicity, let’s define our handler in the same server.py file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# server.py
from socketserver import TCPServer
from http.server import SimpleHTTPRequestHandler


class Handler(SimpleHTTPRequestHandler):
    pass


class Server:
    def __init__(self,
                 server_address=('127.0.0.1', 0),
                 handler=SimpleHTTPRequestHandler):
        self.server_address = server_address
        self.handler = handler

    def serve(self):
        with TCPServer(self.server_address, self.handler) as httpd:
            serving_at = httpd.server_address

            print(f'serving at http://{serving_at[0]}:{serving_at[1]}')
            httpd.serve_forever()


server = Server(handler=Handler)
server.serve()

The Handler class inherits from SimpleHTTPRequestHandler. We keep it empty for now. You’ll see soon how we can customize it. Since we’ve defined a custom handler, the Server class instantiates with the handler attribute equal to our custom Handler class on line 25.

25
26
server = Server(handler=Handler)
server.serve()

 

Serving a custom directory

Now, what if we want to serve on a different directory. Let’s say we have a directory called public containing a few HTML files and images. To serve that directory we must modify the Handler class defined in the previous passage.

6
7
8
class Handler(SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory='public', **kwargs)

Here, we initiate our Handler class by calling the __init__ of the parent class and modifying the directory argument of it. All other non-keyword and keyword arguments remain the same with *args and **kwargs called. It worths mentioning that in Python3.9 the directory argument accepts a path-like object.

 

Basic routing

SimpleHTTPRequestHandler class allows us to manage the requests coming from a client via do_GET(), do_POST() and do_HEAD() methods. Since our Handler class inherits from SimpleHTTPRequestHandler, we can define custom routes by overriding these methods. In this article, we do so only to do_GET() method and define custom routes for GET requests.

 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Handler(SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory='public', **kwargs)

    def do_GET(self):
        if self.path == '/':
            self.path = '/index.html'

        if self.path == '/lyrics':
            self.path = '/lyrics.html'

        return super().do_GET()

On lines 11-15, we say that if the root path (/) is requested then the server should look for the index.html file. And if the requested path is /lyrics, then the server should look for the lyrics.html file.

Notice the return statement at the end:

17
        return super().do_GET()

Since we override the do_GET() method, we must call the default one from the parent class at the end so the handler knows how to process the GET requests.

We can do more with do_GET() and even create automatic routing to look for the corresponding HTML files for each path, etc. But that’s a topic for another article or for you to experiment and find it out yourself. 🙂

 

The code in this article is available in my repo here: github.com/oorkan/a-simple-python-http-server.

I hope you enjoyed it. Cheers! 🍻

 

References

The Python Standard Library (@3.9, lup: March 20, 2021) 

socketserver — A framework for network servers (@3.9, lup: March 20, 2021) 

http.server — HTTP servers (@3.9, lup: March 20, 2021) 

Python *args and **kwargs by Programiz