Writing/The Unbearable Lightness of Coding

Chapter 32 · Draft · 36 min read

GUIs: an Overrated Interface, and Why You Should Use Javascript

An Overrated Interface

The holy grail: a system where I just press a button and it does whatever I tell it to — and for most people (my mother, for one), the computer is exactly that: its graphical user interface, or GUI, the windows, icons and menus and pointers we use to interact with the system.

In fact, GUIs are much more than that — they're a way to visualize data and see patterns in it, like a plot; they're a way to make an abstraction more tangible, like a windowing system that lets us drag and drop files into different folders; and they're a way to build a brand, a story, a human connection — an entire dimension of possibilities, really, and a vastly different one at that.

So while on one hand, they're much more intuitive and convenient for humans — on the other, as we saw in the previous chapter, they're much harder to integrate with and to automate; and no matter how pretty they are, they're still meant to be manually operated, so they're a solution rather than a platform for solutions. In a well-designed system, then, the GUI is just another consumer of the same API — much like the CLI or the libraries are. Spotify's web player, desktop app and mobile apps all talk to the same backend, you see.

So really, the point I'd like to make in this chapter is that native desktop GUIs, written with something like Tkinter, are actually quite overrated. The de facto standard for GUIs is the web, anyway, and it seems to me that it's a waste of effort to do anything else; why not just stick to the web, work there — with HTML, CSS and Javascript, using some framework like React (or one of the 200 others)?

To me, this is like learning to drive: I was terrified of it when I was younger, but what got me going was that everyone drives, and they can't all be that gifted, right? If they can do it, then I can, too — and although driving well isn't trivial, not crashing at 30 km/h in a residential neighborhood is pretty doable; so is writing some basic frontends.

The real challenge is to build a UI that people will want to use. Xenophanes, a pre-Socratic philosopher, once said that if horses would worship gods, and create statues of them, those statues would look like horses. We often build systems the same way, assuming that the user is like us, and as programmers, this is especially bad, because most people don't see the world through the same computational prism as we do.

So we'll talk about what a GUI is under the hood — widgets and layout, an event loop calling callbacks, our async chapter's reactor in another costume; and we'll see how it's done natively in Python, with tkinter — and why it's such a cumbersome road. Then, we'll discuss the web as the universal GUI: from the DOM and its events to fetching data from a Python backend, and React's idea of the UI as a function of state. We'll also see the whole argument in a nutshell, in one of the coolest tools I've ever seen: Jupyter. And finally, we'll circle back and see how all our interfaces fit together, and the recurring shapes that lead us into the next chapter, about design patterns.

What a GUI Is

So, how exactly does a graphical user interface work? The short answer is, with a lot of widgets. If you're familiar with the concept of a tree data structure, then you can imagine a window as the root, which contains a frame, which contains a button. The way these widgets are laid out — where each element goes and how big it is — is called their layout. Then, we have their behavior: how they react to a click, a key press, a mouse move or a resize. In other words, we map a callback to an event — so when a button is clicked, something happens. Unlike a CLI, which is a straight line from top to bottom that we run and get a result, a GUI program sets everything up, and then hands the reins to an event loop. This loop waits for events, like a click or a key press, from the windowing system, and dispatches them to the callbacks they're mapped to, forever (or at least, until the window is closed).

If this reminds you of the reactor we mentioned in our chapter on asynchronous programming, it's because it's exactly that: we register our interest, we wait, and we react. It's called inversion of control: don't call us, we'll call you. Let's illustrate this with a silly example:

>>> import collections
>>> class EventLoop:
...     def __init__(self):
...         self.handlers = collections.defaultdict(list)
...         self.events = collections.deque()
...     def bind(self, event, handler):
...         self.handlers[event].append(handler)
...     def post(self, event, *args):
...         self.events.append((event, args))
...     def run(self):
...         while self.events:
...             event, args = self.events.popleft()
...             for handler in self.handlers[event]:
...                 handler(*args)
>>> loop = EventLoop()
>>> loop.bind('click', lambda x, y: print(f'clicked at ({x}, {y})'))
>>> loop.bind('key', lambda key: print(f'pressed {key!r}'))
>>> loop.post('click', 10, 20)
>>> loop.post('scroll', 3)
>>> loop.post('key', 'q')
>>> loop.run()
clicked at (10, 20)
pressed 'q'

Handlers are registered for specific event names; posting an event simply appends it to the queue; and running the event loop pops events off the queue, in order, and calls all the handlers that were registered for this type of event. If nobody has expressed an interest in some event — like, our scroll event here — it is simply discarded; and note that in a real event loop, the moment the queue becomes empty, it doesn't just stop — it actually blocks, until the next event is delivered by the operating system, which is why mainloop() doesn't return until the window is closed.

