How To Use Python for Front End Development? - Brython Tutorial


How To Use Python for Front End Development? - Brython Tutorial

Python is a general-purpose programming language that has seen great success in a lot of different areas, but can it be used on the frontend instead of JS?

Brython is a complete Python implementation that runs in the browser. It aims to allow you to completely replace your JavaScript frontend code with Python.

In this tutorial, I’ll show you how to get started with Brython, point out some of its caveats and try to evaluate how realistic it is to use it in a real-world application.

Prerequisites

To be able to follow this tutorial you should have at least a basic understanding of Python programming (variables, classes, methods and lambdas) and some general web programming concepts (the difference between backend and frontend, ajax requests and event handlers).

To run the example project all you’ll need is a browser and a web server (Python’s builtin simple http module will do - so if you have Python installed you’re good).

What We’re Going To Build

If you follow along with this tutorial you’ll have a small, but fully functional frontend app built fully in Python. It’ll send an Ajax request to a backend API and populate a list with the results.

Clicking on any of the list elements will send another request to get the details of the selected item, and display it next to the list.

We’ll use the free and open Rick and Morty REST API as an example backend.

The end result will look something like this:

Python frontend demo with Brython

Set Up the HTML Boilerplate

To begin, let’s create a file called index.html in the project’s root directory with the following contents:

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <div class="container">
            <div id="characters">
                <p>Loading...</p>
            </div>
        </div>
    </body>
</html>

Just an empty html file, with a placeholder Loading... indicator. We will inject the results from the API into the #characters div.

Start a Webserver for Local Development

If we just open the html file via the file:// protocol, Brython won’t be able to import packages, so we’ll need to start a webserver.

We’ll use Pythons built-in http server, like so:

python3 -m http.server 8888

Now, by visiting http://localhost:8888/ in the browser, we’ll have a blank page saying Loading....

Include Brython in Your HTML File

To include Brython all we need is include the following two scripts in the index.html in the <head> element:

<head>

...

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/brython.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/brython_stdlib.js"></script>

...

</head>

For simplicity’s sake, we’ll just load Brython and the Brython Standard Library from CDN.

Initialize Brython and Load Our Script

To include our Python script we’ll add the following tag to the <head> element:

<head>

...

<script type="text/python" src="./main.py"></script>

...

</head>

Now, all we need is initialize Brython when the html document is loaded. To do that, just call brython() on the body tag’s onload event.

...

<body onload="brython()">

...

Now we’re ready to implement our frontend logic in pure Python.

Create an Ajax Request to Fetch Some Data from a Remote Server

Let’s create a new file called main.py. We’ll create a class that will be responsible for fetching data from the API.

from browser import ajax
import json

class CharactersApi:
    @classmethod
    def get_all(cls, callback):
        ajax.get(
            "https://rickandmortyapi.com/api/character/1,2,3,4,5,6,7,8",
            oncomplete=cls._callback_with_result(callback)
            )

    @staticmethod
    def _callback_with_result(callback):
        return lambda req:callback(json.loads(req.text))

The get_all method sends a request to the backend to fetch the first eight items.

The ajax.get method expects a callback function as its second param. We do not want this callback function to deal with parsing the response, that’s why we wrap it with the _callback_with_result method.

The _callback_with_result method returns a lambda function that passes the json decoded results to the callback, that we received as a parameter.

Populate the List with the Response Data

The next step is to generate the html list from the results that we received from the API.

To do that, let’s add this class to our main.py:

from browser import document 
from browser.html import UL, LI

class Characters:
    def __init__(self, list_id, api):
        self._list = document[list_id]
        self._api = api

    def load(self):
        self._api.get_all(self.show_list)

    def show_list(self, items):
        self._list.html = ""
        self._list <= self._generate_list(items)

    def _generate_list(self, items):
        ul = UL()
        for item in items:
            ul <= self._generate_list_element(item)
        return ul

    def _generate_list_element(self, item):
        li=LI(item["name"])
        return li

