Skip to content

Python

Mateu has a Python server-side implementation. You annotate plain Python classes, and the existing renderers (web and native) render them with zero client changes — exactly as they render the Java backend.

Coming from Java? The Language Rosetta maps every declaration idiom side by side, and the parity matrix shows exactly what this server supports today.

Every Mateu renderer speaks one protocol: POST /{baseUrl}/mateu/v3/sync/{route} in, a UIIncrement JSON tree out. So the Python side does not re-implement the whole framework — it emits the same JSON. FastAPI hosts the sync endpoint; a reflection mapper turns your annotated classes into the Mateu component tree; Pydantic v2 discriminated unions produce the type discriminators the renderers expect.

The implementation lives at backend/python (DESIGN.md for the plan, README.md for status). Its output was verified byte-identical to the C# reference for the same view.

Python attributes can’t carry C#-style attributes, so:

  • Field modifiers ride inside Annotated[T, Marker(...)].
  • Class and method features are decorators.
Terminal window
cd backend/python
python3 -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
uvicorn samples.demo.main:app --host 0.0.0.0 --port 8594 # serves the sync API
pytest # golden tests

Point any Mateu renderer at it — e.g. set the Compose app’s mateu.baseUrl=http://localhost:8594. The server binds to 0.0.0.0, so the iOS simulator and Android emulator reach it too.

Wire it up:

from fastapi import FastAPI
from mateu_fastapi import add_mateu
from samples.demo import views # a module holding your @ui / @app classes
app = FastAPI()
add_mateu(app, views)

A @ui class becomes a routed form. Public type-hinted fields become fields (the type is inferred: str, int→integer, float/Decimal→number, bool→boolean, date/datetime→date, Enum→ dropdown). Required() makes a field required and is enforced server-side. A @button method returning a Message shows a toast.

from typing import Annotated
from mateu_uidl import ui, title, subtitle, section, button, Required, Section, Message
@ui("person")
@title("Person")
@subtitle("Personal data")
class Person:
name: Annotated[str | None, Required(), Section("Identity")] = None
age: int = 0
subscribed: Annotated[bool, Section("Preferences")] = False
role: Role = Role.GUEST # Enum → dropdown
@button()
def save(self) -> Message:
return Message(f"Saved {self.name}")

Section("…") on the first field of a group starts a titled card that following fields join.

Derive from Crud[T] and override fetch (and, as needed, get / save / delete). You get a searchable listing — a table on desktop, cards on mobile — plus full detail / edit / new flows (routes /x, /x/{id}, /x/{id}/edit, /x/new), with Required() validation on save.

@ui("reservations")
@title("Reservations")
class Reservations(Crud[Reservation]):
def fetch(self, search):
return [r for r in store if not search or search in r.locator]
def get(self, id): return store_by_id.get(id)
def save(self, entity): store_by_id[entity.id] = entity
def delete(self, id): store_by_id.pop(id, None)

The listing renders a smart search bar whose filters come straight from the entity: enums become multi-selects (IN over the picked values), date/datetime fields become from–to date ranges, numerics annotated RangeFilter() become min–max ranges, and strings/bools/plain numbers keep single-value widgets. The values are applied automatically over what fetch returns — no filter code to write:

class Reservation:
id: str = ""
guest: str = ""
channel: Channel = Channel.WEB # multi-select filter
arrival: date = date(2026, 1, 1) # date-range filter
total: Annotated[float, RangeFilter()] = 0.0 # number-range filter

An @app class is the application shell; each @menu_item method contributes a menu entry that navigates to the view it returns (read from the method’s return annotation).

@app("My Python Mateu app")
class DemoApp:
@menu_item("Reservations")
def reservations(self) -> Reservations: return Reservations()
@menu_item("Person")
def person(self) -> Person: return Person()

Derive from Wizard, tag each field with Step(n), and implement complete(). Mateu renders a progress bar plus Back/Next; the step + field values round-trip through component state.