Now, the reason I'm telling you all this is to illustrate a point: everything happens in callbacks, and on the same thread as the event loop. While a callback is being executed, nothing else is happening — the window is not repainted, and doesn't react to other clicks. If a callback takes ten seconds, it will effectively freeze the entire window for ten seconds — and the operating system might even think it's unresponsive and display this annoying banner about it. Incidentally, timers are also events: in tkinter, we have after(ms, callback), and in Javascript we have setTimeout(callback, ms) to schedule a callback to happen on the same loop, which is how a GUI animates, or otherwise polls for something, without blocking.

So you see, a graphical user interface has no choice — it has to be multithreaded, so that one thread can handle the user, while another one does some background work. If you're using an IDE with a Cancel button, it has to respond even while it's compiling — and if you're downloading a large file, it probably has a cancel button that has to work while the download is happening. It's the same rule we saw with coroutines: anything that might take a while must be delegated. The difference is that here, it's usually delegated to a thread — and its result must somehow find its way back to the event loop's thread, since most GUI toolkits aren't safe to touch from other threads. As an aside, in Javascript nested callbacks are pretty tolerable, thanks to anonymous functions (or, more precisely, arrow functions), which is why the whole event-driven paradigm feels so at home there; we'll see it soon enough.

A Native GUI with tkinter

If we want to create a native GUI — one that runs as a proper application on the system — we have to use a different approach. Luckily, Python's standard library comes equipped with just what we need: tkinter, a binding to the Tk widget toolkit. This is a free, cross-platform library of graphical components; we describe to it what we want — "draw a button here" — and it magically draws a button, on either Windows, macOS or Linux. It was actually developed for the Tcl programming language ("tickle"), a dynamic and highly flexible language created by John Ousterhout (yes, the same one who teaches the software design studio at Stanford). In fact, IDLE — the graphical editor that comes with Python — is written with tkinter.

To draw a window with a label and a button, which, upon being clicked, changes the label:

>>> import tkinter as tk
>>> window = tk.Tk()
>>> window.title('Example')
''
>>> label = tk.Label(window, text='Hello, world!', font=('Helvetica', 20), fg='#629ebb')
>>> label.grid(column=0, row=0)
>>> button = tk.Button(window, text='Click Me')
>>> button.grid(column=0, row=1)
>>> def on_click():
...     label.configure(text='Clicked!')
>>> button.configure(command=on_click)
>>> window.mainloop()

Let's go over it, top to bottom: we create the main window with Tk(), set its caption with title() — which actually returns an empty string (Tcl's way of saying nothing), but nevermind — and then create the rest of the widgets, specifying their parent as the first argument (so tkinter knows to nest them in the widget tree). The font (a family and size) and fg (the foreground color) options are pretty self-explanatory, and grid tells the window to arrange the widgets in an imaginary table, placing them in row 0 and column 0 (the label), and row 1 and column 0 (the button). configure is used to change some options after a widget has been created, in particular the command option, which specifies what happens when the widget is clicked. Finally, we call the window's mainloop method, which launches the event loop, blocking the current thread until the window is closed.

At this point, we should have a small window titled Example, with "Hello, world!" in blue, 20-point Helvetica, and a Click Me button beneath it. If we click the button, the label changes to "Clicked!", just like we wanted.

And that's just the tip of the iceberg. We have several other layout managers to choose from: pack arranges the widgets in a box, stacking them one after the other (top to bottom, by default); and place lets us specify their exact coordinates. If we want more than just a click, we can do widget.bind('<Return>', handler), which attaches our handler to any event we like — and even passes an event object to it, so we can extract more information if we want.

In fact, that's how I checked this, without having to look at the screen: I created the window in hidden mode, asked the event loop to quit after half a second, and simulated the button click:

>>> window.winfo_children()
[<tkinter.Label object .!label>, <tkinter.Button object .!button>]
>>> label.cget('text')
'Hello, world!'
>>> window.update()
>>> window.winfo_width(), button.winfo_x()
(117, 14)
>>> button.invoke()
'None'
>>> label.cget('text')
'Clicked!'
>>> window.update()
>>> window.winfo_width(), button.winfo_x()
(88, 0)

