Back to Index

FastHTML first look - Answer.AI dev chat #4


Transcript

Hello again, another Dev Chat with Jono and Alexis. And a lot of stuff to share today. Actually, maybe a good place to start would be some updates from yesterday, which I haven't shown anybody yet. I kind of felt like it was going slowly, but then when I was done, I was like, oh, I actually got quite a lot done.

So that's often the way, isn't it? You just kind of keep slowly battling away. So in Fastlight-- tell me if you guys-- I don't know if this is obvious and easy or not, but basically, I added this .dataclass thing. And so what that does is it creates a type.

That type is a data class. You can make stuff with it. So the reason why is, for a few reasons. One is to type completion, at least in Jupyter, it's nice to be able to see what I'm meant to be typing in. And it's nice for Python to know about what types are expected and stuff like that, particularly, as we'll see later, for it's necessary for the web application framework stuff I was building, as you'll see.

So basically, given that this is something that you pass keyword arguments into, and the keyword arguments are the same as the field names. And you'll see all of them are defined as the database type or none. Things like SQL model, Sebastian's thing, are much more careful about actually knowing if it's nullable or not, and if it's not nullable, it doesn't say so, blah, blah, blah.

I very intentionally don't do that, because this int, actually, you don't have to pass it when you create it. It's created by the database for you. And so in SQL model, you have to have different types for the update version and the create version. And in this case, just like, you know what?

I'm not going to be strict about this. So anyway, that way, I've got a dictionary here for AC/DC albums. There are a lot more. I don't know why they only have two in this database. It's a bit of a disappointment, but at least they're good ones. So you can pass those in for a particular AC/DC album as keyword arguments.

And you get back an object. OK, so that's basically how you can convert the result of a call for your model, for querying something in the model. Now you've got back an object. AUDIENCE: Could I interrupt with a basic question here? MARTIN SPLITT: Any time. AUDIENCE: So when you define the variable album_dc, is that album_dc completely equivalent to what I would get if I declared a data class in the usual way by saying @data_class and then said class data-- MARTIN SPLITT: I've even written a function called data_class source that takes the data class and returns the source code to recreate the same data class.

AUDIENCE: OK, so it is. So in that case-- MARTIN SPLITT: There's no magic here. There's actually-- the data classes thing in the standard library has something called make_data_class in it. AUDIENCE: OK. In that case, would you say it's more idiomatic if album_dc was capitalized because it's a class definition in Constructor, or not really?

MARTIN SPLITT: Yeah, maybe. I'm not really going to use it for much, as you'll see. So don't worry too much about that. AUDIENCE: OK, just wanted to double-check my understanding. MARTIN SPLITT: Yeah, yeah, it's good. So I couldn't find something that spits out the source code for a data class.

So I just wrote a little one. It doesn't do everything, but it does enough to cover what this particular one does, at least. So this would actually recreate that. So the reason that's potentially interesting is then that I added this create_module thing, which literally just opens a file, prints from data classes, import data class, and then prints the source code to it.

And so that means that you could now, as you see here, create a module. And so create_mod is actually going to do every single table. And if you want to, also every single view. It's going to print out the data class source for all of them. So here, if I go create_mod_db, then this will create a file that contains all of them.

Now, in Jupyter, that's entirely useless. But if you use VS Code, it's very useful, because you can now do that, or import star, or whatever. And I've now got-- even in VS Code, you now have autocomplete. So this is like the other way around to something like SQL model, where you define the data classes, and then it builds the database from that.

This is like, oh, you have the database. We'll build the data classes from that. Yeah, exactly, exactly. And I don't love CodeGen. But in the end, if you want VS Code support, you have to do one of them. And the reason I prefer this one, for me, is that this way I can create my database schema using a GUI, or using SQLite utils, or using SQL statements, or using a migration system, or whatever.

It doesn't matter. And at any point, I could just reflect that into my module with a single line of code. So I think that's good. OK. AUDIENCE: The $5 word for that benefit would just be to say composability, right? Because by just being able to consume the SQL model as it comes in, you can use anything you wanted to create it.

JOHN MUELLER: My brain's not working well enough to quite see why that's composability, but I will take your word for it. AUDIENCE: Well, it is in the sense that what you've created now composes with any of a variety of tools that someone else could have made for defining the SQL schema to begin with.

JOHN MUELLER: OK. I guess I can see that. Now, we already had this thing where you could-- I think, or is this new? We already had this thing where I had dundercall. And dundercall is basically the same as doing a SELECT WHERE ORDER BY LIMIT OFFSET. But the thing that's now been added is that when you call dot data class, it optionally, and by default, also stores the data class inside the table object.

And if that's happened, then whenever you call dundercall, it automatically casts it. So now you can see when I call ALBUM_LIMIT=2, the things I get back are not dictionaries anymore. They're actually data classes. So this is slightly opaque that, oh, if you happen to have made a data class from this at any time in the past, you now have different behavior when you call it.

Yep, exactly. And it's not meant to be opaque. It's actually like-- another way to think of it would be like, oh, ignore the thing that's returned from this. And in fact, probably what you'd want to do most of the time is-- I'm not sure I documented this, so let's do this now.

Oh, here we go. It's at the end. If you want to have all tables, just call all DCs. And probably you would actually just not bother with the return type. This is how you turn it on, is you just run that. So you probably always just run that. And so that applies then not just to DUNDER_CALL, also applies to .get, which gets by primary key.

So those are the two things that let you get data classes. So then I've also added a lot of additional behavior, particularly to UPDATE, INSERT, and UPSERT. So if you haven't come across UPSERT, it's not a SQL keyword. It's a concept, which is that a lot of databases, including SQLite-- and originally, I think it came from Postgres-- have some kind of thing that lets you insert things.

And if the thing is already there, then update the existing thing where already there is referring to the primary key. So let's create a new table called CATS. So one of the nice things about SQLite utils is the fact that CATS is not there doesn't matter. OK, we've still got a table CATS.

