Crash course: Python developer, new to the web

You can write Python; you have never built a web page. This page teaches the four web concepts you need, shows how Rational Web expresses each one in Python, and ends with a working app you wrote while reading. Nothing here requires knowing HTML, CSS or JavaScript first.

What actually happens when someone visits your site

  1. Their browser asks your server for an address, like GET /profile.

  2. Your server runs your Python code and produces a page: HTML, the text-and-structure format browsers render.

  3. The browser draws it. If the page needs to react to clicks, a very small script (Rational Web ships it for you) tells your server “the user clicked this”, and your Python runs again.

That’s the loop: browser asks, Python answers. Everything below is a variation on it.

Concept 1 — Pages: code that answers an address

from rational import App, Heading, Page, Text

app = App(title="My site")


@app.page("/")
class Home(Page):
    def render(self):
        return Heading("Hello, world", level=1)


@app.page("/user/{name}")          # /user/ada, /user/grace, ...
class Profile(Page):
    name: str                       # validated from the URL

    def render(self):
        return Heading(f"Hello, {self.name}", level=1)

if __name__ == "__main__":
    app.run()                       # run: python app.py --dev

render() returns the page’s content as Python objects — Heading, Text, Button. The framework turns them into HTML. There is one object per HTML building block; you learn them as you need them, and View page source in the browser always shows you what your objects became.

Concept 2 — Interactivity: Python functions called by clicks

A page that just displays is done: it ships zero JavaScript. To react to the user, connect an event handler — a normal method, but it runs on your server when the user acts:

from rational import Button, Column, Page, State, Text


@app.page("/")
class Counter(Page):
    count = State(0)                # "remember this between clicks"

    def increment(self):
        self.count += 1

    def render(self):
        return Column(
            Text(f"Count: {self.count}"),
            Button("Add one", on_click=self.increment),
        )

State is the memory of the page. When a handler changes it, the parts of the page that used it are re-rendered on the server and updated in the browser — focus and typing are preserved. You never write the update step; assigning to State is the whole job.

Forms work the same way, and their input is checked before your code sees it:

from pydantic import BaseModel, Field


class JoinForm(BaseModel):
    name: str = Field(min_length=1, max_length=40)


# inside a page:
#     Form(Input("Your name", name="name"), Button("Join", type="submit"),
#          on_submit=self.join)
async def join(self, form: JoinForm):
    ...   # form.name is a valid, trimmed string — or the user saw a message already

Concept 3 — Remembering things: the database

Visitors come back; pages forget. Data you keep between requests lives in a database — you talk to it with SQLAlchemy, and Rational Web wraps the async parts:

# models/database.py — one engine for the whole app
from rational.backend import Database

database = Database("sqlite+aiosqlite:///app.db")


# models/entry.py — a table is a class; a row is an instance
class Entry(database.Base):
    id: Mapped[int] = mapped_column(primary_key=True)
    text: Mapped[str] = mapped_column(String(280))

Then a service — your rules, in one place — reads and writes it:

from rational.backend import Service, remote


class GuestbookService(Service):
    @remote(method="GET")
    async def entries(self) -> list[Entry]:
        """Every entry, newest first."""
        ...

Pages call services through self.use(GuestbookService); loading data belongs in async def mount(self), which runs before the first render:

class Home(Page):
    entries = State(factory=list)

    async def mount(self):
        self.entries = await self.use(GuestbookService).entries()

Concept 4 — Making it look right: styles

CSS (the web’s styling language) is expressed here as Style values — Python objects, composed with |:

from rational import Style

CARD = Style(padding="md", shadow="lg") | Style(border_left="4px solid var(--rw-color-primary)")

Card(..., style=CARD)

Named sizes ("md", "lg"), theme colours, viewport={"narrow": ...} for phones — the framework compiles clean CSS from it and reuses what it has already emitted. Start with the default theme; restyle later by changing values, not by learning a stylesheet language first.

Your first app, in one file

# app.py — run with: python app.py --dev
from rational import App, Button, Card, Column, Heading, Input, Page, State, Text

app = App(title="Notes")


@app.page("/")
class Home(Page):
    notes = State(factory=list)

    def add(self, value: str):          # wired to the input; runs on the server
        self.notes = [*self.notes, value]

    def render(self):
        return Column(
            Heading("My notes", level=1),
            Card(
                Input("New note", on_input=lambda v: None),   # display only, see below
                Button("Add", on_click=lambda: self.add("first note")),
            ),
            *[Text(n) for n in self.notes],
            max_width=560,
        )


if __name__ == "__main__":
    app.run()

Save it, run python app.py --dev, open http://127.0.0.1:8000. Edit the file: the server restarts and the browser reloads by itself; errors are shown with the failing line of your code and a hint. When it works, replace the input-and-button with the Form pattern above to get validation, then follow the tutorial to add the database.

Words you’ll meet

HTML

The text format pages are written in; you produce it through Python objects.

route

An address (/, /user/ada) and the code that answers it.

request / response

One browser question and your server’s answer.

session

The server’s memory of one visitor (“who is signed in”), keyed by a cookie the browser returns automatically. Rational Web creates and protects it for you.

API

Your service methods, callable as JSON over HTTP — free, documented at /api/docs.

CSS

The language of visual style; here, values you pass as style=.

Where to go next