That is, we get a list of the window's widgets (.!label and .!button are their path names in Tk's nomenclature), then we call the button's command (.invoke) as if it was clicked, and get the result as a Tcl string, 'None' (in Tcl, everything is a string — kind of like in bash). As you can see, the geometry is not the same: the label with the shorter text is actually smaller, so the window shrank from 117 pixels to 88, and the button moved accordingly, from x=14 to x=0. The moral of the story is that even changing a text moves a button; making a GUI that's consistent, pretty and convenient is no mean feat. Still, it's doable — although I have to admit, the exact pixels will vary depending on the platform, and its fonts.

As for the looks, I'm afraid Tk widgets look a bit dated — for a more modern look, the tkinter.ttk module provides themed widgets that blend better with each platform.

Another problem that might arise when working with a GUI is threading. Consider this: if we add a Download button that, when clicked, performs some slow download — for example, by calling time.sleep(3) — the entire window will freeze for three whole seconds, which is rather unpleasant. So how do we avoid it here?

# download.py
import threading
import time
import tkinter as tk


def download():
    time.sleep(3)  # pretend this is a slow download
    return 'done!'


def on_click():
    button.configure(state='disabled')
    status.configure(text='downloading...')
    threading.Thread(target=work, daemon=True).start()


def work():  # runs in a background thread
    result = download()
    window.after(0, finish, result)  # have the event loop call finish


def finish(result):  # runs in the GUI thread
    status.configure(text=result)
    button.configure(state='normal')


window = tk.Tk()
status = tk.Label(window, text='idle')
status.pack()
button = tk.Button(window, text='Download', command=on_click)
button.pack()

if __name__ == '__main__':
    window.mainloop()

In on_click, we first disable the button and show that we're "downloading...", then start a daemon thread to do the slow work in the background, and return immediately — so the event loop can keep ticking away. work then does whatever it needs to do slowly, and once it's done, it tells the event loop, "hey, when you get a moment, call finish, and pass it the result" — so finish, which actually touches the widgets, is executed in the GUI thread. If you're paying close attention, you might have noticed that this isn't exactly what I said before: when tkinter is called from a different thread, it actually forwards the call to the thread that's running the Tcl interpreter (in the standard, threaded builds) — still, the clean rule is that only the GUI thread should touch the widgets.

And indeed, if we simulate a click and sample the label every half a second, we'll get "downloading..." with the button disabled for three seconds, then "done!" with the button enabled again, while the loop keeps ticking away merrily.

But notice what we wrote, and you might be a bit annoyed. What is it exactly, with all this imperative code, trying to describe an essentially static situation — "a label, and a button under it"? It feels needlessly verbose, and generally, pretty old-fashioned — so let's see how we can do better.

Why Native GUIs Are Overrated

This is doable, but cumbersome: the solution works at the engineering level, but it's not a good idea at the design level, because it's so hard to build on top of it. The main reason, I think, is that GUIs are inherently declarative, while programming languages are inherently imperative; or, if you will, that the former are about describing, while the latter are about instructing.

When you look at something and describe it, you can say: "this is big", "this is on the left", "this is red", "this is under the title". But when you're writing a program, you don't describe how your result should look — you lay out a series of steps that, once executed, should achieve the desired effect: create this, then put it there, then loop over this, then accumulate that. Describing a static picture as a series of commands is not an obvious way of thinking; and the bigger and more elaborate the UI, the harder it gets.

Another reason is threading: you always need it for a GUI, and it's always fun (races, anyone? Or that only the GUI thread is allowed to touch the widgets?). Yet another reason is platforms and looks: if your app is native, it should look at home on Windows, macOS and Linux, which have different conventions; and if you want to support different themes (the textbook example of the abstract factory pattern: a dark theme produces dark windows and dark buttons, so that you don't have to do if theme == ... everywhere), you get a glimpse of just how much machinery even the mere appearance requires.

And lastly, there's distribution: if you want users to actually use your app, you have to figure out how to install it: how to bundle Python and all its dependencies in an executable for each platform (with various tools, like PyInstaller), how to ship updates, how to sign everything so it's trusted... If you have a website, all you have to do is provide a URL, and it's always up-to-date.

The exception is mobile, of course: native apps can look and feel significantly better than websites there. But on desktop computers, the difference is much less pronounced; Spotify's desktop app is definitely nicer than its web player, but it's not that much nicer — and if I were Spotify, but not a company with infinite money, I probably wouldn't develop it first.

Anyway, just like we did with shell completion, let's do a Whorfian refactoring again. In that case, because of our limited bash skills, the weak link in the chain was the bash language — so we moved the problem into Python, which we know much better, and solved it there. This time, the weak link in the chain is Python itself — so maybe there's another language in which the problem is easier to solve, and we can figure out a way to talk to it from Python.