It just doesn't exist. Yeah, this took me a second or two to wrap my head around. Like, OK, well, I can check if it's there or not by seeing if CAT is in dt or is in dt. Oh, well, I've just added that feature, yes. So I can say CATS in db.

Oh, is it dt? Yeah. So I added a DUNDA contain support to this yesterday. Oh, nice. Oh, well, I was surprised it worked, and I was very happy. I didn't realize it was there because you just added it. Yes, and I think you can also do it on the table itself.

Yep, OK. So then I can create it. And then I can look at the schema. I added this thing. I think it's-- I've added it to Fastcore. Highlight-- it's really badly named. Well, that's right. Highlight as a markdown output. This is actually a markdown output. You can say what language.

This is just a minor convenience. So you can see the schema that this has done. And so if I now re-run CATS in dt, it's now true. Or the Stringify version is now true. OK. So we've created a CATS table. So when we-- what did I say this same applies to?

That it's automatically casting to the data class? Yes. Or that it takes keyword arguments. Right. OK. OK. OK. Maybe we should just mention this curious feature about SQLite utils. OK. So then something else I added is now when you insert or upsert, you get back the inserted row. Now, we haven't called .dataclass yet.

I mean, we called it earlier, but that was before we-- all VCs. We haven't-- but the CATS didn't exist yet at that point. So therefore, we got back the dictionary. So here's an example of using upsert. But yeah, so now if we do the dataclass thing-- and let's just show you.

There's no point storing that. It's just enabling it. So now if I go cat equals CATS.get1. So now I can set my attributes, because this is a data class. And I can pass that in, because the first thing to be passed in is a-- OK, that type annotation is now wrong.

Let's fix that, shall we? BIM_FASTLIGHT/keyword_arguments DEF-- well, there it is. DEF_UPSERT. OK, so that's a dictionary of string to any or a data class which isn't of any particular type. So I think you just have to call it any. Cool. So you can see there wasn't much code for me to write to add this behavior.

So this is actually something quite nice I added to Fastcore yesterday. When you patch something-- so in this case, I'm patching table. If the something already exists, it stores the original version of that function now as _orig_ with that function name. This is a really nice way for me to now patch new behavior.

So it's just calling the original version, but it's doing the data class thing. And at the end-- As long as you don't patch twice. No, it's fine to patch twice. It checks if there's already an _orig__upsert there. So you can patch-- So is the default behavior when you do the second patch to clobber, but you have access to the old patch?

Or is the default behavior to advise, meaning that it executes both of them? So if you repatch, then _orig__upsert will still be the _orig___orig___upsert. So it clobbers the old patch. OK, so it clobbers the old one, but the old one's there if you want to access it from the new one.

The grandparent's there, but the old patch is not there. If you repatch the patched method, then the original patch-- Oh, OK, I see. OK, got it. OK, so-- oh, we can drop the table. So that-- yeah, that was basically stuff that I added as I started to implement the next thing I'll show you.

Nice. All right, so this is where things get fun, is we have a to-do list application. This to-do list application is like-- it's got nice resizing behavior. It all feels very modern. When I click on things, there's no full-screen refresh. I can add new to-dos to people.fast.html. People.support.add just appears.

Click on that. Change it. Enter. And it's kind of like it feels like a very-- you can click on anything at any time, and I haven't found a way to break it. Do you know what I mean? As you can see, it's fast. And obviously, I haven't spent much time styling this.

I haven't spent any time styling this. But it looks and feels like a reasonably modern kind of web application. The entire thing is written in 69 lines of code, many of which are blank. And it's all Python. And there's no templates. There's no JavaScript. And there's no CSS. It's literally a single file of Python.

So this is thanks to a thing I want to show you guys, which I know both of you have seen bits of, called fast.html. And the goal of fast.html and this kind of ecosystem is to allow people to create single-file web applications without the shortcomings of Gradio and Streamlet and stuff, which are really great systems.

But the shortcomings of those is that they're for creating dashboards and proof of concepts and whatever. If you like, you wouldn't create your whole startup as a Gradio app, probably, or a Streamlet app. And when people get to the bit where it's like, OK, I now want to have a different component that works in this different way and takes advantage of this JavaScript library, they say, oh, OK, that's possible.

Now you have to learn this entire new, massive, complicated framework that's harder than it would have been to just use it in the first place. So the idea of this is to let you start building something as easily as Streamlet or Gradio would, but naturally support growth from there.

There's no point where it's like, OK, you're done. Now you're going to have to use TypeScript, or now you're done. You're going to have to learn to create your own IPyWidget plumbing, or like Solara, like FastUI. And it's actually loosely based on the framework I created-- jeez, it's 25 years ago-- for FastMail, which supported one of the busiest sites on the internet.

It's this approach scales out in any way you like. So that's the goal. It's basically reuse your Python knowledge. You can use any CSS framework. You can use any JavaScript library. You can use Web Components. You can use all that stuff. And if you create something with your Python that you think other people might like, you can stick it up on PyPy, and other people can pip install your StyleSheet framework, or your Web Component library, or whatever.

And ditto for other peoples. So the particular one that, at the moment, I think it'll probably ship with is-- the CSS is called Pico, super simple, lightweight thing. But I'm pretty sure, quite soon, we'll have a Daisy UI, pip install, FastHTMLDaisyUI, whatever. Or if there are certain JavaScript things you like, you can easily wrap them with this, and pip install those.

There's no build step. There's no code gen. So in fact, let's take a look. So if I change this here to to-do list demo, and I'll pop up the terminal. You can see this running in the background here. And as soon as I hit Save, it's refreshed itself. And back over here, there it is.

It's-- yeah, because there's no build step or anything to do. It just keeps going. So all right. So let me show you how the app is built. And-- ANDREW FITZ GIBBON: Can I interrupt, again, one or two quick questions? CHRIS BROADFOOT: Any time. You don't need permission to interrupt.