@ui("signup")
@title("Sign up")
class SignupWizard(Wizard):
email: Annotated[str | None, Step(1)] = None
password: Annotated[str | None, Step(2), Password()] = None
def complete(self) -> Message:
return Message(f"Welcome {self.email}")

Tag consecutive fields with Tab("Name") to group them into a tab strip. Field stereotypes:

MarkerEffect
Multiline()multi-line text area
Password()password input
Money()tagged money so the renderer formats it as currency
PlainText()read-only plain text (also valid class-level via @plain_text)
Stereotype("…")an explicit stereotype
class Profile:
name: Annotated[str | None, Tab("Identity")] = None
secret: Annotated[str | None, Tab("Identity"), Password()] = None
bio: Annotated[str | None, Tab("About"), Multiline()] = None
salary: Annotated[float, Tab("About"), Money()] = 0.0
from mateu_uidl import kpi, fab, shortcut, button, compact, confirm_on_navigation_if_dirty
@ui("dashboard")
@compact # high-density rendering
@confirm_on_navigation_if_dirty # warn before leaving with unsaved changes
class Dashboard:
@kpi("Open tickets")
def open_tickets(self) -> str: return "42"
@fab("plus", "Add", 0) # floating action button
def add(self) -> Message: return Message("Added")
@button()
@shortcut("ctrl+s") # keyboard shortcut
def save(self) -> Message: return Message("Saved")

Dashboards, foldouts & UX-pattern components

Section titled “Dashboards, foldouts & UX-pattern components”

The UX-pattern components from the Java backend are available as fluent components in mateu_uidl.components — they emit the exact same wire JSON, so every renderer that supports them renders the Python backend unchanged:

ComponentPurpose
MetricCard (+ MetricTrend)KPI tile: title, value, unit, trend (up/down/neutral), drill-in action_id
Scoreboardhorizontal band of metric cards
DashboardPaneltitled tile wrapping any component; col_span/row_span
DashboardLayoutresponsive dashboard grid (columns=0 = auto-fit)
FoldoutPanel / FoldoutLayoutRedwood-style foldout: fixed overview + lateral fold-out panels
HeroSectionbig page hero: title, subtitle, background image, slotted content
EmptyStatefriendly “nothing here yet” placeholder with an optional call-to-action
Skeleton (+ SkeletonVariant)shimmering loading placeholder (text/card/grid/form)
Gantt / GanttTaskread-only Gantt/timeline chart (ISO dates, progress 0–100, color)

The declarative archetypes compose them from your fields, exactly like the Java Dashboard/Foldout/ItemOverview/Welcome orchestrators. Declare type-hinted fields holding components; mark titled panels with Panel(...) inside Annotated[...] (the analogue of Java’s @Panel):

from datetime import date
from typing import Annotated
from mateu_uidl import ui, title, Dashboard, Panel
from mateu_uidl.components import MetricCard, MetricTrend, Gantt, GanttTask
@ui("dashboard")
@title("Sales dashboard")
class SalesDashboard(Dashboard):
# consecutive MetricCards group into a full-width Scoreboard KPI band
revenue: MetricCard = MetricCard(title="Revenue", value="1.2", unit="M€",
trend=MetricTrend.up, action_id="openRevenue")
incidents: MetricCard = MetricCard(title="Incidents", value="3", trend=MetricTrend.down)
# Panel(...) fields become titled tiles on a responsive grid
plan: Annotated[Gantt, Panel("Rollout plan", col_span=2)] = Gantt(tasks=(
GanttTask(id="t1", title="Design", start=date(2026, 7, 1), end=date(2026, 7, 20),
progress=80.0),
))
def open_revenue(self): # runs when the Revenue tile is clicked
...
  • Dashboard — consecutive MetricCard fields → Scoreboard band; Panel(...) fields → titled tiles; other component fields land on the grid as-is. Override columns() to fix the column count.
  • Foldout — the first component field without Panel is the always-visible overview; Panel(title, subtitle, icon, open) fields are lateral fold-out panels.
  • ItemOverview — the first component field without Panel is the key-info panel (left, sticky); Panel(title) fields become tabs on the right. Override panel_width().
  • Welcome — fluent Button fields become hero call-to-action buttons; Panel(title) fields become highlight tiles below. Override hero_title() / hero_subtitle() / hero_image().