As an aside, this reminds me of a story from machine learning: the so called kernel trick. Simple, efficient linear algorithms like SVM, or Support Vector Machines, look for a line (or hyperplane, in n dimensions) to separate the data — for example, classify red points from blue points. But what if the red points are arranged in a disk, surrounded by a ring of blue ones? No straight line can separate them in two dimensions — but if you add a third one, say z = x² + y² (the squared distance from the center), and then hoist the points up into 3D space accordingly, the red points end up lower than the blue ones, and a flat plane can easily separate them. In other words, you have to find the right dimension to lift your problem into — but once you do, the simple algorithm can solve it just as easily.

Similarly, we can try to lift our problem into another dimension — another platform, if you will, on which it is easier to solve. And if you think about it, there's a platform that's really great for flexible, optimized, cross-platform graphical user interfaces: your browser. I mean, just think how sophisticated the content that you consume on the web is — videos, animations, parallax effects, entire games, really — and it's all language-agnostic, so the backend can be in any language (including Python), which maps nicely to our usual inclination to decouple the API from its clients. And so many people do it, that it can't be that hard.

And in fact, HTML, CSS and Javascript are precisely that: a solution that breaks the problem down by concerns. HTML is used to define the content and its structure, CSS to define its style — fonts, colors, layout, and so on — while Javascript is used for the behavior: what happens when you click here, or hover there. And the nice part is that it's all declarative (except the Javascript part, which is imperative, but only used when necessary); if everything is static, you don't even need any Javascript at all.

HTML, CSS and Javascript

In the Web Development chapter, we talked about what makes the web tick: HTML for the content and the structure, CSS for the presentation, as a series of rules that select and declare what goes where, and Javascript for the behavior, coordinating with the document object model, the tree of objects the browser creates from the HTML and re-renders whenever it changes. Let's consider the same technologies again, but with our new, GUI-tinted glasses on.

HTML elements are the widgets, and their nesting structure is the widget tree; CSS is the layout and the looks, and Javascript's event listeners are the callbacks; and the event loop is the browser's — well, we never even see it; there's no mainloop() to call, is there?

If we put a script tag at the end of our body, it will be run when the page is loaded, at which point its elements will already exist in the DOM, so we can find them:

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Example</title>
    <style>
      #label { font: 20px Helvetica, sans-serif; color: #629ebb; }
    </style>
  </head>
  <body>
    <p id="label">Hello, world!</p>
    <button id="button">Click Me</button>
    <script>
      const label = document.getElementById('label');
      const button = document.getElementById('button');
      button.addEventListener('click', () => {
        label.textContent = 'Clicked!';
      });
    </script>
  </body>
</html>

Let's walk through it: first, we use a CSS rule to style the element whose id is label; then we use document.getElementById to find this element by its id; and finally, we use addEventListener('click', ...) to register a callback, in this case an anonymous (arrow) function, with the browser's event loop, which will call it when a click event is triggered. And all this callback does is set the element's textContent, which the browser automatically re-renders, so it's the same app as before, just a label, a button and a callback.

Compare it to tkinter: here, the structure and the looks are declared, in HTML and CSS, while the only imperative code is the behavior. Also, the button doesn't jump all over the place: the paragraph is a block element, which naturally takes up its own line, no matter how wide its text is.

Now, in this example all three languages are crammed into the same file, for brevity's sake; usually, you'd put the CSS in a separate .css file, linked from the head, and the scripts in .js files, like we will do in a moment. And in case you're wondering, I tested it by loading it in jsdom, a DOM implementation for Node.js, clicking on the button and reading the paragraph, since I couldn't open a browser here.

As for the browser's event loop, it's single-threaded, just like tkinter's, so a long-running callback would freeze the page as well. The difference is that Javascript's slow operations, like network requests or timers, are asynchronous by design: they either accept a callback, or return a promise, so there's nothing to wait for in the first place. Anyway, let's add another script, just to showcase a few more things at once:

// counter.js
let clicks = 0;
button.addEventListener('click', (event) => {
  clicks += 1;
  label.textContent = `Clicked ${clicks} times (at ${event.clientX}, ${event.clientY})`;
});
document.addEventListener('keydown', (event) => {
  if (event.key === 'Escape') {
    clicks = 0;
    label.textContent = 'Hello, world!';
  }
});