You can-- ANDREW FITZ GIBBON: All right. Then I'll interrupt a little more liberally. So I'm aware of what Gradio does. And I think I've used it once or twice. But I have never explored it in detail enough to have an understanding of why it might not be the kind of thing that scales up from a quick start to a full application.

CHRIS BROADFOOT: Sure. ANDREW FITZ GIBBON: So I was wondering-- hard to summarize, but if you could give a sense of, what is it about the way it works that-- or Streamlit works that you feel makes it-- doesn't give you that sort of continuous path from quick start to a bigger, more complicated thing?

CHRIS BROADFOOT: Yeah, absolutely. That's not how it works. So while you're finding that, Alexis, Gradio is very oriented around getting some inputs to a function and then displaying back the outputs. Like, it's ideal for I have a model that makes predictions or I have something that generates something. You can do some state tracking and things like that, but it gets-- it's very, very nice for-- like, this kind of interface here is so easy in Gradio.

But trying to move to something where, I don't know, you're storing stuff in the user's cookies, or you're doing state, or you're keeping track of multiple things, or you want multiple pages in your app, it does get very cumbersome very quickly. Yeah, so here's an example. Right, let's have a look at an example.

So here's an example of something that has an Upload button, or you can click an example, and you can click Submit, and it runs a machine learning model on it. It's indeed a doggie. This is a classic kind of Gradio interface. And if you look at how it's implemented, you create an image, you create a label, and then you create an interface that will call this function.

These are your inputs, this is the output, and these are examples. It's a very specific kind of application it can create. And for that particular kind of application, it creates very quickly and easily. It's not a general-- like you couldn't create Instagram in this. Right. If you wanted to create Instagram, well, Meta, at the time before it was Meta, I guess originally it was created by Instagram, but now you use Django.

So Django, as you all know, is a general purpose web framework for which you can build anything that your brain can think of. So fast HTML is like Django. It lets you build anything. And Streamlet's similar to Gradio. It's the same kind of idea of like sketch out the basic inputs and outputs and maybe one function to call everything.

That's it. So one thing I think about when I think about this design space is that it sounds like Streamlet and Gradio give you abstractions that are very convenient to work with, but they don't fundamentally map on to the underlying abstractions of how a website is built. Like-- Precisely.

They have an HTML and everything. And so you hit that-- Let's take a look at this example. --impedance mismatch. So let's take a look at this example of how-- compare it to this. Let's instead see how was this screen created. So this screen has got a header. It's got a thing here with a button and an input.

It's got a list of to-dos. And optionally, got some details about the to-do underneath. So this one here-- and it's on the root of our website. So here is the implementation of that. And it contains-- if you look at the HTML, it contains a main. The main contains an H1.

The H1 contains an article. The article contains a header, an unordered list, and a footer. So the thing that you write here is actually the same as the HTML. So OK, give me a main with an H1. The body is an unordered list containing some to-dos. And above that will be a header containing a form with an input and a button.

And we'll give the whole thing a title, which you can see up here. So yeah, in this case, the thing that we're actually working with is HTML. And also, we're working with things like posts. And IDs. So yeah, we are working with the same elements that anybody building something with React, or Django, or whatever it is.

And that's why, with this, you can build anything. And the abstractions that you're building with are not totally different. They're tiny wrappers over the basic foundations of the internet. - OK. And those things that look like function applications are creating data structures that you then convert into HTML later.

- Yes, which we will see. - OK. - Yes. - Yeah, so this reminds me of a library called Hiccup that works in a similar way, not a Python library. So I have a second question. - What language is that in? - It's in Clojure, C-L-O-J-U-R-E. And-- - Oh, yes, it was J-U-R-E.

- And it uses the native data structures of vector and dictionaries to represent an element and its attributes. So when you want to build a big HTML page, you kind of build it just by essentially doing things that look like nested function calls that are actually just returning the basic data structures of vector and dictionary and a list that mirrors the same thing.

- Yeah. - And this is very different to-- some libraries will have a form object, right, that's a Python object or a Pydantic model or something like that. And then it also has, on the front end, a view component and some JavaScript and a WebSocket stream to synchronize data between the Python object and the actual front end.

And then the view or Svelte or whatever your JavaScript framework is does the magic and eventually produces the HTML form. So there's a lot of machinery in the middle there, which is sometimes very nice. But yeah, in this case, it's like, oh, you just write the thing that produces that HTML.

- Well, one of the kind of aesthetics that-- or design approaches that can work well when it's possible is to try to embrace basic data structures as much as possible rather than introduce new types, because then the basic data structures are, again, $5 word, composable with generic manipulators that you already have for basic data structures.

Like in Python, that would be like comprehension, right? If you're representing your list in your HTML that will be eventually turned into HTML as just a Python list, then you can go and modify every element in your list just with a comprehension. - So in fact, every one of those HTML functions is returning a three-element Python list.

The three-element Python list contains, in order, the name of the tag as a string, a list of the children, and a dictionary of the attributes. And yeah, it's interesting you mentioned the Clojure one. I haven't seen that before. But basically, almost every functional language has a version of this.

So Haskell has one. OCaml has one. And there are similar things in Python as well, although they're not quite as functional as FastHTML or those OCaml and Haskell ones are. It looks like the Clojure one. And it's how I wrote FastML. That was written in Perl. But to me, apart from everything else, I don't like switching between files.

So templates were created back in the day because web design was very difficult because you had to write lots of HTML in order to deal with the quirks of each browser. And a lot of the visual representation was contained in the HTML. That before CSS, all of it was.

So because we had web designers, that was a very specific kind of job which involved, to a large degree, knowing about quirks of browsers and stuff. Web designers needed to be able to write HTML and look at the HTML. And they were not generally coders. So we came up with this idea as a community of being like, oh, let's give them templates.

And so the place where the name will go will be like curly bracket name. And the place for the email will be curly bracket email. And they don't have to worry about that. They can design the whole thing, give it back to the coder, and then that's now a template they run.