For full control, subclass ComponentTreeSupplier and return any fluent component tree:

from mateu_uidl import ui, title, ComponentTreeSupplier
from mateu_uidl.components import Gantt, GanttTask
@ui("project-plan")
@title("Project plan")
class ProjectPlan(ComponentTreeSupplier):
def component(self) -> Gantt:
return Gantt(tasks=(
GanttTask(id="a", title="Analysis", start=date(2026, 1, 7), end=date(2026, 2, 1),
progress=100.0),
GanttTask(id="b", title="Build", start=date(2026, 2, 1), end=date(2026, 5, 1)),
))

Action ids referenced by MetricCard, EmptyState or Button components are advertised automatically and dispatch to the method of the same (camelCased) name.

  • @subtitle("…") — a subtitle under the page title.
  • @banner(BannerTheme.INFO, "Title") on a method — a banner below the header; if the method returns a string, that’s the description. Themes: INFO, SUCCESS, WARNING, DANGER.
  • HeaderBadge(color="success") in a field’s Annotated[...] — a status chip in the header strip.
  • i18n — subclass Translator, override translate, and pass it to add_mateu(..., translator=…).
  • Events@emits("event-name") advertises an event; @subscribe_to("event", "action") runs action when that event fires.
  • Security@secured("permission") marks a view as requiring a permission.
class UpperTranslator(Translator):
def translate(self, key: str) -> str: return key.upper()
Section titled “Navigation links, radio groups & adaptive layout”

Annotated[str, LinkTo("/customers/${state.customerId}")] puts a navigation icon on a field (templates interpolate client-side); give the view a link(member_name) method for runtime decisions. UseRadioButtons() forces an enum to render as a radio group, and @auto_layout enables the adaptive layout inference (small enums become radios, long forms fold, section-heavy forms become tabs) — the same heuristics as the Java server.

An @app_context method of the app class becomes a selector on the app header that fixes a value for EVERY screen (the active hotel, the company…) — return the options as Option objects or (value, label) pairs, or annotate the return type as an Enum. The picked value travels in the app_state of every request:

@app("Backoffice")
class BackofficeApp:
@app_context("Hotel")
def hotel(self):
return [(h.id, h.name) for h in hotels]

Annotated[str, Signature()] renders a drawing pad (the accepted strokes land in the value as a PNG data URI) and Annotated[str, PhotoCapture()] a camera capture (JPEG data URI) — no upload endpoint, the image travels in the string. Annotated[str, TreeSelect(leaves_only=True)] unfolds the field’s dropdown as a TREE; the hierarchy comes from the view’s options(field_name) method returning options with children:

@ui("checkin")
class CheckIn:
guest_signature: Annotated[str, Signature()] = ""
document_photo: Annotated[str, PhotoCapture()] = ""
zone: Annotated[str, TreeSelect()] = ""
def options(self, field_name):
if field_name == "zone":
return [
Option(value="es", label="Spain", children=[Option(value="mca", label="Mallorca")]),
Option(value="pt", label="Portugal"),
]
return []

Under PEP 649 annotations evaluate lazily and are no longer eagerly stored in __dict__['__annotations__']. Mateu reads them via inspect.get_annotations(...), so the field markers work on Python 3.11 through 3.14+.

Forms + sections + field types + validation, Crud[T] (list / detail / edit / new / save / delete), the @app shell + menu navigation, wizards, page decorations, tabs, stereotypes, KPIs, FABs, shortcuts, compact, the unsaved-changes guard, i18n, events, security scaffolding, and the UX-pattern components (MetricCard/Scoreboard/DashboardPanel/DashboardLayout, FoldoutLayout, HeroSection, EmptyState, Skeleton, Gantt) with the Dashboard/Foldout/ItemOverview/Welcome declarative archetypes. 43 golden-JSON tests assert wire compatibility; the live showcase view is byte-identical to the C# reference.