Here, we see how listeners are added cumulatively, so on every click both the first script's listener and this one are called, in that order; how the event object contains the details, like clientX and clientY for the mouse position or key for the keyboard; how the click count is stored in a variable that the anonymous function closes over (we've seen closures in the Functions chapters, haven't we?); and how we can add a keydown listener on document, which means the entire page, to catch any keys. In fact, handling Escape and Enter is the classic example of GUI event handling; and just like the Web Development chapter showed, every browser has developer tools (usually F12), which have a console in which we can run Javascript on the live page — a GUI REPL, if you will. Go ahead and try it: document.getElementById('label').textContent = 'hi' and watch the window change, something you'd never get in a tkinter app from the outside.

Fetching from Python

The chapter about Web Development ended with a full-fledged web app: a page that fetches JSON from an API and builds itself out of it in the browser — with the frontend being just another client of the backend. Let's do something of this sort ourselves, this time with a Python backend — and take it one step further, to a page that can also write things, not just read them.

Remember the alternative, baking the data into HTML (like MVC frameworks such as Django do): it might be quicker to start, and often good enough, but then the data is not available as an API to CLIs and other programs. Consider then server.py:

# server.py
import flask

app = flask.Flask(__name__, static_folder='static', static_url_path='')

posts = [
    {'author': 'alice', 'content': 'Hello, world!'},
    {'author': 'bob', 'content': 'GUIs are overrated.'},
]


@app.get('/')
def index():
    return app.send_static_file('index.html')


@app.get('/api/posts')
def get_posts():
    return posts


@app.post('/api/posts')
def add_post():
    post = flask.request.get_json()
    posts.append(post)
    return post, 201

Let's go over it: the static_folder argument and static_url_path='' combination means that the files in static/ are served from the root, so /posts.js works; the index function sends index.html; and GET /api/posts returns a list of posts (Flask automatically converts a list or a dict into a JSON response). POST /api/posts, in turn, appends the JSON body to this list and returns it with 201 Created. We can exercise the API with curl:

$ curl -s localhost:8000/api/posts
[{"author":"alice","content":"Hello, world!"},{"author":"bob","content":"GUIs are overrated."}]
$ curl -s -X POST localhost:8000/api/posts -H 'Content-Type: application/json' -d '{"author": "carol", "content": "Me too!"}'
{"author":"carol","content":"Me too!"}
$ curl -s localhost:8000/api/posts | python -m json.tool
[
    {
        "author": "alice",
        "content": "Hello, world!"
    },
    {
        "author": "bob",
        "content": "GUIs are overrated."
    },
    {
        "author": "carol",
        "content": "Me too!"
    }
]
$ curl -s localhost:8000/ | head -4
<!-- static/index.html -->
<!DOCTYPE html>
<html>
  <head>

As an aside, these posts live in a Python list, so they disappear when the server restarts — in a real scenario, we'd keep them in a database, as we did in the web chapters. But notice how this API is usable before any GUI is even conceived; we can curl it, or use it in a CLI or test — the GUI is just one more client of the API. (And Flask's development server is just that: in production, we'd run the app behind a proper WSGI server, as we did in the web programming chapter.)

So now we can write the GUI: static/index.html has an empty table, a form, and a script tag:

<!-- static/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Posts</title>
  </head>
  <body>
    <table id="posts"></table>
    <form id="form">
      <input name="content" placeholder="What's on your mind?">
      <button>Post</button>
    </form>
    <script src="posts.js"></script>
  </body>
</html>

And static/posts.js can fetch the posts and put them in table rows, as well as post the form's contents:

// static/posts.js
async function loadPosts() {
  const response = await fetch('/api/posts');
  const posts = await response.json();
  const table = document.getElementById('posts');
  table.replaceChildren();
  for (const post of posts) {
    const row = document.createElement('tr');
    for (const value of [post.author, post.content]) {
      const cell = document.createElement('td');
      cell.textContent = value;
      row.append(cell);
    }
    table.append(row);
  }
}

document.getElementById('form').addEventListener('submit', async (event) => {
  event.preventDefault();
  const content = event.target.elements.content.value;
  await fetch('/api/posts', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({author: 'me', content}),
  });
  event.target.reset();
  await loadPosts();
});

loadPosts();

Let's go over loadPosts: fetch returns a promise, and await (from async/await, just like in Python, remember our asynchronous programming chapter?) is used to wait for its response, and then for its JSON. replaceChildren empties the table; and then for each post, it creates a tr and some tds with their textContent (we never, ever concatenate strings into innerHTML — that would let a post with a <script> tag inject code into our page). This is AJAX, yeah: the page loads once, empty, and then fetches its data asynchronously.