This doesn't seem useful anymore because we just do semantic HTML anyway. So to me, the idea of having a separate file that contains two separate languages, the first separate language is HTML, the second separate language is like Ginger or whatever, I'm not very fond of. So I want to be able to do it all in one file.

And yeah, this functional approach, it was actually Austin who-- and also Daniel, who's one of the authors of one of my favorite books, which is called Two Scoops of Django. They both told me about some of these functional libraries. OK. So yeah, so I'll show you guys more about this in a moment.

But-- I got one more question. Sorry. You said I had a license to ask questions. So here's question number two. You talked about how this design, one of the goals you had in mind was for it to offer a smooth path onto a more full-featured website that you would do for a real app.

And part of that is cleaving to the underlying abstractions of the browser so you can work with them, rather than hit the impedance mismatch problem. But another part of it is using components that other people have made. Yes. So-- Like this one, for example, which I made. OK. I was going to ask about calendar pickers and React components and stuff like that.

This one, which I made. So we will see that in a moment. And they are written in Python and distributed as pip-installable things. And so, yeah, we'll see that. So let's look more at how to implement this particular form. OK. So I'm going to assume you know HTML. So you know there's a thing called a form.

You know there's a thing called a button. And as you see, we've got like, you know, it's all autocompletes all here. So if I start typing, it's all here, ID, inert, input mode. So I've got all of the attributes there as autocompletable things. But they're all standard stuff. You'll see I don't return an HTML object with a body.

But instead, I return a tuple with two things. One is what's going to be inside the body, which, as we saw, is a main. So the body contains a main. The other things that are there have been added by my extensions, so you can ignore those. So this is the thing that goes into the body.

And then this is the thing that goes into the head. So that's-- you can return an HTML if you want to. Like, I could have gone return HTML head body, but you don't have to. And we'll see why this has got some benefits later. So for now, I'll just say, like, this is both more convenient.

And as it turned out, it's got some benefits. All right, so the child of a-- so when you put in a tag name, you'll see that the first thing is star C. This is where you put children. So you can write as many children as you like. So title, hello, there, whatever you like.

Put a link there. So that's fine. And then, yeah, any attributes are just attributes to the tag. Class is special. You can't write class equals, because class is a keyword in Python. So you have to write C or S equals. So you'll see that we have a-- the main has a class.

OK, so that's the basic idea of constructing HTML in Python. You have to have it respond to this. And so FastHTML uses a pretty standard approach of having a decorator. So you start out by creating a FastHTML app. And then the decorator, this can be any HTTP verb. So get, we'll do a get.

And this is what it's going to listen on. So if you've used Flask, this is pretty standard. Or FastAPI, FastHTML is loosely based on Fast-- or quite strongly based on FastAPI. I went through the FastAPI tutorial and tried to make each section of the tutorial work with the same syntax.

So when you create your FastHTML app, since we optionally make it, you don't have to manually create your HTML header. You have to have some way to say what headers are going to be there. So this is how you do that. And in this case, it's going to be-- so FastHTML comes with a link ready to go for Pico CSS.

This is, again, something you can give people. It's just these things predefined. And I added some additional stuff into a stylesheet. So if you look in the FastHTML repo, you'll find examples. And yeah, I just changed some of the sizing a little bit. I found their defaults a bit too big for my liking.

But you don't have to use any CSS if you don't want to. OK, so how on earth does it go ul star todos? What's going on there? Well, todos, you hopefully won't be too surprised to learn, comes from Fastlight. So I create a database using SQLite utils. And then I ask for the todos table.

And you'll be pleased to see, Alexis, I used capital T for my todos data class. So as we've seen, if you type todos parentheses, it calls a select statement. And by default, there'll be no limit and nowhere. And therefore, this is all of your todos. They get passed as children to a ul.

So how on earth does an unordered list render a data class? And the answer is that if your class contains a special dunder xt, these things are structures. I call them xt structures, standing for XML tags. If you have this special thing, this is what's used to render it in HTML or actually as an xt.

So I patch this into my todo. There's lots of ways you could have done this, right? Like, if you don't like this, you could instead simply have a function called xt. You could get rid of patch. And you could have just gone map, like that, right? Like, you could certainly do it that way as well.

Either one's fine. - A very minor thing, Jeremy, but the todos table, if it doesn't exist, might, I think, cause issues. So-- - Yep, exactly. So there's a few ways you can handle this, right? So in my case, I just have a little notebook where I've been fiddling around with things, and I created it there.

And I added a few todos to it. So in fact, you can see, if I run these cells, I've got recreate equals true, so that's actually deleted and recreated it. So if I now go back to here-- wait, is that using a different database, todos.tb? That's strange. Hm, now why did refreshing-- something was caching that.

Interesting. Yeah, that's what I think about. Yeah, so if I change the-- rerun the database, I get back the different set of rows. So yeah, it's like-- Simon Willison, in his examples, tends to have stuff in his insert that has additional things to describe how to create the table if it's not already there, kind of neat.

But I prefer to use something called migrations, which we probably won't talk about today, to do more stuff in SQL or outside of the main application to get the initial structure in place. Yeah, or some people have like a if dunder name equals main at the bottom. And that way, you can run Python db app.py, and it would create things.

Yeah, you could have something at the top that's like, oh, if it doesn't exist, then create it. So yeah, there's lots of ways you can do that. But it's a good point. Now, the each todo has two links. And so here are those two links. Now, rather than using an a tag, I'm using an ax tag that sometimes I add an extended version of a tag.

And when I extend a tag, I add an x to the end of it. And it's not very extended. It's just like-- partly, it's like some of the defaults are a bit different. So it starts out with the text you want is first. And so I don't need any keywords here or whatever.

