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:
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.