The submit handler prevents the default behavior (that would submit the form by reloading the page), reads the input (through the form's elements), and POSTs the content as JSON with the right Content-Type; it then resets the form and reloads the table. Since the page and the API come from the same origin (meaning host and port), the browser lets the page fetch the API without any additional configuration — if we'd serve the frontend from elsewhere, we'd have to configure the API to allow this explicitly (that's what CORS is for). (To be sure, I did run the Flask server and load the page in jsdom, which lends it Node's fetch — and it did render the table with the posts; and after I submitted the form, it did show the new post as well.)

So, the irony: we moved to the web because building widgets imperatively in tkinter was painful, and here we are, building a table imperatively again, createElement after createElement. The HTML we declared is almost insignificant, and all the work is done in Javascript — and when the line between the two is so blurry, it takes a lot of self-discipline to keep things organized; that's why a lot of people prefer working in frameworks. Historically, this is how it was done with jQuery ($.get, $.getJSON, $('<td>')), which papered over a lot of the differences between browsers; nowadays, we have fetch and a decent enough DOM API, so we can get by with plain Javascript.

Still, you might ask: why did we move the weights? And it's true that in the olden days, dynamic websites did all the heavy lifting on the server, but these days phones have more compute than connectivity (whenever I'm unhappy with my phone, it's because of the reception, not the speed). So now, the browser does the rendering, and the server just sends the raw data.

React: the UI as a Function of State

This is the problem we set out to solve: graphical user interfaces are great, but it's really nice to describe them in a declarative fashion — and since code is fundamentally imperative, something has to give. Unless the system is very simple, the code we write to orchestrate it quickly balloons and explodes. Modern websites, as we've seen, tend to maintain a relatively complex state, and we'd like to be able to take some data and place it on the screen, without having to manually construct widgets for every item, which is what we'd have to do in tkinter; so let's see if there's a better way.

In fact, there are two: one, popularized by Angular from Google, went all-in on the declarative part. It extended HTML with attributes like ng-repeat, which repeated an element for each item in a list. The other, popularized by React from Facebook, went the opposite way: let's write code, it said — after all, this is what we're good at — and find a way to make describing the user interface pleasant within it. Personally, I prefer React, for its progressive disclosure: you can do something small with it, but also something big, whereas Angular's learning curve is much steeper (and another framework, Vue, came later, as a third option).

Anyway, the stroke of genius behind React was JSX: an extension of Javascript's syntax, whereby a tag is treated as an expression — <p>Hello, {name}!</p> is not a string, but code, that results in a paragraph with some text in it. This means we can embed any Javascript expression in it, and this is how we write loops and conditions — they stay in plain, old-fashioned Javascript.

// menu.jsx
function Menu({ links }) {
  return (
    <ul>
      {links.map((link) => (
        <li key={link.url}>
          <a href={link.url}>{link.text}</a>
        </li>
      ))}
    </ul>
  );
}

function App() {
  const links = [
    { url: '/', text: 'Home' },
    { url: '/posts', text: 'Posts' },
  ];
  return (
    <div>
      <h1>Hello, world!</h1>
      <Menu links={links} />
    </div>
  );
}

Let's break this down: components are functions that return JSX, but we need to capitalize their name, so JSX can tell them apart from regular tags. The attributes that we pass to a component are received as a single object, known as props — here, we destructure it as { links }. We then use map to transform the list of links into a list of li elements, which JSX will interpolate between the <ul> and </ul>; the key attribute is used by React to distinguish between the different items, in case it needs to re-render them. The App component then uses Menu as if it was a regular tag, and we mount the entire tree onto a real DOM element, which we select with document.getElementById('root') and pass to createRoot().render(<App />). Any content that appears between a component's tags is also passed to it, as props.children — this is how wrappers like a modal message can work. Of course, browsers don't actually understand JSX: it has to be transpiled (that is, compiled from one language to another) into plain Javascript:

$ esbuild menu.jsx --jsx-side-effects | head -n 3
function Menu({ links }) {
  return React.createElement("ul", null, links.map((link) => React.createElement("li", { key: link.url }, React.createElement("a", { href: link.url }, link.text))));
}

As you can see, every tag became a function call: the first argument is the tag (or component) we want to use, the second is the props we want to pass to it, and the third is the children we want to put inside it — so in fact, we're literally describing the user interface by calling functions (although newer setups compile it to jsx() calls, from react/jsx-runtime, but that's beside the point).

// state.jsx
import { useState, useEffect } from 'react';

function ColorButton({ colors }) {
  const [index, setIndex] = useState(0);
  return (
    <button
      style={{ backgroundColor: colors[index] }}
      onClick={() => setIndex((index + 1) % colors.length)}
    >
      Click Me
    </button>
  );
}