- So a doesn't mean anchor. a is a general purpose constructor for a tag-like structure. - No, a means anchor. This is an anchor tag. Yeah, these are all-- you see their links? They're a links. OK, yeah. - So ax is an anchor tag that also has other-- - An extended version of an anchor tag, or an extended version of a hyperlink, yeah.

- OK, so it's not like block or some kind of superclass that represents all block-like tags or anything. It's just an anchor. Got it. - Otherwise, it wouldn't know what tag to use. I didn't see. Yeah, so each to-do item is a list that contains that first link. And maybe it's done.

And then that second link. And also, it's got an ID. So it knows which to-do it is. So it's got this little tiny function here, which just returns to-do dash ID. So if I look at one of these list items, you can see here, li to-do one. I don't know where all this data default font size is coming from.

Curious, probably some Pico thing, or I don't know. OK, yeah, so when I click Delete, that sends a delete HTTP method to that endpoint. And I can just call todos.delete, because that's how SQLite utils slash Fastlight works. So the next thing to talk about is how come we have what appears to be a SPA?

There's no full screen refreshes or anything. But it looks like I've written a web 1.0 style app here. And the trick to make this work is something called HTMX. And HTMX basically adds what I think of as the obvious missing features to your browser and HTML. And so it adds four features, basically.

And once you've got HTMX, which is a JavaScript library that's auto-installed if you use FastHTML, your browser behaves as if the following things are true. Normally, a browser, or kind of without JavaScript and stuff, a web 1.0 browser, could do two things. It could provide a hyperlink, which, when clicked on, sent a GET request.

Or it could have a form, which, when submitted, sends normally a POST request. There are lots of other HTTP methods. So HTMX adds all of those other methods as things that a browser link button, et cetera, can do. The second issue with web 1.0 HTML was only two things could cause any action to happen.

Clicking a hyperlink could cause a GET request to be called and the entire page replaced with a result. Or it could cause a submit to be done with the form data. And again, the page to be replaced with a result. HTMX makes it so that any object can cause behavior.

The third is that with web 1.0, the result from the server can only do one thing, which is replace the whole page. HTMX makes it so that you can either replace the page or replace a single DOM element or delete a DOM element or insert after a DOM element or insert before a DOM element.

So it lets you change where the result from the server goes. Damn it, what's the fourth one? So we've got any HTTP method. You've got what you change. You've got what can trigger it. I'm sure the fourth one will come up in a moment. So the way this works is that you add additional attributes that's-- let's right click on this and choose Inspect-- that have an HX at the start.

So HX-get means this will cause a get method to be sent to this, which obviously is the same as what an ATAG does anyway. It's the sender get method, but this is an HTMX get method. OK, so I mentioned that the result doesn't necessarily replace the entire page. How do you decide what it does do?

Whatever target is is what's replaced. So there's a thing called currentTodo, which is actually a empty div down here. So when we click on this, it'll call todo/1, and it'll insert the result into currentTodo. So we could see what's going to happen by manually going to /todos/1. And you can see, there it is.

There's that little snippet. So the HTML and head and body has got added by my browser. But actually, I guess if we look at the network tab, you can see, actually, this was the response. So you get this little HTML partial, and it will be inserted into this div.

So let's try it. If I click, and you can now see, yep, there it is. It's been inserted into this div. That make sense? So that's what HTMX is. And Alexis was talking earlier about walking into the flexible foundations of the web rather than creating some single-purpose abstraction. So HTMX basically endeavors to make that web foundation dramatically more flexible and powerful by just taking what's already there and doing an almost, in hindsight, the obvious thing, which is just to remove the constraints.

And maybe just to clarify something, you don't have to use HTMX with FastHTML. These are two orthogonal things. I've built basically the same app. But any time you want to do something, you can just get the full page, and the full page is rebuilt now with an extra to-do item in the list.

Yeah, I mean, maybe either now or in the future, you could show us that if you've got the web 1.0 version of this. But yeah, absolutely. So the things that FastHTML does to make HTMX more convenient is, for example, by default, it includes the JavaScript for HTMX in the header.

And in the autocomplete, all of the HX things are there for you. And it's just minor little things. But yes, it's not-- you certainly don't have to use HTMX. So here's a question about HTMX. You say that it allows you to make any element interactable. That makes sense. Thinking about native development, this is like in Cocoa Touch having a gesture recognizer.

It's an object that you can attach to any UI element and then will allow gesture recognition to be associated with it, taps or swipes, other things. And then you say that it lets you choose which kind of HTTP method is going to be executed. So that's good, because beyond GET and POST, there's things like PATCH and whatnot that enable partial updates that aren't as well known but have relevant semantics.

Or here, we've got-- sorry, you can see them here. We've got DELETE to delete a TODO, POST to create a TODO, and PUT to update a TODO. But so my question is, while those HTTP methods have a lot of relevant semantics, there's all sorts of actions one wants that aren't expressed by an HTTP method, that aren't just-- yeah.

So in that case, then you would use PATH. So for example, SHOW and EDIT FORM, there's a different PATH for that. So you can have a-- so like, and in fact, you could use only POST, or you could use only GET. Like, there's no particular reason you have to use any other HTTP verb.

You can only use PATHs for everything, and some people do. So you could have slash TODO, slash DELETE, slash ID. That's a GET. - So I can use HTMX to make an arbitrary element interactable, but then I have a choice about what the action means. It could mean, in a simple way, hey, update this component that's in the DOM.

Or it could mean, actually, hey, send a thing to my server, a GET request, and that thing is now going to trigger-- - Well, I mean, it always means send an HTTP request to the server. You choose what request method it is, and you choose what PATH it goes to, and you choose what data is in it, and you choose what headers are there.

So it's like, it's the normal things you have to choose when you create any HTTP request. And then on the server, this is what you write. You then decide what happens. What happens when this PATH gets this HTTP method? Oh, it runs this function. - OK, so it's single page and very dynamic, because we don't see a whole page reload.

