[HN Gopher] Htmy - Async, pure-Python rendering engine
___________________________________________________________________
Htmy - Async, pure-Python rendering engine
Author : friendly_deer
Score : 135 points
Date : 2024-11-27 01:10 UTC (21 hours ago)
(HTM) web link (volfpeter.github.io)
(TXT) w3m dump (volfpeter.github.io)
| throwaway314155 wrote:
| Looks great. Anyone using this in production?
| ddanieltan wrote:
| How does this compare with FastHTML?
| revskill wrote:
| Slower because there is no fast in the name.
| murkt wrote:
| Would love to see some benchmarks for all these libraries that
| compare them to Jinja2.
| mixmastamyk wrote:
| They are almost always slower, because jinja uses some
| compilation tricks. But, it usually doesn't matter compared to
| remote database access.
| pplante wrote:
| I was looking for something like this a few weeks ago. I
| typically use Django and hate the template engines limitations. I
| needed to make some reusable components and the best option
| available was switching to jinja to get their macro support,
| bleh.
|
| This reminds me of the best part of Flutter UI composition, but
| in a language I always return to.
|
| Have you done any benchmarking? I don't even know what the
| comparison would be.
| neeleshs wrote:
| You might want to look at django-cotton for components
| kissgyorgy wrote:
| Check this out: https://compone.kissgyorgy.me/
|
| Much simpler than this library, components are simply
| functions, rendered to strings.
|
| I made one microbenchmark, it's "only" 2x slower than Jinja2
| right now, but I know how to make it faster.
| anentropic wrote:
| if you can make it as fast as jinja2 I'm sold ...I haven't
| done my own benchmarking but so far I haven't seen any of
| these HTML-in-Python libs able to report comparable
| performance
|
| I've implemented a bunch of AlpineJS "components" as jinja
| macros in my current project and ... it works, but it's
| pretty ugly and it sucks not having type safety or ability
| for the IDE to understand connections between the template
| and the Python code
|
| what I really want is something like JSX/TSX for Python...
| having gone through this process I can see why that approach
| is desirable. I kind of feel like libs which mimic the syntax
| but unable to provide the type-safety/IDE support are missing
| the point. So although I love the look of "Python HTML
| element objects" approach libs like yours and OP have I think
| for now it is probably the best way available.
|
| for my current project we are pre-compiling all the jinja
| templates (via Jinja's own utils) for deployment as AWS
| Lambda
|
| I did look into JinjaX but it has its own separate jinja env
| and secondary template cache and didn't look like it would be
| easy to plug it into the pre-compile step
| rubenvanwyk wrote:
| +1 for TSX for Python, that would be great!
| globular-toast wrote:
| How many of these are there? I also pointed out htpy
| elsewhere in the thread.
| dcreater wrote:
| Is there a comparison or guide to choosing python frameworks?
| Every few weeks there's a new one posted here
| eyegor wrote:
| In the real world, for web things, people use django or
| fastapi. I'd suggest picking a project with lots of
| stackoverflow questions and poking around their docs to see
| which makes you the most comfortable. Personally I tend to
| favor litestar these days since it has good docs and issues
| don't sit around for years waiting on one dude to merge prs
| (fastapi) and it's a lot nicer than django (and I hate django
| docs).
|
| Flask/quart are painful to work with due to horrible
| documentation in my experience, but they're popular too. Quart
| is just an async rewrite of flask by the same owners.
|
| Litestar has a half baked comparison chart here:
| https://docs.litestar.dev/latest/
| devjab wrote:
| I think the "rule of thumb" is that none of them are better
| than using HTMX with templates. HTMX obviously having some
| limits in terms of security and complex REBAC.
| LaundroMat wrote:
| Or Unpoly. I've been working with it for a month now and it's
| a real pity such a robust library it gets so little
| attention.
| anentropic wrote:
| HTMX + templates are complementary to a backend framework
| rather than an alternative to one
| fermigier wrote:
| Not a comparison, but a fairly comprehensive list that I
| maintain, with github stars as a proxy for popularity:
|
| https://github.com/sfermigier/awesome-python-web-frameworks
|
| Note: as you probably know, popularity is not necessarily
| correlated with "actively maintained". For instance, Hug and
| Sanic are quite popular, but haven't seen a commit for quite a
| long time.
| keithasaurus wrote:
| There's a bunch of these kinds of html renderers. Here's mine:
| https://pypi.org/project/simple-html/
|
| But there are many others. Not sure I understand the point of
| async rendering, unless you want to mix IO with rendering? Which
| feels a bit too close to old PHP+html for my blood.
| guidopallemans wrote:
| What's wrong with the old PHP+html ways? It's one of the best
| toolchains to knock out a small to medium sized project. I
| guess that fundamentally, it's not scalable at all, or can get
| messy wrt closing tags and indenting. But with this approach I
| think you're good on both these aspects?
| johnisgood wrote:
| For websites you make for Tor, you would typically go for PHP
| or OpenResty, as it needs to be JavaScript-free. I personally
| aim for JavaScript-free projects regardless.
|
| Of course if you want client-side whatever, you need
| JavaScript.
| skeledrew wrote:
| JavaScript is optional even on the client side nowadays
| with the advent of PyScript via WASM, etc.
| rafram wrote:
| Not clear why HTML rendering needed to be infected with async.
| None of the example code has a clear need for async - even the
| `is_admin()` method would be a prefetched property in any
| reasonable database model.
| hansvm wrote:
| Your counterpoint still naturally involves something like async
| _somewhere_ (your proposal is just to move it out of the HTML
| rendering and into an initial data-gathering stage). If you
| accept that premise then the question is just where the async
| code goes.
|
| While on some level it makes sense for HTML rendering to be a
| pure function where the inputs are gathered from elsewhere
| (potentially asynchronously), it looks like htmy wants to make
| it easy to define hierarchies of components. Instead of
| `is_admin()`, imagine a dashboard whose layout is stored in a
| database, supporting configurable charts of various flavors.
| The heterogeneity of the data supporting different types of
| charts makes it hard to efficiently pull data in a single SQL
| query (equivalently, any reasonable database model), so
| somewhere in your code you're pulling a bunch of data
| asynchronously, and somewhere else you're rendering it. The
| question, still, is "where?"
|
| Going back to the idea of htmy defining hierarchies of
| components, imagine how annoying it would be to have to
| manually grab all the data for a "reporting page" component
| only to feed it straight back into the renderer -- either
| having to duplicate the hierarchial structure when feeding data
| into the renderer (a technique some UI libraries employ, though
| I don't like it) -- or having to come up with a method for
| flattening that hierarchy when instantiating the component
| (another technique some UI libraries employ, one I like more
| for small projects and less for large ones).
|
| They solve that (to the extent that you think it needs solving)
| by bundling all that background logic into the components
| themselves. Did they really need to implement that recursively
| instead of just walking the hierarchy, gathering the data up-
| front, and populating it? Eh. The code winds up being similar
| either way, and either way it definitely forces async back into
| the middle of HTML rendering.
|
| Mind you, that tends to either make some applications hard to
| build or to cause the framework to explode in complexity over
| time as people need new and new ways to say "yes, re-render
| this thing; no, re-render that other thing, but don't grab its
| data, ...." There's enough less particularly annoying code
| involved though that fat, smart components are a natural place
| for people to gravitate.
|
| Unrelated to htmy completely, a technique I like from time to
| time even for problems which don't need async per se (and I'm
| usually using lower-level languages, so the implementation is
| some sort of more manual continuation pattern, but all those
| things are basically async, so I won't dwell on the details) is
| explicitly designing pausable/restartable structures for long-
| running computations. It's about as easy to write as purely
| iterative code, and you can run the result as purely iterative
| with no runtime overhead, so the downsides are low. It opens
| the door though to easily tuning how long you defer invariant
| maintenance (too infrequent and your algorithm devolves to the
| slow thing it's replacing, too frequent and the overhead isn't
| worth it), easily checkpointing a computation, adding other
| custom runners for an algorithm (like animating its progress),
| .... I can absolutely see a use-case for wanting to visualize
| each step of an HTML rendering, or log OS network counters
| after each step, and so on. Python's async isn't really the
| right tool for the job for that (it's hard to modify the
| runtime to support them without building quite a lot of
| nonsense from scratch), but async in the abstract isn't bad at
| all per se.
| mattigames wrote:
| Imagine you have 2 big components, one fetches from an third-
| party API and the other from your backend, this way they can
| load at the same time instead of sequentially.
| ramon156 wrote:
| Because checking for two conditions is impossible? This seems
| like a solution for a non-existent problem. I could be
| missing something
| anentropic wrote:
| I was imagining more like you have a Django view that does
| all the async data fetching and then you hand off the results
| to a 'dumb' page component that does only rendering
|
| I guess the point is to have components know how to fetch
| their own data, particularly when combining with HTMX and
| having backend return page fragments that correspond to
| components. But maybe this makes more sense in React than it
| does when translating the pattern back to server-side?
|
| e.g. same author has this
| https://github.com/volfpeter/fasthx?tab=readme-ov-
| file#htmy-... which is doing that, but there's still a 'view'
| endpoint. Why not put the data fetch code there and have
| 'dumb' components that don't need to be async?
| mattigames wrote:
| It seems like the view endpoint would be for functionality
| shared the full view, like auth safeguards and such, while
| the components would fetch the data they need; this would
| make it so you don't need to pass around the data to the
| view and save a few lines of code; of course this is not
| compatible with the idea of having "dumb" components vs
| "logic" ones like people do in React and alike.
| volfpeter wrote:
| Components don't really need to fetch anything, they
| don't need to be smart. It's up to you where data
| fetching happens. If you look at fasthx for example,
| you'll see that routes/views normally handle your
| business logic and fasthx does the rendering (now with
| Jinja or htmy). With Jinja for example, it can only work
| like this. With htmy, you have more flexibility (which
| can be an advantage but of course it can also be
| misused).
|
| Async components can be handy for example when you need
| to load files. See the Snippet utility and the markdown
| support in the lib.
| https://volfpeter.github.io/htmy/examples/markdown/
| volfpeter wrote:
| You're right, fetching all the data (that you may or may
| not need during rendering) in advance is of course doable
| and quite common. That's what you do for example with tools
| like Jinja. That may or may not work well for your use-
| case.
|
| htmy does not force you to put data fetching or anything
| else into components (you can still have dumb components).
| It also doesn't force you to write async components. The
| most important thing it does is it gives you the option to
| build your application entirely in Python (no ugly custom
| templating language syntax with lack of static analysis or
| proper IDE support) and enables the use of modern async
| tools.
|
| And admittedly, the lib was built with FastAPI and HTMX in
| mind, which kind of necessitates async support...
| rafram wrote:
| But does it actually work that way? If I `await
| fetch_from_api()` in the first component before returning the
| tree with the second component that fetches from my backend,
| `fetch_from_api()` has to resolve before Htmy finds out about
| the second component.
| scotty79 wrote:
| Async infrastructure allows your stuff to be sync or async.
| While sync infrastructure forces your stuff to be sync.
|
| If anything sync (not async) infects everything you do.
|
| Of course it depends if you call the infrastructure (then it's
| better for it to be sync) of if the infrastructure calls you
| (then it's better to be async).
|
| Rendering engine is something you rarely call, but it often
| calls your functions.
| rafram wrote:
| Yes, and that's the _worst part_ of async. That's why you
| need to be very strategic about where you introduce it into
| your code in order to minimize the number of functions it
| infects, not give up and write a framework that's all async
| for no good reason.
|
| https://journal.stuffwithstuff.com/2015/02/01/what-color-
| is-...
| scotty79 wrote:
| Yes. But you should be equally strategic about introducing
| sync code into your platform. Because making your platform
| sync basically makes it only be able to call sync functions
| of your code.
|
| It's not that async infects. It's sync that infects and
| restricts. We are just used to it by default.
|
| The fact that we started from sync was the cause of all the
| trouble of rpc because everything outside of CPU is
| innately async.
|
| So make your utility functions sync whenever you can but
| make your platforms and frameworks async.
| rafram wrote:
| I just completely disagree. Async is syntactic sugar that
| can be reduced to sync code with callbacks. It doesn't
| exist on equal footing. If you want to call sync code
| from async code, you just... call it. If it performs
| blocking IO, it'll block, but that's exactly what it
| would do if called from other sync code, too.
|
| By contrast, calling async code from sync code requires a
| special blocking wrapper (Python) or unavoidably breaks
| control flow (JavaScript).
| scotty79 wrote:
| > By contrast, calling async code from sync code requires
| a special blocking wrapper (Python) ...
|
| That's exaclty my point. If you don't have async by
| default in your platform you need to do stupid things to
| fake it. If function calls and main in Python were
| innately async you could be calling async code just as
| easily as sync code.
|
| > [...] or unavoidably breaks control flow (JavaScript).
|
| async/await syntax avoids it completely.
|
| Tbh await should be default function call semantics and
| there should be special keyword for calling without
| awaiting. But since we come from sync primitives that
| would require small revolution that might happen at some
| point.
|
| > Async is syntactic sugar
|
| You could make sync code be syntactic sugar for await.
| gpderetta wrote:
| > would require small revolution that might happen at
| some point.
|
| or python could have blessed gevent and done away with
| all the nonsense.
| btown wrote:
| I hope that someone does an oral history of why gevent
| wasn't seen as the solution here. The existence of models
| like Twisted, and a general idea that yields to an event
| thread should be explicit in some way, I think caused the
| exact kind of fracturing of the ecosystem that everyone
| was trying to avoid. "Everyone will write async code"
| simply didn't happen in practice.
| crubier wrote:
| > Tbh await should be default function call semantics and
| there should be special keyword for calling without
| awaiting.
|
| Your comment made me realize this is exactly what golang
| "go" keyword does. This is actually great.
| koolba wrote:
| > Async infrastructure allows your stuff to be sync or async.
| While sync infrastructure forces your stuff to be sync.
|
| Is that specific to the threading model for Python?
|
| The reverse is true in nodejs where once you've got one async
| call, the entire chain must be async.
| scotty79 wrote:
| Python is the same as JS.
|
| Async function (that returns something you need) can be
| called only from async function. That's why autor of this
| specific rendering framework/lib chose it to be async. So
| that the user functions called in components can be either
| sync or async.
| volfpeter wrote:
| Thanks for this answer. Async support is handy if the
| framework in which you're using the tools is async (let's say
| FastAPI). See my answer to a similar question on reddit: http
| s://www.reddit.com/r/Python/comments/1fvv11p/comment/lqb...
| jackson928 wrote:
| Looks similar to a framework I've been using for some personal
| sites reflex.dev, pretty cool when would you recommend using this
| over that?
| 01HNNWZ0MV43FF wrote:
| Oh it's server side "rendering"?
| Jaxan wrote:
| To me "rendering engine" also means something else. Namely
| taking html and rendering it to the screen.
| zupa-hu wrote:
| Consider updating your vocabulary because the term is often
| used for both.
|
| Note that rendering to the "screen" really means writing bits
| at a memory range, which is just one interface for displaying
| things. Html is another, higher level interface these days.
| shkkmo wrote:
| "rendering engine" has a pretty clear meaning and is a
| pretty poor term to use for a system for tranforming one
| kind of text bits into another.
|
| Perhaps you should consider using less confusing
| terminology in your vocabulary?
|
| https://developer.mozilla.org/en-
| US/docs/Glossary/Engine/Ren...
|
| Edit: You say "often used for both" but I am struggling to
| find any other examples. Yhe closest I can find is this
| extremely poorly named static site generator project:
| https://github.com/render-engine/render-engine?tab=readme-
| ov...
|
| Edit2: Man, the appropriation of the term "rendering" by JS
| people has led to some pretty stupid stuff, like this
| statement: "SSR, short for Server-Side Rendering, is a
| technique in web development where the webpage's content is
| rendered on the server instead of the client's browser."
| Karellen wrote:
| https://en.wikipedia.org/wiki/Server-side_scripting#Server-s...
| eddautomates wrote:
| I think in almost-2025 any dataclass heavy library should
| probably use pydantic (or support it)
| tirpen wrote:
| Probably, but I fail to see how that's relevant here. This is
| not a "dataclass heavy" library in any sense, they just used
| dataclass in the examples to make them shorter.
|
| Based on everything I see in the documentation, you should be
| able to use Pydantic models as well, or standard python
| objects, or anything else, as long as it has a method `def
| htmy(self, context: Context) -> Component`.
| franga2000 wrote:
| Please don't! Pydantic demands 100% type correctness at runtime
| in a language that can't guarantee basically anything at
| "compile" (lint) time. Screw up one type annotation for one
| edge case and your entire system turns into one big
| ValidationError.
|
| Dataclasses let you return "incorrect" data and that's a good
| thing. I'd rather get an unexpected None here and there (which
| can be handled) than have library code crash because the wrong
| type snuck into a field I don't even care about.
|
| As for support, is any explicit support needed? You can
| Pydantic models into things expecting dataclasses and often the
| other way around too.
| worthless-trash wrote:
| Spoken like a true dynamic types programmer. Some programmers
| prefer having errors over these surprises.
| azinman2 wrote:
| But what do you do use to create dynamic updates on the client
| side? I'm guessing it still has JS and makes API calls, no? And
| if so, it seems easier (to me) to just do all of the rendering
| client side and let the backend just be REST queries.
| mattigames wrote:
| This is just the static html renderer, it has no JavaScript to
| update client side, but the author has another project for
| fastapi + this + htmx: https://github.com/volfpeter/fasthx
| liendolucas wrote:
| I can't clearly see a use case. I went on to the "why" section
| but I'm having a hard time trying to understand what this is
| trying to solve. Perhaps a clear and simple example to see why
| you would use it could be useful. Also I find it extremely
| verbose to write HTML the way is shown in the examples at the
| top. Having used Jinja for a very long time, its simplicity and
| separation from logic makes it almost (for me) the only
| templating lang that you need to learn in Python. Writing HTML
| code the way is shown is clearly not for me, but there might be
| uses cases for it.
| v3ss0n wrote:
| Sometimes I want to do things totally pure html, with more
| dynamicness and more reusability. Jinja template fall short.
| littlestymaar wrote:
| This! I recently wanted to get back to writing a web app
| entirely rendered on the server side without the need for a
| JavaScript framework and I was really struck by how
| embarrassingly clumsy templating engines are compared to JSX.
| volfpeter wrote:
| That's a fair point, although my feeling after working quite a
| bit with Jinja recently is the opposite (primarily for lack of
| static analysis and IDE support).
|
| You're right, for example the documentation should be improved
| quite a bit. Keep in mind that this project is pretty new, I
| simply had no time to add more docs or further improve the
| existing one.
|
| Ps.: with the Snippet utility and markdown support, you can
| actually write quite a bit of your HTML in a html files rather
| than Python. You could even use Jinja templates as the backend
| for some of your components. This part will see more work as I
| have spare time to work more on the project.
| voidUpdate wrote:
| Hypertext markup Yanguage?
| v3ss0n wrote:
| This is what I am looking for.When FastHTML was announced I
| expected to work like this one but it was with it's own
| webserver.
| DonnyV wrote:
| Rendering html is something that needs to happen within 300ms.
| Anything more and its perceived as lagging. So why would you
| choose python to do visual rendering?
| nickpsecurity wrote:
| It could be useful for content creators that value reusing
| their Python expertise over other factors. Also, many apps work
| better when every integrated component is written in the same
| language. Also, there's a lot of code in Python for or
| supporting web programming. Finally, if people use AI auto-
| complete, many people say they're more effective at common uses
| of Python vs other languages or situations.
|
| I've found the performance issue to be serious in some
| situations. Fortunately, there's a number of accelerators for
| Python code that boost its performance. They range from JIT's
| (eg PyPy) to custom VM's (eg Cinder) to writing fast paths in
| Cython to Python-to-C++ compilers (eg mycpp).
|
| So, you get the productivity and familiarity of Python with
| performance boosting in many use cases. If it doesn't work,
| then it's better to write that component as an extension in a
| systems language.
| globular-toast wrote:
| There is already htpy: https://htpy.dev/ I have used it in
| production and like it.
|
| For those asking the point is being able to do similar to React
| JSX components, but on the server side. It's so much nicer to use
| than templates like Django or Jinja (there might be other
| reasons, but this is quite clearly the goal of htpy and I assume
| this too).
|
| Just looking at this one briefly it seems to use magic methods on
| dataclasses. What's the advantage of that over just a function?
| Seems like unnecessary nesting.
| mixmastamyk wrote:
| I like the htmy method that an object can render itself, neat
| idea. But the extra classes for rows etc seems too bureaucratic.
| volfpeter wrote:
| I just noticed on Reddit that someone posted my package here. I
| see there are several comments already. I'll try to answer a few
| as I have time.
___________________________________________________________________
(page generated 2024-11-27 23:01 UTC)