On initialization the Characters class will find the target html node, and store a reference to it. It’ll also have a reference to the CharactersApi class that we implemented before.

When we trigger load it’ll make the CharactersApi class to fetch the results. The show_list method will be passed as a callback.

This method will receive an already parsed response containing the list of the characters. First it clears the Loading... placeholder message by setting the contents of the #characters target div to an empty string. Then ul node generated by _generate_list will be appended to the empty div. (Brython uses the <= operator for that.)

The _generate_list method creates a new ul element (at this point it is not part of the DOM), and by iterating through all the characters it populates this element with the nodes generated by _generate_list_element.

We can see the magic happen if we instantiate our class and trigger the load method.

characters = Characters(list_id="characters", api=CharactersApi)
characters.load()

Add Some Styling

To make the list look a bit nicer we’ll create a file called main.css:

.container {
    margin: 0 auto;
    width: 400px;
    text-align: center;
}
#characters {
    display: inline-block;
    width: 140px;
    border: 1px solid rgba(0,0,0,.03);
    box-shadow: 0 2px 2px rgba(0,0,0,.24), 0 0 2px rgba(0,0,0,.12);
}
#characters p{
    padding: 10px;
    text-align: center;
}
#characters ul {
    margin: 0;
    padding: 0;
}
#characters li {
    list-style-type: none;
    text-transform: capitalize;
    font-family: Roboto, "Helvetica Neue", sans-serif;
    padding: 10px 20px;
}

And pull that into our index.html:

 <head>

    ...

    <link rel="stylesheet" type="text/css" href="./main.css">

    ...

</head>

Making Our Page Interactive - Adding Event Handlers

To show the details of the selected character we’ll bind an event handler to each list element. This handler will initiate another ajax request and display the results on the page.

We can add the event handler when we generate the li elements. We will store the url for each character on its list node in the data-url attribute. Let’s extend the _generate_list_element method:

class Characters:

    ...

    def _generate_list_element(self, item):
        li=LI(item["name"])
        li.attrs["data-url"] = item["url"]
        li.bind("click", self._load_details)
        return li

This will grab the previously stored url form the DOM and pass it on to the _load_details method on the Characters class. We need to implement that as well:

class Characters:

    ...

    def _load_details(self, evt):
        self._api.get_one(evt.target.attrs["data-url"], self.show_details)

In the load_details method we are calling the yet-to-be-implemented get_one method on the CharactersApi class, which will work similarly to the get_all method. It should fetch the details for a single character:

class CharactersApi:

    ...

    @classmethod
    def get_one(cls, url, callback):
        ajax.get(url, oncomplete=cls._callback_with_result(callback))

As you can see, it expects a callback. The callback will get the parsed result and use that to display the character details:

from browser.html import H2, IMG, LI, UL

...
class Characters:
    def __init__(self, list_id, details_id, api):
        self._list = document[list_id]
        self._details = document[details_id]
        self._api = api

    ...

    def show_details(self, details):
        self._details.html = ""
        self._details <= H2(details["name"])
        self._details <= IMG(src=details["image"])

    ...

characters = Characters(list_id="characters", details_id="details", api=CharactersApi)
characters.load()

We also need to add a target div in the html, where we can display the character details.

...

<body onload="brython()">
    <div class="container">
        <div id="characters">
            <p>Loading...</p>
        </div>
        <div id="details"></div>
    </div>
</body>

...

Add Some CSS to Make Details Box Prettier

As the last step we can add some css for the character details:

#details {
    width: 200px;
    display: inline-block;
    padding: 20px;
}
#details h2 {
    text-align: center;
}

And add a nice little hover effect for the list items:

#characters li:hover {
    background: #ccc;
    cursor: pointer;
}

Wrapping Things Up

Putting it all together, you’ll have something like this:

index.html:

<!doctype html>
<html>
    <head>
        <meta charset="utf-8">
        <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/brython.min.js"></script>
        <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/brython_stdlib.js"></script>
        <script type="text/python" src="./main.py"></script>
        <link rel="stylesheet" type="text/css" href="./main.css">
    </head>
    <body onload="brython()">
        <div class="container">
            <div id="characters">
                <p>Loading...</p>
            </div>
            <div id="details"></div>
        </div>
    </body>
</html>

main.py

from browser import ajax, document
from browser.html import H2, IMG, LI, UL
import json


class CharactersApi:
    @classmethod
    def get_all(cls, callback):
        ajax.get(
            "https://rickandmortyapi.com/api/character/1,2,3,4,5,6,7,8",
            oncomplete=cls._callback_with_result(callback)
        )

    @classmethod
    def get_one(cls, url, callback):
        ajax.get(url, oncomplete=cls._callback_with_result(callback))

    @staticmethod
    def _callback_with_result(callback):
        return lambda req: callback(json.loads(req.text))


class Characters:
    def __init__(self, list_id, details_id, api):
        self._details = document[details_id]
        self._list = document[list_id]
        self._api = api

    def load(self):
        self._api.get_all(self.show_list)

    def show_list(self, items):
        self._list.html = ""
        self._list <= self._generate_list(items)

    def show_details(self, details):
        self._details.html = ""
        self._details <= H2(details["name"])
        self._details <= IMG(src=details["image"])

    def _load_details(self, evt):
        self._api.get_one(evt.target.attrs["data-url"], self.show_details)

    def _generate_list(self, items):
        ul = UL()
        for item in items:
            ul <= self._generate_list_element(item)
        return ul

    def _generate_list_element(self, item):
        li = LI(item["name"])
        li.attrs["data-url"] = item["url"]
        li.bind("click", self._load_details)
        return li


characters = Characters(list_id="characters", details_id="details", api=CharactersApi)
characters.load()

main.css:

.container {
    margin: 0 auto;
    width: 400px;
    text-align: center;
}
#characters {
    display: inline-block;
    width: 140px;
    border: 1px solid rgba(0,0,0,.03);
    box-shadow: 0 2px 2px rgba(0,0,0,.24), 0 0 2px rgba(0,0,0,.12);
}
#characters p{
    padding: 10px;
    text-align: center;
}
#characters ul {
    margin: 0;
    padding: 0;
}
#characters li {
    list-style-type: none;
    text-transform: capitalize;
    font-family: Roboto, "Helvetica Neue", sans-serif;
    padding: 10px 20px;
}
#characters li:hover {
    background: #ccc;
    cursor: pointer;
}
#details {
    width: 200px;
    display: inline-block;
    padding: 20px;
}
#details h2 {
    text-align: center;
}

You can also check the sources on GitHub.

Conclusion

I really appreciate the effort Brython developers put into this project. It was fun to play around with Brython and it was awesome to see Python code running in my browser.

Brython is very easy to set up, I loved the text/python script tags. The DOM implementation also seems OK. It is actively maintained and it seemed stable, I haven’t encountered any bugs or crashes.

Still, I’d strongly advise against using Brython in production, some of my reasons for that:

Performance

I’ve encountered huge performance problems. Decoding JSON strings from ajax response takes ages and is just hogging the CPU, so it made even a trivial toy app unusable as the browser became unresponsive for seconds.

Handling Asynchronous Calls, Callbacks

Pythons lambda syntax is a bit clumsy compared to JavaScript functions. That and the lack of async/await or promises makes handling ajax request a real PITA. I felt like jumping into a time-machine and going back to 2005 to code a Web 2.0 DHTML website.

Documentation

The documentation is just thin and badly organized. Also the official page looks like as if it was designed in the late 1990s - it’s not a dealbreaker, but not very convincing in the case of a frontend library.

Ecosystem

You can import packages, which is great, but there are no Python frameworks for the frontend, so you miss out on the awesome and ever-growing ecosystem that JavaScript has.

Summary

Brython is a cool project, and it nicely demonstrates the flexibility and power of Python, but it is not suitable for real projects and I don’t think it’ll ever be a competitor of JavaScript.