But it's still doing a round trip to the server, even for incremental local modifications in the page. - Exactly. So for example, one of the things you are likely to want at some point is something where maybe it's create new user. And as you start typing the username, a little thing pops up saying username is available.

So that's an example. So you can absolutely attach hx-keyup or whatever. Sorry, like you can attach-- I think it'd be hx-trigger=keyup. So now we're attaching to the-- as you type, it's sending stuff to the server. And then the server sends back something saying, put user is available into the user available MTDIV or whatever.

So it's surprisingly OK. Like, at least even from Australia, we're at 70 milliseconds each way to the US. And most servers are in the US. It doesn't feel at all slow. Like, you wouldn't write a computer game in this because every time you click a key, it has to do a full round trip.

But anything that can happen in about 150 milliseconds is going to be fine. - OK. - And for stuff you do want, you can still write JavaScript, of course, if you did want stuff that happens in less than 150 milliseconds. - So I noticed that the HTMX magic is managed through these hx attributes.

And part of interoperating with the web is not just being able to use HTML and JS, but using the enormous suite of components that other people have made. So I know that in terms of heavyweight reusable components, like if I want a complicated-- like, let's say I want to have a location picker, which is going to present a browsable, zoomable little view into a map and allow me to put a pin and then show me the address and then confirm it.

That's a very plausible thing I would want. - HTMX is great for that. - OK. Because I don't want to make that myself, but I also don't want to have to become a full-time React JS person in order to be able to use it in this thing. So what's the integration story?

- So look, somebody else, hopefully, somebody who's not me or who likes Python more than I do-- sorry, who likes JavaScript more than I do, hopefully, will create the pip installable thing. So for example, there's a sortable JavaScript library. - OK. - And here's an HTMX demo of using the sortable JavaScript library.

You can see these things are being dragged around. And I've got to try and get it between the two. And you can see underneath, these are the requests that were coming in. So this is actually using HTMX that now, because sortables has been attached to HTMX, these are triggering HTMX HTTP events.

And you can see the response coming back is, again, a little HTML partial. So the first person to make HTMX and sortables work together is going to have rights in JavaScript. And so here's the JavaScript they wrote. But once somebody's written that once, they can stick that on PyPy, and now everybody can use it by just writing sortable equals true or something in their form, whatever.

And it's not a lot of-- A, it's not a lot of JavaScript. And B, the JavaScript's not complicated because HTMX is using very standard JavaScript stuff. So I don't know much about React, but I know that it tends to be integrated. It provides a reusable component model, which is why it's been successful.

But I also know that component model is integrated with a certain data flow model. Do we know if anyone has used HTMX to-- This is not for React. So this is for stuff more like-- What about web components? Sure, web components, for example. Yeah, web components. So this is the way the web's going, right?

Well, let us hope, yeah. Is these kinds of-- these components can go in React or Vue or Angular. Hopefully, at some point, somebody's going to add the first HTML version in here. And React, I'm not familiar with that. I mean, but it seems like it's the first generation of an idea that has been refined with some of these later component models anyway.

So maybe some of those component models are easier to HTMX-ify. And then the kind of promised land that you might get to is that there are PIP-installable components once you're there. Yeah, that's not some distant promised land. I mean, hopefully, within a few weeks, there'll be quite a few.

And as I say, it comes with some already super basic stuff like group and card. So yeah, great. So let's have a look behind the scenes a little bit. And then we can maybe come back to here to look at some of the details, if that's of interest. So the kind of basic thing, I guess, this is all built on is fastcore.xml.

And originally, it was just a list. XT was just a list. Now it derives from list and has three very handy properties-- tag, children, and attributes, which are just in case you forget the order of what's 0, 1, and 2. That's all they are. And it also has an init that you can only pass in exactly three things.

But it's just a list. And so that's what an XT object is. It's a list. And you can just use a list. There's no need to use an XT object. This is just meant for your convenience. So if you use the XT function, you pass in a tag, you pass in the children, and you pass in the keywords.

And so XT got my fancy tag. And then that contains a child and another child and an attribute. AUDIENCE: So why are you doing XT as a function rather than patching it in as a method onto the list type? Well, I guess because list isn't a type of search, OK?

You know, I mean, now that I've added this-- so originally, I wrote this. Now that I've got this, I could have replaced it as having under init could have done these tiny little things. So it's just history. Anyway, it's not much code, so it's fine. AUDIENCE: I'm just thinking about the analogy with the anchor type, where you had anchor, and then you used patch to retroactively conform to this de facto protocol of underscore underscore XT.

Yeah, but you're not doing that here with list. And I don't want to change the constructor of all lists. OK, all right. I very much doubt you even could, because it's built in. OK, so then I just grabbed all of the main HTML tags and added them all to the module.

So now, as a result, if you call HTML, head, title, body, div, p, input, image, instead of using class, you can use HTML class or class or class. These are all kind of standard things. Oh, actually, I've also seen some people use underscore class, as you said, as well.

Underscore class, which means we should also allow underscore for. OK, so there's various different ways you can spell it. So yeah, so as you can see, it's just returned 1, 2, 3 element list. Or these are three element lists. So that's it. And it doesn't have to be-- like, you can put text there, as well.

Maybe that would be better to demonstrate. So OK, so as I mentioned, they've got property names. What else is there to show you? OK, so then the thing that converts that structure into XML/HTML is this. So you can see here, I've-- here it is. OK, so that's the basic foundation.

So there's hardly any code, as usual. I don't like writing code. I don't like having to make people read lots of code. So then on top of that, there's fasthtml.core. Now, fasthtml.core is the thing which does this stuff. And this is basically a pretty thin wrapper around Starlet. Starlet is a ASGI-- I think it's a ASGI framework.

This ASGI thing is a standard way to create Python web applications. And basically, almost nobody understands it or knows what it is, because when I started asking around to try to learn about it, everybody was like, I don't know what it is. I just use other people's things. So now that I do know what it is, at some point, I'll do a little course in that.