Let's break this one down as well: we use useState to store the color's index, and it returns two items — the current value, and a function that we can use to set it. Then, when the button is clicked, we call setIndex — notice that we never assign to index = ..., because this is something React can't track. This function, then, effectively schedules a re-render: React calls our function again, with a new value, and compares the new tree with the old one (known as the virtual DOM) to see what changed, so it can update just the necessary bits in the real DOM. In other words, the user interface is a function of the state — so we never change the label, we simply change the state, and describe the user interface again.

// state.jsx (continued)
function Posts() {
  const [posts, setPosts] = useState(null);
  useEffect(() => {
    fetch('/api/posts')
      .then((response) => response.json())
      .then(setPosts);
  }, []);
  if (posts === null) {
    return <div>Loading...</div>;
  }
  return (
    <table>
      <tbody>
        {posts.map((post, i) => (
          <tr key={i}>
            <td>{post.author}</td>
            <td>{post.content}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

And again: this time, we start with a null state, which is why the first render results in a "Loading..." message. Then, we use useEffect to run some code after the component is mounted (the empty array means we only want to run it once, after the first mount). We fetch the posts and set them, which re-renders the table; and this time, there's no need to build rows by hand, or to decide when to re-render. I actually checked, by bundling it with esbuild and rendering it with jsdom and React 18: the menu worked, the button cycled through red, green and blue and back to red, and Posts showed "Loading..." until the fetch resolved, at which point it displayed the table.

Note that this wasn't always the case: when I first taught this lesson, back in 2020, we used class components: class Button extends React.Component { ... }, this.state, this.setState, componentDidMount and the infamous .bind(this). Since then, however, these were replaced by hooks like useState and useEffect in most React codebases.

So, because of the transpilation we mentioned before, there's a build step involved — luckily, there are tools like Vite (or previously, Babel and Webpack, which were wrapped in create-react-app) that can take care of bundling everything into static HTML, CSS and Javascript files, which we can then host anywhere: on a web server, or even on a CDN. That's what the phone has to download just once — from now on, all it needs to fetch from the API is the raw data.

So how do you choose one framework over another, with the hundreds of options out there? To be fair, nobody knows which frameworks will survive, and it's a bit of a gamble — but if I can give you one piece of advice, it would be to trust the people who are concerned with progressive disclosure: when something is really hard to get started with, I raise an eyebrow, and look for alternatives.

Jupyter: a GUI for Python, Written in Javascript

If you think about it, code is quite an interactive medium: we can change it, run it and see what happens. The only problem is that it's quite difficult to visualize, and even harder to share: an interpreter session can be copied and pasted into an email, but it's hardly beautiful.

Enter Jupyter. Originally, it was called IPython Notebook, because it was just that: a notebook for Python, in which you could write code, see the results and add annotations, all in your browser. As it grew, though, support for other languages was added — so it was renamed to Jupyter, after the languages it supported: Julia, Python and R.

But what exactly is it? Jupyter is actually a web server, which you can run on your own machine. It provides you with HTML pages, but also with something called kernels: processes that receive code, execute it and send back the results, pretty much like the Python interpreter you get when you run the python command.

The notebook itself is a web page, which looks like an interpreter. When you execute some cell, it gets sent back to the kernel, which executes it and sends back the result, which is then displayed on the page. In other words, it's a Python interpreter, but in a web browser — and all the advantages that this medium entails for visualization, annotation and sharing.

But because it's a web page, we can actually get results that are richer than just text: %matplotlib inline, for example, lets us embed plots as images on the page, rather than having them pop out in a separate window, and any object can define its own _repr_html_ method, which will be used to determine how it is rendered. For example:

>>> import html
>>> class Table:
...     def __init__(self, rows):
...         self.rows = rows
...     def __repr__(self):
...         return f'Table({self.rows!r})'
...     def _repr_html_(self):
...         cells = ''.join(
...             f'<tr><td>{html.escape(str(a))}</td><td>{html.escape(str(b))}</td></tr>'
...             for a, b in self.rows
...         )
...         return f'<table>{cells}</table>'
>>> table = Table([('alice', 'Hello, world!'), ('bob', 'GUIs are <overrated>')])
>>> table
Table([('alice', 'Hello, world!'), ('bob', 'GUIs are <overrated>')])
>>> table._repr_html_()
'<table><tr><td>alice</td><td>Hello, world!</td></tr><tr><td>bob</td><td>GUIs are &lt;overrated&gt;</td></tr></table>'

In a regular Python interpreter, we'd just see the repr; but in a notebook, the same object will actually render as a proper table. The pandas library uses this exact method to render DataFrames, and if you notice the html.escape, you might appreciate how even the <overrated> part of the data is rendered as &lt;overrated&gt;, so as not to inject any actual markup into the page.

And that's not even the best part. If you do research, for example, and you have a bunch of data to play with, you can do it in a notebook; and then, you can just keep the bits that you want, add some markdown cells with explanations, links and images, and send it over to someone with a "take a look at my work". But rather than sending over the results of your thinking process, you're sending over your thinking process itself, if that makes sense.

Or, if you write books or teach, you can imagine how cool it would be to have a textbook on probability, in which you can change the mean and the standard deviation of the normal distribution and see the resulting plot.

So yeah, that's the Whorfian refactoring in full: we started with a completion system, and noted how annoying it is to use bash. So we said, "let's solve this problem in Python"; and here, we've noted how annoying it is to use GUIs in Python, especially when they're done in something as unpleasant as tkinter. But then, we noted that the web is actually a really cool place to visualize, edit and present things — so we've pushed the problem there, and solved it in that world, with HTML and CSS and Javascript. And all that was left of our "problem", the part that was actually relevant to Python, was solving the kernel — literally, the kernel: the part that receives code, executes it and sends back the result. So that's the only part that we left in Python; and then, we just let people use the kernel, that relevant part, to do whatever they wanted to do, and in a much more beautiful way.

One final caveat: because the kernel runs whatever code it receives, if you let users you don't trust send stuff to it, they can easily hack your system — like, delete your file system — just by sending the appropriate code.

Interfaces, and the Shapes They Share

To sum up this long chapter, then: a graphical user interface is an event loop that calls our callbacks. If we're using a native toolkit, we have to describe a picture to it in an imperative way, while juggling multiple threads; but the web has reduced it to the much more comfortable notion of documents with some scripts, and React has achieved a near-perfect balance between the two, treating the entire UI as a function of state. The best GUI for your Python system might not even be in Python: a Python API with a Javascript frontend might be a much more natural interface, with each language doing what it's good at. This also means that the three types of interfaces — for programs, for scripts, and for people — can be thought of as one family: an API for programs, a CLI for scripts (or people who don't fear the terminal), and a GUI for everyone — all consuming the same API, and all shrinking the gulf of execution for a different audience.

This move, of a Whorfian refactor, was actually a recurring theme: whenever we faced an issue that was hard to articulate, we moved it to a place where it's more natural to express. We moved completion from bash to Python, and the entire graphical user interface from Python to the browser; Jupyter, in fact, left us with nothing but the kernel in Python, and solved everything else in the much more intuitive mediums of HTML, CSS and Javascript. But what no framework will solve for us is the human on the other side; Xenophanes's horses would have built a GUI that looks like a horse, and the graphical user interface is the interface that's most exposed to people who don't think like programmers. It's still our responsibility to make an abstraction tangible for them, not just provide them with the widgets to do so.

Looking back on the last few chapters, though, there was another recurring theme: all those different shapes that kept popping up under different names. Callbacks, for example — we pass a function to a framework, and it calls us when the time comes: it's tkinter's command, or the DOM's event listeners, or the reactor from our asynchronous chapter, or Click invoking our command functions. It's actually a more general idea called inversion of control, also known as the Hollywood principle: don't call us, we'll call you. Then there were observers — when something happens, some other things get notified; an event listener subscribes to clicks, and React re-renders a component when its state changes, so that the view automatically follows the data, without us having to update it manually. There were composites: a part and a whole that look the same from the outside — a tkinter window contains frames that contain buttons, the DOM is a tree of elements, React components nest through children and a Click group contains commands and other groups. To top it off, there was the abstract factory behind the graphical user interface's themes, and hooks like Jupyter's _repr_html_, where the framework defines the steps, but calls our method for one of them — a cousin of what's called the template method in the patterns literature.

This is no coincidence, either: a lot of the patterns literature grew out of the context of graphical user interfaces. In fact, Smalltalk-80's model-view-controller is the example used by the Gang of Four to introduce the observer, the composite and the strategy patterns in their seminal 1994 book, Design Patterns: Elements of Reusable Object-Oriented Software, and the book's big case study is a WYSIWYG document editor.

Which brings us to the next chapter. We've been using these patterns all along, without even mentioning it, so it's high time we look at the patterns literature directly. Where did it come from, what can we learn from it, and which of those patterns earn their keep in Python, and which ones dissolve into language features we already know and love?

We've built interfaces for programs, for scripts, and for people; next, let's talk about the shapes that keep showing up between them.