For now, I'm just going to say, don't worry about it too much. It's a thing which Starlet provides that thing for you. And Starlet lets you create routes that send you off to functions, and have responses, and have things in requests, or whatever. I think it's not necessarily designed to be something that most people write things indirectly.

I suspect it's mainly designed for people to write stuff like fastHTML in. Or indeed, fastAPI, which is a super popular library, is written on top of Starlet as well. So fastAPI is designed to create APIs. So all of the-- so you go to the tutorial. Wait, how am I supposed to use the tutorial?

This is not good. Oh, I see. That's interesting. You zoom in, and it makes everything disappear. Yeah. So basically, in the tutorial, it's a really good tutorial. And a lot of the things you learn here will also apply to fastHTML. It always returns JSON. It returns dictionaries which become JSON.

So at the end of this tutorial, you don't actually end up with an application at all. You end up with an API that you could write an application on top of. And that's going to require JavaScript. So fastHTML is designed to be something where the thing that gets returned here are HTMX partials or HTMX pages.

So that's basically what this is. So here's a list of all of the HTMX headers, for example. So we haven't even talked about this. But when HTMX calls your HTTP verb endpoint, it always includes, for example, an hx-request header. So you can always tell if something is sent by HTMX or not.

What I might do is jump to the tests just to give you a sense of what fastHTML.core does. So fastHTML is something that I can create an app. And then this is quite neat. Starlet has a thing called testClient, which I can now call cli.get on, for example. So that may be the first one we should do this manually.

So cli.get-- it handles any HTTP verb that can go here. And I can say, OK, what URL? And you can also pass in headers, cookies, et cetera. So we're going to go to /hi, because we've just defined a listener there. And you can see that's returned a response. Which has various things in it, including text.

OK, so-- It's somewhat helpful here to know a little bit about the starlet requests, and responses, and things like that. But maybe that's a good separate cutting point for-- I mean, you kind of hopefully don't, right? Because here, I just returned text. So I'm kind of trying to make it so you don't have to know about any of those things.

But you can use them if you want to. But in fact, in our tests, none of them use a request or a response, except for the very first one just to prove to you that you can. Yeah. So if I didn't know the fast HTML way to do things, but I did know, well, if you give me a starlet request, I know what I'm doing with that, then that's great for the people who have that.

But if you don't have that, then you don't need to worry about it. But if you do have-- if you're fine with that, just use starlet, in a sense, maybe. You know, so yeah. So basically-- so I mean, one thing you do need to know from starlet and from Flask and from just about everything is that the thing where you say, oh, this is the endpoint I want you to listen on, you can whack a thing in curly brackets.

And that means anything that gets put in there will work. Yeah, maybe the first few we should do manually. So yeah, as you say, starlet has this concept of requests. And if you add a parameter to your function, to your route function, that's called request or rec or r or any sub scene of request, it will automatically be passed this special request object.

Oh, you know what? It would be better to look at index, because that actually shows you, I think. Yeah, here we go. So this one here-- well, there's a lot of stuff in request. I mean, we could just print it out. So that's-- actually, no, it probably won't work.

Never mind. So yeah, the request object is something that you could go to starlets and go to requests. And you can see here, request. And there's various things in a request object. The idea is, though, with FastHTML is that you never have to worry about any of that. But it is there if you want to.

So one of the things in there is a dictionary of headers. So the test client uses this as their header. So authentication is often the domain where one starts to care about HTTP headers, because cookies and things like that are managed through headers. Well, you're not going to have to worry about that either.

It's all going to be automatic. But let me get there so you can see. We won't do authentication today, but we will do cookies. OK, so if there's a curly bracket thing here, that's going to get passed to your function if your function has a parameter with the same name.

And give it a type, right? So it can-- there you go, OK? So this is a bit of magic, although it's pretty standard magic for these kinds of libraries is to work this way. And it will endeavor to cast things appropriately. So in this case, obviously, there's no such thing as ints or whatever in paths.

But we've asked it to become an int, and it will turn it into one, like so. OK, which actually, that's exactly what this one already does, so it's not very interesting. If you return an xt, it will be automatically 2xml'd. So when we call /html/1, we get back all this HTML, because that's what this gets.

So you can-- this is quite a nice, neat little thing. You can register regular expression parameter types. So here, I've registered something called image extensions as a regex. And then if you stick it in here, it will match anything that matches this regular expression. The type can be other more interesting things.

In this case, model name is an enum. So /models/alexnet returns alexnet. AUDIENCE: Oh, you overwrote /name. I broke it. That's what I did. Come back to that. So if I look for /models/gpt5-- sorry, yes, /models/gpt5, then I'll get back a value error. gpt5 is not a valid model name, at least not at the point we're recording this video.

OK, so you can also put stuff as query parameters. So again, it just comes through exactly the same way. You can also add stuff as headers. They'll be lowercased and dashes turned into underscores. If there are HTMX special headers, and you have a HTMX headers type, they will be passed to there.

Or you can just use the special name, HTMX. AUDIENCE: So in those examples, is what we would see if we hit those endpoints, that those values are echoed back? Like, it would read the user agent from the request and then return that back in the response? Yeah, so it's returning a string.

So the string is the number 1. So it just returns the number 1. You can see here, this is the expected value. So it's returning the number 1 as a string. Just like up here, we returned the string. Hi there. This time, we're returning-- ANDREW BROGDON: Yeah, I understand the return mechanism.

But what I'm looking at is the parameters in the-- or the-- let me see the def of UA, right. OK, so UA user agent colon. User agent's a special keyword that means I'm going to take my input parameter from the HTTP header, is that right? No, no, no. Nothing special at all.

There's only two special ones, which is HTMX and request. Everything else is not special. Everything else looks in order to see if it's a path parameter. If not, is it a query string? If not, is it a header? So it just keeps looking to try and-- it does everything it can to find something.

And so it found a header for user agent. OK. Yeah, got it. And if not, is it a cookie? So here, we set a cookie. So this is where we do, for now at least, require-- have to create a starlet response object. And we set the cookie. And so here, that cookie was called now.

So here, we put now. It will grab the cookie, because it's not a header, and it's not a query string, and it's not a path object. So it gets the cookie. Also, you can pass things into a POST request. So in this case, the data we're going to pass in is this dictionary.

And so if it's-- this is quite fun, I think. If you have data in a POST request, and you have a data class, it will automatically put it into that data class. So here, data is now a bode object, because you asked it to be. And if we try to put in something that you can't fill into a bode object, then you will, of course, get an exception.

It's like, oh, you can't put a C in there. You don't have to use a data class. You can also use a dictionary. Now, if you use a dictionary, then it won't be a and the number one anymore. It'll be a and the string one, because it has no idea what data type you want it to be.

So this is a good reason that data classes work nicely with FastHTML, and that's why I added Fastlight's data class support. - And you could do, like, if you wanted to be real fancy, you could do Pydantic and then have custom validation on that. Oh, it has to be a string, and also it needs to be a string that starts with these letters.

And-- - You can do all that. I don't love it myself. I would rather do that in the function, the handler. To me, that's a better place for that, unless-- yeah, I don't see the point of using Pydantic for that, because you can put it in the function, and then you can respond to those validation issues in a more nuanced, handler-specific way.

But yeah, you could. You can also create a named tuple. So that works fine. Again, named tuples are untyped, so it won't know what that is. Or you can just create any old class, and as long as it has annotations, it will know what types things have to be.

So Bodhi2 is a number, because it's looked inside here and being like, oh, it's a number or none, so we'll try and make it a number. Yeah, so it tries-- basically, FastHTML does everything it can to give you the parameters that you've requested, is basically the way to think of it.

So here are two things that occurred to me. Well, three. One is I'm having all these flashbacks from the last time I built a significant web app endpoint. There's so many of the design patterns are similar. I'm not sure if this is because how everyone builds endpoints, or it's just a coincidence.

Yeah, it's an attempt to be like-- no, it's an attempt to be as normal as possible. OK, well, that explains it. I guess I wasn't doing it in a weird way. If we go to the FastAPI tutorial, I literally went through every line of code, copied and pasted it, and tried to make it work exactly the same way, except the return is not JavaScript anymore.

So yeah, if you're a FastAPI user, it should be extremely normal. So the second thing that occurs to me is that the behavior of search with fall through when you get a key, essentially. Is the key something that was given to me in the path? No. OK, well, is it something that's there in a query parameter?

No. OK, is it in the header? And here it is, by the way. And when I sent this code to John O, he was happy. He was like, oh, OK, it's nice to see. Well, and I had to see that, right? Before I saw that, I didn't trust it.

And what I was writing was, just always give me the request. Thank you very much. And I'll check if in request.cookies, or request.useragent, or whatever, I'll go get the thing myself. Because I didn't know what magic you were doing. And so I was like, OK, fine, I can see it now.

OK, now I'm happy to trust. It's slightly magic, and I'm not saying that's bad. I can see it's dead handy. It also triggers my magic Spidey sense. The one possible failure mode I could imagine here is if someone naively chose as their name for a parameter, a query parameter, a key that's already conventionally used as a header name, or a cookie name, or something like that.

Well, that would be OK, because headers go last. So the order of these was carefully chosen. But if you have a query parameter and a path parameter that are the same, or if you have a query parameter that's the same as a cookie, you won't get your cookie anymore.

Well, also fortunately, the header field namespace is not polluted with like 8 million things that are out of our control. It's a relatively orderly part of the universe. So it's not a problem. The last observation I have-- and this may be just that I'm not used to doing a ton of web development-- is when I look at how the code looks and how the logic of it looks, there's these two namespaces that are side by side, because we're trying to understand their interaction.

But they're still different. And that's the namespace of the URL path. We have to choose the string that represents the path. And then separate from that is the namespace of the function names and the kind of parameters within the functions. So one is necessarily-- The name of the function, you could call them all underscore, I think.

And it bothers me a little bit that there's just like-- yeah. So the reason I haven't done anything about that is because this is how fast API and Flask and everything works. Yeah, when we're building the web from scratch, one wouldn't want to have this weird world where you have the path.

You have this effectively a mini-language, which is what's expressed in the URL path. Yes, that's-- And then another language, which expresses the action that's actually performed, the symbol that represents the action that's performed in the server program. And then you have a whole mapping thing in between. And you have to like, well, is this the right name?

Am I changing the underlines? Is this exposing too much? Is this exposing too little? It all feels a bit jerry-rigged together. But I guess that's the web for you. I quite like it, especially because you can have multiple methods. So you can have-- the get on slash is generate home page, or home might be my function, because it's going to respond back with a full web page full of all the content that I want.

But the post on that might just be like you're submitting the form to sign up for my email newsletter. And so then I can have a separate function that's listening for the post option. I just renamed everything to underscore. And it still works. Nice. Maybe that's not a bad thing.

Because yeah, why are you spending time naming this function that never gets used? How would you call it with testing, then? You can't use the function name anymore, right? It's fine. I'm just-- client.get. OK. So I do have this. All my tests work that way. I kind of like that.

But I don't spend a lot of time doing web development. So I don't know if my tastes are informed by my deep judgment and experience, or informed by my vast ignorance of the domain. I guess a little of each. I'm not sure which of those I should listen to.

Anyway. OK. I've got to go. This was very cool. This was very cool. Thanks very much. I had a feeling that was going to take a while. We've got a lot more. Well, not a lot more than before. But anyway. A bit more. But this is something that we'll be showing people a lot more from, I suspect.

Different ways to write the same apps. Different extensions of it. How to make it play nicely with the database stuff that you showed, which we didn't really get a chance to go into. Like, why those data classes play so nicely with it. Yeah. Yeah. So there's a few cool parts, too.

All right. See you. Bye.