mesop Documentation

Rapidly build AI apps in Python

6,582 stars Python Python 108 files · ~64,121 tokens
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy mesop on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name mesop -p 32123:32123 --restart always mesop-dev/mesop:latest
Configured Services & Ports
⚙️ mesop
image: build locally
🔌 Ports: 32123:32123
docker-compose.yml
yaml (docker-compose.yml)
services:
  mesop:
    build: .
    volumes:
      - .:/workspaces/mesop
      - node_modules:/workspaces/mesop/node_modules
      - vscode_extensions:/home/mesop-dev/.vscode-server/extensions
    ports:
      - '32123:32123'
    tty: true

volumes:
  # Store node modules on volume for better performance. Also prevents overwriting node
  # node modules installed on host machine via bind mount.
  node_modules:
  vscode_extensions:

Technical Documentation: mesop-dev/mesop

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`README.md`)

Mesop: Build delightful web apps quickly in Python 🚀

[!WARNING]
Mesop is no longer maintained as of September 30, 2026. No further updates, bug fixes, or support will be provided.



Used at Google for rapid internal app development

Mesop is a Python-based UI framework that allows you to rapidly build web apps like demos and internal apps:

Intuitive for UI novices ✨

  • Write UI in idiomatic Python code
  • Easy to understand reactive UI paradigm
  • Ready to use components

Frictionless developer workflows 🏎️

  • Hot reload so the browser automatically reloads and preserves state
  • Rich IDE support with strong type safety

Flexible for delightful demos 🤩

  • Build custom UIs without writing Javascript/CSS/HTML
  • Compose your UI into components, which are just Python functions

Write your first Mesop app in less than 10 lines of code...

Demo app

python
import mesop as me
import mesop.labs as mel


@me.page(path="/text_to_text", title="Text to Text Example")
def app():
  mel.text_to_text(
    upper_case_stream,
    title="Text to Text Example",
  )


def upper_case_stream(s: str):
  return "Echo: " + s.capitalize()

Try it

Colab

You can try Mesop on Colab!

Locally

Step 1: Install it

sh
pip install mesop

Step 2: Copy the example above into main.py

Step 3: Run the app

sh
mesop main.py

Learn more in Getting Started.

Reporting a security issue

If you identify a security vulnerability with Mesop, please file a GitHub security issue and we will promptly respond to it.

Disclaimer

This is not an officially supported Google product.


Overview (`.yarn/README.md`)

Yarn Vendoring

We utilize Yarn's yarn-path configuration in a shared .yarnrc file to enforce
everyone using the same version of Yarn. Yarn checks the .yarnrc file to
determine if yarn should delegate the command to a vendored version at the
provided path.

How to update

To update to the latest version of Yarn as our vendored version:

  • Run this command
sh
yarn policies set-version latest
  • Remove the previous version

Overview (`demo/README.md`)


Mesop demo app

This app demonstrates Mesop's various components and features. Create your own Cloud Run app by following: https://mesop-dev.github.io/mesop/guides/deployment/

Development

Setup

From workspace root:

sh
rm -rf demo/venv && \
virtualenv --python python3 demo/venv && \
source demo/venv/bin/activate && \
pip install -r demo/requirements.txt

Run

  1. cd demo
  2. mesop main.py

Generate screenshots

If you add more demos and want to re-generate screenshots, do the following steps:

  1. in demo/screenshot.ts change test.skip to test
  2. Run: yarn playwright test demo/screenshot.ts
  3. Install cwebp using brew install webp or download from here.
  4. From the workspace root, run:
sh
`for file in demo/screenshots/*; do cwebp -q 50 "$file" -o "${file%.*}.webp"; done`

Deployment

Pre-requisites:

  • Make sure you generate screenshots before deploying!
  • Ensure a recent version of Mesop has been published to pip, otherwise the demos may not work (because they rely on a new API).

Deploy to Cloud Run

This app is deployed to Google Cloud Run.

sh
gcloud run deploy mesop --source .

See our Mesop deployment docs for more background.

Deploy to Hugging Face Spaces

NOTE: You need to update demo/requirements.txt to point to the latest Mesop version because Hugging Face Spaces may use a cached version of Mesop which is too old.

NOTE: You'll need to install git xet to binary images. We need this for uploading screenshots. See https://huggingface.co/docs/hub/xet/using-xet-storage#using-git-xet.

sh
./demo/deploy_to_hf.sh ../hf_demo

You can change ../hf_demo to any dir path outside of your Mesop repo.

Note: if you get an error in Hugging Face Spaces "No app file", then you can create an "app.py" file in the Spaces UI to manually trigger a build. This seems like a bug with Hugging Face.


Overview (`scripts/README.md`)

Scripts

All these scripts should be executed from the root directory of the Git repo.

Example shell script usage:

shell
scripts/${script_name}.sh

Example Python script usage:

shell
python scripts/scaffold_component.py ${component_name}

Overview (`showcase/README.md`)


title: Mesop Showcase emoji: 👓 colorFrom: red colorTo: yellow sdk: docker pinned: false license: apache-2.0 app_port: 8080


Overview (`tools/README.md`)

Tools

The files in this subtree are forked from: https://github.com/angular/components/tree/ff67a416d19e9237607605bec0d7cc372025387f/tools

These tools provide Bazel tooling support for Angular.


Overview (`build_defs/app_bundle/README.md`)

Forked from https://github.com/angular/dev-infra/tree/301473cb5fbcd6cca8b70a6369dcdf3dc2d12e08/bazel/app-bundling


Overview (`mesop/pip_package/README.md`)

Mesop

Mesop is an open-source package to build UIs in Python.

Source code: github.com/mesop-dev/mesop


Overview (`mesop/examples/web_component/firebase_auth/README.md`)

README

sh
gcloud config set project mesop-auth-test
sh
gcloud auth application-default login

Index (`docs/index.md`)


hide:

  • navigation
  • toc

Rapidly build AI apps in Python
text
Create web apps without the complexity of frontend development.
Used at Google for rapid AI app development.


<a class="primary-button" href="./getting-started/installing">Get started</a>
<a class="secondary-button" href="./showcase">View showcase</a>
text
Create your AI chat app in minutes


  
  
      
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
              
          </svg>
          Open in new tab
Python-native
text
Write your UI in idiomatic Python


With Mesop, you can leverage your existing Python expertise to build UIs effortlessly. No new languages to learn - just write clean, idiomatic Python using familiar constructs like functions, loops, and conditionals.

=== "State management"

text
``` python
@me.stateclass
class State:
  val: str

@me.page()
def page():
    state = me.state(State)
    me.text(state.val)
```

=== "Control flow"

text
``` python
@me.stateclass
class State:
  is_loaded: bool

@me.page()
def page():
    if me.state(State).is_loaded:
      me.text("Loaded")
    else:
      me.progress_spinner()
```

=== "Streaming UI"

text
```python
--8<-- "mesop/examples/docs/streaming.py"
```

=== "Composable"

text
``` python
@me.content_component
def scaffold(url: str):
  with me.box():
    menu(url=url)
    me.slot()

@me.component
def menu(url: str):
  ...

def page1():
  with scaffold(url="/page1"):
    some_content(...)
```
Modern UI principles
text
Declarative UI that's easy to understand


Mesop streamlines UI development with a declarative approach. Build expressive, maintainable interfaces using battle-tested patterns in Python. Say goodbye to complex imperative logic and hello to intuitive, clean code.
py
@me.stateclass
class State:
    image_data: str
    detections: list[Detection]


@me.page()
def object_detector():
    state = me.state(State)

    me.text("Real-time Object Detection", type="headline-4")
    me.uploader(label="Upload an image", on_upload=on_image_upload)

    if state.image_data:
        me.image(src=f"data:image/jpeg;base64,{state.image_data}")

    if state.detections:
        me.text("Detected Objects:", type="headline-5")
        for detection in state.detections:
            detection_component(detection)

def detection_component(detection):
    me.text(f"{detection.obj}: {detection.confidence:.2f}")

def on_image_upload(e: me.UploadEvent):
    state = me.state(State)
    state.image_data = base64.b64encode(e.file.read()).decode()
    state.detections = detect_objects(e.file)
Building blocks
text
Jumpstart with ready-to-use components


Mesop provides a versatile range of [30 components](./components/), from low-level building blocks to high-level, AI-focused components. This flexibility lets you rapidly prototype ML apps or build custom UIs, all within a single framework that adapts to your project's use case.

<a class="section-button" href="./components/chat">
  AI components
  e.g. chat, text to image
</a>
<a class="section-button" href="./components/input">
  Form components
  e,g. input, checkbox, radio
</a>
<a class="section-button" href="./components/table">
  Data display components
  e,g. table, plot
</a>
Build anything
text
Build any user interface you can imagine

With Mesop, you can build virtually any web-based user interface or application you can imagine. From quick prototypes to enterprise tools, Mesop provides the customizability to bring your ideas to life.

Extensible

Seamlessly integrate JS with web components

text
Get the best of both worlds with Mesop [web components](./web-components/). Leverage Python's simplicity for core logic, while accessing the vast ecosystem of JS libraries

=== "counter_component.py"

text
```python
--8<-- "mesop/examples/web_component/quickstart/counter_component.py"
```

=== "counter_component.js"

text
```python
--8<-- "mesop/examples/web_component/quickstart/counter_component.js"
```
Simple deployment
text
Deploy your app and share in minutes

Mesop streamlines cloud deployment, enabling you to share your AI application with the world in minutes. With step-by-step guides for deploying to Google Cloud Run or any cloud service that takes a container, you can go from local development to production-ready deployment without wrestling with complex server setups.

text
Google Cloud Run
  Free for small apps
  
</a>
Developer experience
text
Delightful developer experience

Mesop streamlines app development with features like hot reload and strong IDE support with static types, eliminating friction and boosting productivity.

text
Instant hot reload





  IDE support with static types
Community
text
See what others are saying

Join developers around the world who are building AI apps in Mesop.

text
is this the thing that will finally save me from ever learning front
      end?[https://t.co/eDgY0AfG6U](https://t.co/eDgY0AfG6U)
    
    &mdash; xlr8harder (@xlr8harder)
    <a
      href="https://twitter.com/xlr8harder/status/1798673386425786724?ref_src=twsrc%5Etfw"
      >June 6, 2024</a
    >
  
  <script
    async
    src="https://platform.twitter.com/widgets.js"
    charset="utf-8"
  ></script>

  
    
      i hate writing frontend code, but can’t resist a sleek UI. just
      tried Google’s quietly released Mesop—what a find! no frontend fuss,
      just python. if you value your sanity and good design,
      you should def try it. gives a balance between streamlit and
      gradiomesop docs :…
      [pic.twitter.com/SmBAH5Leri](https://t.co/SmBAH5Leri)
    
    &mdash; Sanchay Thalnerkar (@7anchay)
    <a
      href="https://twitter.com/7anchay/status/1804890091816644906?ref_src=twsrc%5Etfw"
      >June 23, 2024</a
    >
  
  <script
    async
    src="https://platform.twitter.com/widgets.js"
    charset="utf-8"
  ></script>

  
    
      New Python-based UI framework in town - have you tried Mesop from
      Google?I gave it a spin, here&#39;s what I found out:📌
      Provides a unique approach to building web hashtag#UIs in
      hashtag#Python with component-based architecture for customized UIs
      by taking inspiration from…
    
    &mdash; Harshit Tyagi (@dswharshit)
    <a
      href="https://twitter.com/dswharshit/status/1800557173073691000?ref_src=twsrc%5Etfw"
      >June 11, 2024</a
    >
  
  <script
    async
    src="https://platform.twitter.com/widgets.js"
    charset="utf-8"
  ></script>

Disclaimer

This is not an officially supported Google product.


Codelab Index (`docs/codelab/index.md`)

Mesop DuoChat Codelab

This tutorial walks you through building DuoChat, an interactive web application for chatting with multiple AI models simultaneously. You'll learn how to leverage Mesop's powerful features to create a responsive UI and integrate with AI APIs like Google Gemini and Anthropic Claude.

What you will build

By the end of this codelab, you will build DuoChat (demo) that will allow users to:

  • Select multiple AI models to chat with
  • Compare responses from different models side-by-side
  • Provide their own API keys

If you want to dive straight into the code, you can look at the DuoChat repo and each branch represents the completed code after each section.

Setting Up the Development Environment

Let's start by setting up our development environment:

  1. Create a new directory for your project:
bash
mkdir duochat
cd duochat
  1. Follow the Mesop command-line installation guide and create a virtual environment and activate it.

  2. Create a requirements.txt file with the following content:

text
mesop
gunicorn
anthropic
google-generativeai
  1. Install the dependencies:
bash
pip install -r requirements.txt

Setting Up the Main Application

Let's start by creating a basic Mesop application. Create main.py and add the following code:

python title="main.py"
import mesop as me

@me.page(path="/")
def page():
    me.text("Welcome to DuoChat!")

This creates a simple Mesop application with a welcome message.

Running the Application

To run your Mesop application:

bash
mesop main.py

Navigate to http://localhost:32123 in your web browser. You should see the welcome message.

Getting API keys

Later on, you will need API keys to call the respective AI models:

TIP: You can get started with the Gemini API key, which has a free tier, first and create the Anthropic API key later.

Troubleshooting

If you're having trouble, compare your code to the solution.

Next Steps

In the next section, we'll start building the user interface for DuoChat, including the header, chat input area, and basic styling. We'll explore Mesop's components and styling system to create an attractive and functional layout.

Building the basic UI

Components Index (`docs/components/index.md`)

Components

Please read Core Concepts before this as it explains the basics of components. This page provides an overview of the different types of components in Mesop.

Types of components

Native components

Native components are components implemented using Angular/Javascript. Many of these components wrap Angular Material components. Other components are simple wrappers around DOM elements.

If you have a use case that's not supported by the existing native components, please file an issue on GitHub to explain your use case. Given our limited bandwidth, we may not be able to build it soon, but in the future, we will enable Mesop developers to build their own custom native components.

User-defined components

User-defined components are essentially Python functions which call other components, which can be native components or other user-defined components. It's very easy to write your own components, and it's encouraged to split your app into modular components for better maintainability and reusability.

Web components

Web components in Mesop are custom HTML elements created using JavaScript and CSS. They enable custom JavaScript execution and bi-directional communication between the browser and server. They can wrap JavaScript libraries and provide stateful client-side interactions. Learn more about web components.

Content components

Content components allow you to compose components more flexibly than regular components by accepting child(ren) components. A commonly used content component is the button component, which accepts a child component which oftentimes the text component.

Example:

python
with me.button():
  me.text("Child")

You can also have multiple content components nested:

python
with me.box():
  with me.box():
    me.text("Grand-child")

Sometimes, you may want to define your own content component for better reusability. For example, let's say I want to define a scaffold component which includes a menu positioned on the left and a main content area, I could do the following:

python
@me.content_component
def scaffold(url: str):
  with me.box(style=me.Style(background="white")):
    menu(url=url)
    with me.box(style=me.Style(padding=me.Padding(left=MENU_WIDTH))):
      me.slot()

Now other components can re-use this scaffold component:

python
def page1():
  with scaffold(url="/page1"):
    some_content(...)

This is similar to Angular's Content Projection.

Advanced content component usage

Multi-slot projection

Mesop supports multi-slot projection using named slots.

Here is an example:

python
@me.slotclass
class LayoutSlots:
  header: me.NamedSlot
  content: me.NamedSlot
  footer: me.NamedSlot

@me.content_component(named_slots=LayoutSlots)
def layout():
  with me.box(style=me.Style(background="black")):
    me.slot("header")
  with me.box(style=me.Style(background="white")):
    me.slot("content")
  with me.box(style=me.Style(background="black")):
    me.slot("footer")

Now other components can re-use this layout component:

python
def page1():
  with layout() as c:
    with c.header():
      me.text("Header")
    with c.content():
      me.text("Content")
    with c.footer():
      me.text("Footer")

Composed content components

Content components can also use other content components, but you need to be careful since
slot rendering cannot be deferred to the parent component.

???+ failure "Slot rendering cannot be deferred by setting another slot."

text
```python
@me.content_component
def inner():
    me.slot()

@me.content_component
def outer():
  with inner():
    me.slot()
```

???+ success "Content components can use content components so long as the slots get rendered by the parent content component."

text
```python
@me.content_component
def header(background_color: str):
  with me.box(style=me.Style(background=background_color)):
    me.slot()


@me.content_component
def footer(background_color: str):
  with me.box(style=me.Style(background=background_color)):
    me.slot()


@me.content_component()
def content_layout():
  with header(background_color="black"):
    me.text("Header")
  with me.box(style=me.Style(background="white")):
    me.slot()
  with footer(background_color="red")
    me.text("Footer")
```

Now other components can re-use this `content_layout` component:

```python
def page1():
  with content_layout():
    me.text("Content")
```

Component Key

Every native component in Mesop accepts a key argument which is a component identifier. This is used by Mesop to tell Angular whether to reuse the DOM element.

Resetting a component

You can reset a component to the initial state (e.g. reset a select component to the unselected state) by giving it a new key value across renders.

For example, you can reset a component by "incrementing" the key:

py
class State:
  select_menu_key: int

def reset(event):
  state = me.state(State)
  state.select_menu_key += 1

def main():
  state = me.state(State)
  me.select(key=str(state.select_menu_key),
            options=[me.SelectOption(label="o1", value="o1")])
  me.button(label="Reset", on_click=reset)

Event handlers

Every Mesop event includes the key of the component which emitted the event. This makes it useful when you want to reuse an event handler for multiple instances of a component:

py
def buttons():
  for fruit in ["Apple", "Banana"]:
    me.button(fruit, key=fruit, on_click=on_click)

def on_click(event: me.ClickEvent):
  fruit = event.key
  print("fruit name", fruit)

Because a key is a str type, you may sometimes want to store more complex data like a dataclass or a proto object for retrieval in the event handler. To do this, you can serialize and deserialize:

py
import json
from dataclasses import dataclass

@dataclass
class Person:
  name: str

def buttons():
  for person in [Person(name="Alice"), Person(name="Bob")]:
    # serialize dataclass into str
    key = json.dumps(person.asdict())
    me.button(person.name, key=key, on_click=on_click)

def on_click(event: me.ClickEvent):
  person_dict = json.loads(event.key)
  # modify this for more complex deserialization
  person = Person(**person_dict)

!!! Tip "Use component key for reusable event handler"

text
This avoids a [subtle issue with using closure variables in event handlers](../guides/interactivity.md#avoid-using-closure-variables-in-event-handler).

Getting Started Quickstart (`docs/getting-started/quickstart.md`)

Quickstart

Let's build a simple interactive Mesop app.

Before you start

Make sure you've installed Mesop, otherwise please follow the Installing Guide.

Starter kit

The simplest way to get started with Mesop is to use the starter kit by running mesop init. You can also copy and paste the code.

python
--8<-- "mesop/examples/starter_kit/starter_kit.py"

Running a Mesop app

Once you've created your Mesop app using the starter kit, you can run the Mesop app by running the following command in your terminal:

sh
mesop main.py

If you've named it something else, replace main.py with the filename of your Python module.

Open the URL printed in the terminal (i.e. http://localhost:32123) in the browser to see your Mesop app loaded.

Hot reload

If you make changes to the code, the Mesop app should be automatically hot reloaded. This means that you can keep the mesop CLI command running in the background in your terminal and your UI will automatically be updated in the browser.

You can also manually trigger a hot reload using a keyboard shortcut:

  • MacOS: Cmd + Shift + R
  • ChromeOS: Alt + Shift + R
  • Windows/Linux: Ctrl + Shift + R

Next steps

Learn more about the core concepts of Mesop as you learn how to build your own Mesop app:

Core Concepts

Web Components Index (`docs/web-components/index.md`)

Web Components

Note: Web components are a new experimental feature released under labs and may have breaking changes.

Mesop allows you to define custom components with web components which is a set of web standards that allows you to use JavaScript and CSS to define custom HTML elements.

Use cases

  • Custom JavaScript - You can execute custom JavaScript and have simple bi-directional communication between the JavaScript code running in the browser and the Python code running the server.

  • JavaScript libraries - If you want to use a JavaScript library, you can wrap them with a web component.

  • Rich-client side interactivity - You can use web components to deliver stateful client-side interactions without a network roundtrip.

Anatomy of a web component

Mesop web component consists of two parts:

  • Python module - defines a Python API so that your Mesop app can use the web component seamlessly.
  • JavaScript module - implements the web component.

Next steps

Learn how to build your first web component in the quickstart page.


Web Components Quickstart (`docs/web-components/quickstart.md`)

Quickstart

Note: Web components are a new experimental feature released under labs and may have breaking changes.

You will learn how to build your first web component step-by-step, a counter component.

Although it's a simple example, it will show you the core APIs of defining your own web component and how to support bi-directional communication between the Python code running on the server and JavaScript code running on the browser.

Python module

Let's first take a look at the Python module which defines the interface so that the rest of your Mesop app can call the web component in a Pythonic way.

python title="counter_component.py"
--8<-- "mesop/examples/web_component/quickstart/counter_component.py"

The first part you will notice is the decorator: @me.web_component. This annotates a function as a web component and specifies where the corresponding JavaScript module is located, relative to the location of this Python module.

We've defined the function parameters just like a regular Python function.

Tip: We recommend annotating your parameter with types because Mesop will do runtime validation which will catch type issues earlier.

Finally, we call the function me.insert_web_component with the following arguments:

  • name - This is the web component name and must match the name defined in the JavaScript module.
  • key - Like all components, web components accept a key which is a unique identifier. See the component key docs for more info.
  • events - A dictionary where the key is the event name. This must match a property name, defined in JavaScript. The value is the event handler (callback) function.
  • properties - A dictionary where the key is the property name that's defined in JavaScript and the value is the property value which is plumbed to the JavaScript component.

Note: Keys for events and properties must not be "src", "srcdoc", or start with "on" to avoid web security risks.

In summary, when you see a string literal, it should match something on the JavaScript side which is explained next.

JavaScript module

Let's now take a look at how we implement in the web component in JavaScript:

javascript title="counter_component.js"
--8<-- "mesop/examples/web_component/quickstart/counter_component.js"

In this example, we have used Lit which is a small library built on top of web standards in a simple, secure and declarative manner.

Note: you can write your web components using any web technologies (e.g. TypeScript) or frameworks as long as they conform to the interface defined by your Python module.

Properties

The static property named properties defines two kinds of properties:

  • Regular properties - these were defined in the properties argument of insert_web_component. The property name in JS must match one of the properties dictionary key. You also should make sure the Python and JS types are compatible to avoid issues.
  • Event properties - these were defined in the events argument of insert_web_component. The property name in JS must match one of the events dictionary key. Event properties are always type String because the value is a handler id which identifies the Python event handler function.

Triggering an event

To trigger an event in your component, let's look at the _onDecrement method implementation:

javascript
this.dispatchEvent(
  new MesopEvent(this.decrementEvent, {
    value: this.value - 1,
  }),
);

this.dispatchEvent is a standard web API where a DOM element can emit an event. For Mesop web components, we will always emit a MesopEvent which is a class provided on the global object (window). The first argument is the event handler id so Mesop knows which Python function to call as the event handler and the second argument is the payload which is a JSON-serializable value (oftentimes an object) that the Python event handler can access.

Learn more about Lit

I didn't cover the render function which is a standard Lit method. I recommend reading through Lit's docs which are excellent ahd have interactive tutorials.

Using the component

Finally, let's use the web component we defined. When you click on the decrement button, the value will decrease from 10 to 9 and so on.

python title="counter_component_app.py"
--8<-- "mesop/examples/web_component/quickstart/counter_component_app.py"

Even though this was a toy example, you've learned how to build a web component from scratch which does bi-directional communication between the Python server and JavaScript client.

Next steps

To learn more, read the API docs or look at the examples.


Comparison (`docs/comparison.md`)

Comparison with Other Python UI Frameworks

This page aims to provide an objective comparison between Mesop and other popular Python-based web application frameworks, specifically Streamlit and Gradio. This is a difficult doc to write but we feel that it's important to explain the differences as this is frequently asked.

While we believe Mesop offers a unique philosophy for building UIs, we strive to be fair and accurate in highlighting the strengths of each framework.

Because this is a fast-moving space, some of the information may be out of date. Please file an issue and let us know what we should fix.

Streamlit

Streamlit and Mesop share similar goals in terms of enabling Python developers to rapidly build web apps, particularly for AI use cases.

However, there are some key differences:

Execution Model

Streamlit executes apps in a script-like manner where the entire app reruns on each user interaction. This enables a boilerplate-free UI development model that's easy to get started with, but requires mechanisms like caching and fragments to optimize the performance with this model.

Mesop uses a function-based model commonly found in web frameworks where the program is executed once on server initialization and then the page and component functions are executed in each render loop. This provides regular Python execution semantics because top-level initialization code is executed exactly once.

Styling and Customization

Streamlit offers pre-styled components with customization primarily through themes, prioritizing consistency and ease of use over flexibility.

In addition to providing Material-themed components, Mesop offers a low-level Style API to configure CSS properties. Mesop provides limited theming support with dark theming and doesn't support theming to other colors.

Components

Both Streamlit and Mesop offer a range of standard components (e.g., forms, tables, chat interfaces), with Streamlit providing a larger set of built-in components, especially for data science use cases like data visualization.

Streamlit supports custom components rendered in iframes for isolation. It offers first-class support for React components and can accommodate other frameworks through a framework-agnostic template.

Mesop enables creating custom web components based on open web standards, facilitating interoperability with components from different frameworks like Lit. Mesop web components are rendered in the same frame as the rest of the Mesop app which provides more flexibility but less isolation.

Streamlit has a more established ecosystem of community-developed components, while Mesop's community and component ecosystem are still developing.

Gradio

Gradio and Mesop both enable rapid ML/AI app development but with different approaches.

Gradio has a strong focus on creating demos and interfaces for machine learning models and makes it easy to build a UI for a model. Gradio also offers a lower-level abstraction known as Blocks for more general web applications.

Mesop, while well-suited for ML/AI use cases, is a more general-purpose framework that can be used for a wide range of web applications.

Components

Gradio provides a set of pre-built components optimized for common ML inputs and outputs (e.g. image classification, text generation). This makes it fast to set up standard model interfaces. In addition to built-in components, you can create custom components in Python and JavaScript (Svelte).

Mesop provides general-purpose UI components, which can be used for a variety of layout and UI designs. Higher-level components like the chat component are built on top of these low-level UI components. This makes it better suited for building custom interfaces, such as the demo gallery. Mesop also supports creating custom web components based on open web standards, facilitating interoperability with components from different frameworks.

Styling and Customization

Gradio features a robust theming system with prebuilt options and extensive UI customization. It also supports custom CSS via direct string construction.

Mesop offers a statically typed Style API for CSS properties. While it includes dark theme support, Mesop's theming capabilities are currently limited and does not allow custom color schemes.

State management

Gradio uses an imperative approach to state management, coupling state with component updates. State is typically managed through function parameters and return values, which can be straightforward for simple interfaces but may become complex as applications grow.

Mesop adopts a declarative state management approach, separating state updates from UI rendering. It uses dataclasses for state, providing type-safety and structure for complex states. This separation allows for more granular control over UI updates but may have a steeper learning curve for beginners.

Deployment

Gradio makes it easy to share demos via Hugging Face Spaces. Mesop apps can also be deployed on Hugging Face Spaces, but requires a few more steps.

Conclusion

Both Streamlit and Gradio offer gentle learning curves, making it easy for Python developers to quickly build standard AI applications.

Mesop embraces a declarative UI paradigm, which introduces additional concepts but can provide more flexibility for custom applications.

Ultimately, the best choice depends on your specific use case, desired level of customization, and development preferences. We encourage you to explore each framework to determine which best fits your needs.


Demo (`docs/demo.md`)


hide:

  • navigation
  • toc


Faq (`docs/faq.md`)

Frequently Asked Questions

General

What kinds of apps is Mesop suited for?

Mesop is well-suited for ML/AI demos and internal tools because it enables developers without frontend experience to quickly build web apps. For use cases that prioritize developer experience and velocity, Mesop can be a good choice.

Demanding consumer-facing apps, which have strict requirements in terms of performance, custom UI components, and i18n/localization would not be a good fit for Mesop and other UI frameworks may be more suitable.

How does Mesop compare to other Python UI frameworks?

We have written a comparison doc to answer this question in-depth.

Is Mesop production-ready?

Dozens of teams at Google have used Mesop to build demos and internal apps.

Although Mesop is pre-v1, we take backwards-compatibilty seriously and avoid backwards incompatible change. This is critical to us because many teams within Google rely on Mesop and we need to not break them.

Occasionally, we will do minor clean-up for our APIs, but we will provide warnings/deprecation notices and provide at least 1 release to migrate to the newer APIs.

Which modules should I import from Mesop?

Only import from these two modules:

py
import mesop as me
import mesop.labs as mel

All other modules are considered internal implementation details and may change without notice in future releases.

Is Mesop an official Google product?

No, Mesop is not an official Google product and Mesop is a 20% project maintained by a small core team of Google engineers with contributions from the broader community.

Deployment

How do I share or deploy my Mesop app?

The best way to share your Mesop app is to deploy it to a cloud service. You can follow our deployment guide for step-by-step instructions to deploy to Google Cloud Run.

Note: you should be able to deploy Mesop on any cloud service that takes a container. Please read the above deployment guide as it should be similar steps.


Goals (`docs/goals.md`)

Goals

I think it's helpful to explicitly state the goals of a project because it provides clarity for not only the development team, but also developers who are evaluating Mesop amongst other options:

  1. Prioritize Python developer experience - Provide the best possible developer experience for Python engineers with minimal frontend experience. Traditional web UI frameworks (e.g. React) prioritize developer experience, but they are focused on web developers who are familiar with the web ecosystem (e.g. HTML, node.js, etc.).
  2. Rich user interactions - You should be able to build reasonably sophisticated web applications and demos (e.g. LLM chat) without building custom native components.
  3. Simple deployment - Make deployment simple by packaging Mesop into a container which can be deployed as a standalone server.

Examples of applying these goals

  • Web performance: This doesn't mean other goals like web performance have no weight, but we will consistently rank these goals as higher priorities. For example, we could improve performance by serving static assets via CDN, but this would complicate our deployment. For instance, we'd need to ensure that pushing a new Python server binary and JS static assets happened at the same time, or you can get version skews which can cause cryptic errors.

  • Template vs. code: Mesop adopts the pattern of UI-as-code instead of using a separate templating language. Our belief is that writing Python code is a significantly better learning curve for our target developers. Rather than making them learn a new templating language (DSL) that they are unfamiliar with, they can write Python code which allows them idiomatic ways of expressing conditional logic and looping.


Showcase (`docs/showcase.md`)


hide:

  • navigation
  • toc


Api Config (`docs/api/config.md`)

Config

Overview

Mesop is configured at the application level using environment variables.

Configuration values

MESOP_STATIC_FOLDER

NOTE: This feature will be enabled by default in the next Mesop release and set to use static as the default static folder. You can still set it to another directory or "" to disable the feature.

Allows access to static files from the Mesop server.

It is important to know that the specified folder path is relative to the current
working directory where the Mesop command is run. Absolute paths are not allowed.

Example:

In this case, the current working directory is /srv, which means Mesop will make
/srv/static the static folder.

bash
cd /srv
MESOP_STATIC_FOLDER=static mesop app/main.py

Here are some examples of valid paths. Let's assume the current working directory is
/srv/

  • static becomes /srv/static
  • static/ becomes /srv/static
  • static/assets becomes /srv/static/assets
  • ./static becomes /srv/static
  • ./static/ becomes /srv/static
  • ./static/assets becomes /srv/static/assets

Invalid paths will raise MesopDeveloperException. Here are some examples:

  • Absolute paths (e.g. /absolute/path)
  • .
  • ./
  • ..
  • ../

MESOP_STATIC_URL_PATH

This is the base URL path from which files for your specified static folder will be
made viewable.

The static URL path is only recognized if MESOP_STATIC_FOLDER is set.

For example, given MESOP_STATIC_FOLDER=static and MESOP_STATIC_URL_PATH=/assets, the
file static/js/script.js can be viewable from the URL path /assets/js/script.js.

Default: /static

MESOP_STATE_SESSION_BACKEND

Sets the backend to use for caching state data server-side. This makes it so state does
not have to be sent to the server on every request, reducing bandwidth, especially if
you have large state objects.

The backend options available at the moment are memory, file, sql, and firestore.

memory

Users should be careful when using the memory backend. Each Mesop process has their
own RAM, which means cache misses will be common if each server has multiple processes
and there is no session affinity. In addition, the amount of RAM must be carefully
specified per instance in accordance with the expected user traffic and state size.

The safest option for using the memory backend is to use a single process with a
good amount of RAM. Python is not the most memory efficient, especially when saving data
structures such as dicts.

The drawback of being limited to a single process is that requests will take longer to
process since only one request can be handled at a time. This is especially problematic
if your application contains long running API calls.

If session affinity is available, you can scale up multiple instances, each running
single processes.

file

Users should be careful when using the file backend. Each Mesop instance has their
own disk, which can be shared among multiple processes. This means cache misses will be
common if there are multiple instances and no session affinity.

If session affinity is available, you can scale up multiple instances, each running
multiple Mesop processes. If no session affinity is available, then you can only
vertically scale a single instance.

The bottleneck with this backend is the disk read/write performance. The amount of disk
space must also be carefully specified per instance in accordance with the expected user
traffic and state size.

You will also need to specify a directory to write the state data using
MESOP_STATE_SESSION_BACKEND_FILE_BASE_DIR.

SQL

NOTE: Setting up and configuring databases is out of scope of this document.

This option uses SqlAlchemy to store Mesop state sessions
in supported SQL databases, such as SQLite3 and PostgreSQL. You can also connect to
hosted options, such as GCP CloudSQL.

If you use SQLite3, you cannot use an in-memory database. It has to be a file. This
option has similar pros/cons as the file backend. Mesop uses the default configuration
for SQLite3, so the performance will not be optimized for Mesop's usage patterns.
SQLite3 is OK for development purposes.

Using a database like PostgreSQL will allow for better scalability, both vertically and
horizontally, since the database is decoupled from the Mesop server.

The drawback here is that this requires knowledge of the database you're using. At
minimum, you will need to create a database and a database user with the right
privileges. You will also need to create the database table, which you can create
with this script. You will need to update the CONNECTION_URI and TABLE_NAME to match
your database and settings. Also the database user for this script will need privileges
to create tables on the target database.

python
from sqlalchemy import (
  Column,
  DateTime,
  LargeBinary,
  MetaData,
  String,
  Table,
  create_engine,
)

CONNECTION_URI = "your-database-connection-uri"
# Update to "your-table-name" if you've overridden `MESOP_STATE_SESSION_BACKEND_SQL_TABLE`.
TABLE_NAME = "mesop_state_session"

db = create_engine(CONNECTION_URI)
metadata = MetaData()
table = Table(
  TABLE_NAME,
  metadata,
  Column("token", String(23), primary_key=True),
  Column("states", LargeBinary, nullable=False),
  Column("created_at", DateTime, nullable=False, index=True),
)

metadata.create_all(db)

The Mesop server will raise a sqlalchemy.exc.ProgrammingError if there is a
database configuration issue.

By default, Mesop will use the table name mesop_state_session, but this can be
overridden using MESOP_STATE_SESSION_BACKEND_SQL_TABLE.

GCP Firestore

This options uses GCP Firestore to store
Mesop state sessions. The (default) database has a free tier that can be used for
for small demo applications with low traffic and moderate amounts of state data.

Since Firestore is decoupled from your Mesop server, it allows you to scale vertically
and horizontally without the considerations you'd need to make for the memory and
file backends.

In order to use Firestore, you will need a Google Cloud account with Firestore enabled.
Follow the instructions for creating a Firestore in Native mode database.

Mesop is configured to use the (default) Firestore only. The GCP project is determined
using the Application Default Credentials (ADC) which is automatically configured for
you on GCP services, such as Cloud Run.

For local development, you can run this command:

sh
gcloud auth application-default login

If you have multiple GCP projects, you may need to update the project associated
with the ADC:

sh
GCP_PROJECT=gcp-project
gcloud config set project $GCP_PROJECT
gcloud auth application-default set-quota-project $GCP_PROJECT

Mesop leverages Firestore's TTL policies
to delete stale state sessions. This needs to be set up using the following command,
otherwise old data will accumulate unnecessarily.

sh
COLLECTION_NAME=collection_name
gcloud firestore fields ttls update expiresAt \
  --collection-group=$COLLECTION_NAME

By default, Mesop will use the collection name mesop_state_sessions, but this can be
overridden using MESOP_STATE_SESSION_BACKEND_FIRESTORE_COLLECTION.

Default: none

MESOP_STATE_SESSION_BACKEND_FILE_BASE_DIR

This is only used when the MESOP_STATE_SESSION_BACKEND is set to file. This
parameter specifies where Mesop will read/write the session state. This means the
directory must be readable and writeable by the Mesop server processes.

MESOP_STATE_SESSION_BACKEND_FIRESTORE_COLLECTION

This is only used when the MESOP_STATE_SESSION_BACKEND is set to firestore. This
parameter specifies which Firestore collection that Mesop will write state sessions to.

Default: mesop_state_sessions

MESOP_STATE_SESSION_BACKEND_SQL_CONNECTION_URI

This is only used when the MESOP_STATE_SESSION_BACKEND is set to sql. This
parameter specifies the database connection string. See the SqlAlchemy docs for more details.

Default: mesop_state_session

MESOP_STATE_SESSION_BACKEND_SQL_TABLE

This is only used when the MESOP_STATE_SESSION_BACKEND is set to sql. This
parameter specifies which SQL database table that Mesop will write state sessions to.

Default: mesop_state_session

MESOP_PROD_UNREDACTED_ERRORS

Mesop, by default, only shows unredacted errors in debug mode.

If you run in prod mode, the errors are redacted (internal errors are given a generic message "Sorry, there was an error. Please contact the developer.") and the tracebacks are not shown.

If you want to show unredacted errors, including in prod mode, set this to true. This may be useful if you're deploying a Mesop app internally and want to get error details even in production.

Experimental configuration values

These configuration values are experimental and are subject to breaking change, including removal in future releases.

MESOP_CONCURRENT_UPDATES_ENABLED (DEPRECATED)

!!! danger "Deprecated experimental feature"

text
This experimental feature has been removed in Mesop v1. Use `MESOP_WEBSOCKETS_ENABLED` instead.

Allows concurrent updates to state in the same session. If this is not updated, then updates are queued and processed sequentially.

By default, this is not enabled. You can enable this by setting it to true.

MESOP_WEBSOCKETS_ENABLED

!!! warning "Experimental feature"

text
This is an experimental feature and is subject to breaking change. Please follow [https://github.com/mesop-dev/mesop/issues/1028](https://github.com/mesop-dev/mesop/issues/1028) for updates.

This uses WebSockets instead of HTTP Server-Sent Events (SSE) as the transport protocol for UI updates. Using WebSockets also changes the Mesop server architecture to be stateful (e.g. meaning a server restart or routing to a different server instance means the user will lose state). If you enable this feature, then you should ensure the following:

  • Either a single server instance is used or you route a particular user to the same server for a given session (i.e. session affinity).
  • Either ensure user state is not critical (e.g. it's OK if it's cleared out due to server restart, which could happen at any time on typical cloud infrastructure) or is persisted somewhere (e.g. written to a database).

Enabling this also allows user events to be handled concurrently, which is particularly useful supporting interactivity for long-running event handlers.

By default, this is not enabled. You can enable this by setting it to true.

MESOP_APP_BASE_PATH

This is the base path used to resolve other paths, particularly for serving static files. Must be an absolute path. This is rarely needed because the default of using the current working directory is usually sufficient.

MESOP_BASE_URL_PATH

Base URL path for serving the Mesop application. Set this if your app is served from a non-root path (e.g. /myapp). All HTTP routes, static assets, and API calls will be prefixed with this path. The value must start with a slash but should not include a trailing slash.

MESOP_HTTP_CACHE_JS_BUNDLE

Enables HTTP caching for the main JS bundle (i.e. prod_bundle.js) served by Mesop. This minimizes unnecessary HTTP requests for the JS bundle.

Note: this does not cache other JS files like web components, which are configured separately (see below).

MESOP_WEB_COMPONENTS_HTTP_CACHE_KEY

Enables HTTP caching for the web component entry point modules.

Note: this does not cache JS files imported by the web component entry modules.

!!! warning "Experimental"
The signed/encrypted cookie API is experimental and may change in future releases.

Secret key required for using Mesop cookies.

  • Signed cookies (signed=True): The value is HMAC-signed so any
    client-side tampering is detected on the next read. Contents are still
    visible (Base64-encoded) in browser DevTools.
  • Encrypted cookies (encrypted=True): The value is Fernet-encrypted,
    hiding the contents entirely. Also requires pip install cryptography.

Generate a strong key for production:

sh
python -c "import secrets; print(secrets.token_hex(32))"

Set it before starting Mesop:

sh
MESOP_COOKIE_SECRET_KEY=your-random-secret mesop main.py

Or in a .env file:

sh title=".env"
MESOP_COOKIE_SECRET_KEY=your-random-secret

Security notes:

  • Use a long, random value (32+ bytes of entropy).
  • Rotate the key if it is ever compromised — all signed/encrypted cookies
    issued with the old key will become invalid on the next read (users will
    get default instances rather than an error).
  • Never commit the key to source control.

Multi-worker single-use behaviour:

When me.set_cookie() is called, Mesop issues a short-lived signed token
that the browser exchanges for the actual Set-Cookie header. The token
is single-use within a single server process: once a worker has seen a
token's nonce it will reject replays. In multi-worker deployments, a
replay request routed to a different worker would be accepted because each
worker tracks used nonces independently. In practice this is mitigated by
the CSRF Origin check on /__apply-cookies (cross-origin replay is
blocked) and the 60-second token TTL, but it is not cryptographically
guaranteed across workers. If strict single-use across all workers is
required, ensure session affinity (sticky sessions) so that
/__apply-cookies always returns to the originating worker.

Usage Examples

One-liner

You can specify the environment variables before the mesop command.

sh
MESOP_STATE_SESSION_BACKEND=memory mesop main.py

Use a .env file

Mesop also supports .env files. This is nice since you don't have to keep setting
the environment variables. In addition, the variables are only set when the application
is run.

sh title=".env"
MESOP_STATE_SESSION_BACKEND=file
MESOP_STATE_SESSION_BACKEND_FILE_BASE_DIR=/tmp/mesop-sessions

When you run your Mesop app, the .env file will then be read.

sh
mesop main.py

Api Page (`docs/api/page.md`)

Page API

Overview

Pages allow you to build multi-page applications by decorating Python functions with me.page. To learn more, read the see multi-pages guide.

Examples

Simple, 1-page setup

To create a simple Mesop app, you can use me.page() like this:

python
import mesop as me

@me.page()
def foo():
    me.text("bar")

NOTE: If you do not provide a path argument, then it defaults to the root path "/".

Explicit 1-page setup

This is the same as the above example which explicitly sets the route to "/".

python
import mesop as me

@me.page(path="/")
def foo():
    me.text("bar")

API

::: mesop.features.page.page
::: mesop.security.security_policy.SecurityPolicy
::: mesop.events.events.LoadEvent

`on_load`

You may want to do some sort of data-processing when a page is first loaded in a session.

Simple handler

An on_load handler is similar to a regular event handler where you can mutate state.

python
--8<-- "mesop/examples/docs/on_load.py"

Generator handler

The on_load handler can also be a generator function. This is useful if you need to call a slow or streaming API and want to return intermediate results before all the data has been received.

python
--8<-- "mesop/examples/docs/on_load_generator.py"

Api Query Params (`docs/api/query-params.md`)

Query Params API

Overview

Query params, also sometimes called query string, provide a way to manage state in the URLs. They are useful for providing deep-links into your Mesop app.

Example

Here's a simple working example that shows how you can read and write query params.

py
@me.page(path="/examples/query_params/page_2")
def page_2():
  me.text(f"query_params={me.query_params}")
  me.button("Add query param", on_click=add_query_param)
  me.button("Navigate", on_click=navigate)

def add_query_param(e: me.ClickEvent):
  me.query_params["key"] = "value"

def navigate(e: me.ClickEvent):
  me.navigate("/examples/query_params", query_params=me.query_params)

Usage

You can use query parameters from me.query_params, which has a dictionary-like interface, where the key is the parameter name and value is the parameter value.

Get a query param value

py
value: str = me.query_params['param_name']

This will raise a KeyError if the parameter doesn't exist. You can use in to check whether a key exists in me.query_params:

py
if 'key' in me.query_params:
    print(me.query_params['key'])

???+ NOTE "Repeated query params"
If a query param key is repeated, then you will get the first value. If you want all the values use get_all.

Get all values

To get all the values for a particular query parameter key, you can use me.query_params.get_all, which returns a sequence of parameter values (currently implemented as a tuple).

py
all_values = me.query_params.get_all('param_name')

Iterate

py
for key in query_params:
  value = query_params[key]

Set query param

py
query_params['new_param'] = 'value'

Set repeated query param

py
query_params['repeated_param'] = ['value1', 'value2']

Delete

py
del query_params['param_to_delete']

Patterns

Here's an example of how to navigate to a new page with query parameters:

py
def click_navigate_button(e: me.ClickEvent):
    me.query_params['q'] = "value"
    me.navigate('/search', query_params=me.query_params)

You can also navigate by passing in a dictionary to query_params parameter for me.navigate if you do not want to keep the existing query parameters.

py
def click_navigate_button(e: me.ClickEvent):
    me.navigate('/search', query_params={"q": "value})

Api Style (`docs/api/style.md`)

Overview

Mesop provides a Python API that wraps the browser's native CSS style API.

API

::: mesop.component_helpers.style.Style
::: mesop.component_helpers.style.Border
::: mesop.component_helpers.style.BorderSide
::: mesop.component_helpers.style.Margin
::: mesop.component_helpers.style.Padding


Api Viewport Size (`docs/api/viewport-size.md`)

Viewport size

Overview

The viewport size API allows you to access the current viewport size. This can be useful for creating responsive and adaptive designs that are suitable for the user's screen size.

Examples

Responsive Design

Responsive design is having a single fluid layout that adapts to all screen sizes.

You can use the viewport size to dynamically set the property of a style. This can be useful if you want to fit two boxes in a row for larger screens (e.g. desktop) and a single box for smaller screens (e.g. mobile) as shown in the example below:

py
import mesop as me

@me.page()
def page():
    if me.viewport_size().width > 640:
        width = me.viewport_size().width / 2
    else:
        width = me.viewport_size().width
    for i in range(8):
      me.box(style=me.Style(width=width))

Tip: Responsive design tends to take less work and is usually a good starting point.

Adaptive Design

Adaptive design is having multiple fixed layouts for specific device categories at specific breakpoints, typically viewport width.

For example, oftentimes you will hide the nav component on a mobile device and instead show a hamburger menu, while for a larger device you will always show the nav component on the left side.

py
import mesop as me

@me.page()
def page():
    if me.viewport_size().width > 480:
        nav_component()
        body()
    else:
        body(show_menu_button=True)

Tip: Adaptive design tends to take more work and is best for optimizing complex mobile and desktop experiences.

API

::: mesop.features.viewport_size.viewport_size

::: mesop.features.viewport_size.Size


Codelab 2 (`docs/codelab/2.md`)

DuoChat Codelab Part 2: Building the basic UI

In this section, we'll create the main layout for our DuoChat application, including the header, chat input area, and some basic styling. We'll use Mesop's components and styling system to create an attractive and functional UI.

Updating the Main Layout

Let's start by updating our main.py file to include a more structured layout. We'll use Mesop's box component for layout and add some custom styles.

Replace the content of main.py with the following:

text
/* Detailed source-code truncated for AI context efficiency. */

Run the Mesop app and look at the changes:

sh
mesop main.py

Let's review the changes:

  1. We've added a ROOT_BOX_STYLE to set the overall layout and background color.
  2. We're importing a custom font (Inter) using the stylesheets parameter in the @me.page decorator.
  3. We've created separate functions for the header and chat_input components.
  4. The main layout uses nested box components with custom styles to create a centered, responsive design.

Understanding Mesop's Styling System

Mesop's styling system is based on Python classes that correspond to CSS properties. You can learn more by reading the Style API docs.

Adding Interactivity

Now, let's add some basic interactivity to our chat input. We'll update the chat_input function to handle user input:

python title="main.py"
@me.stateclass
class State:
    input: str = ""

def on_blur(e: me.InputBlurEvent):
    state = me.state(State)
    state.input = e.value

def chat_input():
    state = me.state(State)
    with me.box(
        style=me.Style(
            border_radius=16,
            padding=me.Padding.all(8),
            background="white",
            display="flex",
            width="100%",
        )
    ):
        with me.box(style=me.Style(flex_grow=1)):
            me.native_textarea(
                value=state.input,
                placeholder="Enter a prompt",
                on_blur=on_blur,
                style=me.Style(
                    padding=me.Padding(top=16, left=16),
                    outline="none",
                    width="100%",
                    border=me.Border.all(me.BorderSide(style="none")),
                ),
            )
        with me.content_button(type="icon", on_click=send_prompt):
            me.icon("send")

def send_prompt(e: me.ClickEvent):
    state = me.state(State)
    print(f"Sending prompt: {state.input}")
    state.input = ""

Here's what we've added:

  1. A State class to manage the application state, including the user's input.
  2. An on_blur function to update the state when the user switches focus from the textarea.
  3. A send_prompt function that will be called when the send button is clicked.

Running the Updated Application

Run the application again with mesop main.py and navigate to http://localhost:32123. You should now see a styled header, a centered layout, and a functional chat input area.

Troubleshooting

If you're having trouble, compare your code to the solution.

Next Steps

In the next section, we'll dive deeper into state management and implement the model picker dialog.

Managing state & dialogs

Codelab 3 (`docs/codelab/3.md`)

DuoChat Codelab Part 3: Managing state & dialogs

In this section, we'll expand our application's state management capabilities and implement a dialog for selecting AI models. We'll use Mesop's state management system and dialog components to create an interactive model selection experience.

Expanding the State Management

First, let's create a data_model.py file with a more comprehensive state structure:

python title="data_model.py"
from dataclasses import dataclass, field
from typing import Literal
from enum import Enum

import mesop as me

Role = Literal["user", "model"]

@dataclass(kw_only=True)
class ChatMessage:
    role: Role = "user"
    content: str = ""
    in_progress: bool = False

class Models(Enum):
    GEMINI_1_5_FLASH = "Gemini 1.5 Flash"
    GEMINI_1_5_PRO = "Gemini 1.5 Pro"
    CLAUDE_3_5_SONNET = "Claude 3.5 Sonnet"

@dataclass
class Conversation:
    model: str = ""
    messages: list[ChatMessage] = field(default_factory=list)

@me.stateclass
class State:
    is_model_picker_dialog_open: bool = False
    input: str = ""
    conversations: list[Conversation] = field(default_factory=list)
    models: list[str] = field(default_factory=list)
    gemini_api_key: str = ""
    claude_api_key: str = ""

@me.stateclass
class ModelDialogState:
    selected_models: list[str] = field(default_factory=list)

This expanded state structure allows us to manage multiple conversations, selected models, and API keys.

Implementing the Model Picker Dialog

Now, let's implement the model picker dialog in our main.py file. First, we'll create a new file called dialog.py with the following content, which is based on the dialog pattern from the demo gallery:

python title="dialog.py"
import mesop as me

@me.content_component
def dialog(is_open: bool):
    with me.box(
        style=me.Style(
            background="rgba(0,0,0,0.4)",
            display="block" if is_open else "none",
            height="100%",
            overflow_x="auto",
            overflow_y="auto",
            position="fixed",
            width="100%",
            z_index=1000,
        )
    ):
        with me.box(
            style=me.Style(
                align_items="center",
                display="grid",
                height="100vh",
                justify_items="center",
            )
        ):
            with me.box(
                style=me.Style(
                    background="#fff",
                    border_radius=20,
                    box_sizing="content-box",
                    box_shadow=(
                        "0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"
                    ),
                    margin=me.Margin.symmetric(vertical="0", horizontal="auto"),
                    padding=me.Padding.all(20),
                )
            ):
                me.slot()

@me.content_component
def dialog_actions():
    with me.box(
        style=me.Style(
            display="flex", justify_content="end", margin=me.Margin(top=20)
        )
    ):
        me.slot()

Now, let's update our main.py file to include the model picker dialog. Copy the following code and replace main.py with it:

text
/* Detailed source-code truncated for AI context efficiency. */

This updated code adds the following features:

  1. A model picker dialog that allows users to select AI models and enter API keys.
  2. State management for selected models and API keys.
  3. A model switcher in the chat input area that opens the model picker dialog.
  4. Disabling of models based on whether the corresponding API key has been entered.

Running the Updated Application

Run the application again with mesop main.py and navigate to http://localhost:32123. You should now see a chat input area with a model switcher. Clicking on the model switcher will open the model picker dialog, allowing you to select models and enter API keys.

Troubleshooting

If you're having trouble, compare your code to the solution.

Next Steps

In the next section, we'll integrate multiple AI models into our application. We'll set up connections to Gemini and Claude, implement model-specific chat functions, and create a way to interact with multiple models.

Integrating AI APIs

Codelab 4 (`docs/codelab/4.md`)

DuoChat Codelab Part 4: Integrating AI APIs

In this section, we'll set up connections to Gemini and Claude, implement model-specific chat functions, and create a unified interface for interacting with multiple models simultaneously.

Setting Up AI Model Connections

First, let's create separate files for our Gemini and Claude integrations.

Create a new file called gemini.py:

python title="gemini.py"
import google.generativeai as genai
from typing import Iterable

from data_model import ChatMessage, State
import mesop as me

generation_config = {
    "temperature": 1,
    "top_p": 0.95,
    "top_k": 64,
    "max_output_tokens": 8192,
}

def configure_gemini():
    state = me.state(State)
    genai.configure(api_key=state.gemini_api_key)

def send_prompt_pro(prompt: str, history: list[ChatMessage]) -> Iterable[str]:
    configure_gemini()
    model = genai.GenerativeModel(
        model_name="gemini-2.5-pro",
        generation_config=generation_config,
    )
    chat_session = model.start_chat(
        history=[
            {"role": message.role, "parts": [message.content]} for message in history
        ]
    )
    for chunk in chat_session.send_message(prompt, stream=True):
        yield chunk.text

def send_prompt_flash(prompt: str, history: list[ChatMessage]) -> Iterable[str]:
    configure_gemini()
    model = genai.GenerativeModel(
        model_name="gemini-flash-latest",
        generation_config=generation_config,
    )
    chat_session = model.start_chat(
        history=[
            {"role": message.role, "parts": [message.content]} for message in history
        ]
    )
    for chunk in chat_session.send_message(prompt, stream=True):
        yield chunk.text

Now, create a new file called claude.py:

python title="claude.py"
import anthropic
from typing import Iterable

from data_model import ChatMessage, State
import mesop as me

def call_claude_sonnet(input: str, history: list[ChatMessage]) -> Iterable[str]:
    state = me.state(State)
    client = anthropic.Anthropic(api_key=state.claude_api_key)
    messages = [
        {
            "role": "assistant" if message.role == "model" else message.role,
            "content": message.content,
        }
        for message in history
    ] + [{"role": "user", "content": input}]

    with client.messages.stream(
        max_tokens=1024,
        messages=messages,
        model="claude-3-sonnet-20240229",
    ) as stream:
        for text in stream.text_stream:
            yield text

Updating the Main Application

Now, let's update our main.py file to integrate these AI models. We'll update the page function, add the display_conversations and display_message functions and modify the send_prompt function to handle multiple models and display their responses:

text
/* Detailed source-code truncated for AI context efficiency. */

This updated code adds the following features:

  1. A display_conversations function that shows the chat history for each selected model.
  2. A display_message function that renders individual messages with appropriate styling.
  3. An updated send_prompt function that sends the user's input to all selected models and displays their responses in real-time.

Handling Streaming Responses

The send_prompt function now uses Python generators to handle streaming responses from the AI models. This allows us to update the UI in real-time as the models generate their responses.

Running the Updated Application

Run the application again with mesop main.py and navigate to http://localhost:32123. You should now be able to:

  1. Select multiple AI models using the model picker dialog.
  2. Enter API keys for Gemini and Claude.
  3. Send prompts to the selected models.
  4. See the responses from multiple models displayed simultaneously.

Troubleshooting

If you're having trouble, compare your code to the solution.

Next Steps

In the final section, we'll refine the user interface by creating a dedicated conversation page with a multi-column layout for different model responses. We'll also add some finishing touches to improve the overall user experience.

Wrapping it up

Codelab 5 (`docs/codelab/5.md`)

DuoChat Codelab Part 5: Wrapping it up

In this section, we'll create a multi-column layout for different model responses, implement user and model message components, add auto-scroll functionality, and finalize the chat experience with multiple models.

Creating a New Conversation Page

First, let's create a new page for our conversations. Update the main.py file to include a new route and function for the conversation page:

text
/* Detailed source-code truncated for AI context efficiency. */

Try running the app: mesop main.py and now you should navigate to the conversation page once you click the send button.

Adding Example Prompts

Let's add some example prompts to the home page to help users get started. Add the following functions to main.py:

python title="main.py"
EXAMPLES = [
    "Create a file-lock in Python",
    "Write an email to Congress to have free milk for all",
    "Make a nice box shadow in CSS",
]

def examples_row():
    with me.box(
        style=me.Style(
            display="flex", flex_direction="row", gap=16, margin=me.Margin(bottom=24)
        )
    ):
        for i in EXAMPLES:
            example(i)

def example(text: str):
    with me.box(
        key=text,
        on_click=click_example,
        style=me.Style(
            cursor="pointer",
            background="#b9e1ff",
            width="215px",
            height=160,
            font_weight=500,
            line_height="1.5",
            padding=me.Padding.all(16),
            border_radius=16,
            border=me.Border.all(me.BorderSide(width=1, color="blue", style="none")),
        ),
    ):
        me.text(text)

def click_example(e: me.ClickEvent):
    state = me.state(State)
    state.input = e.key

And then uncomment the callsite for examples_row in home_page.

Updating the Header

Let's update header to allow users to return to the home page when they click on the header box:

python title="main.py"
def header():
    def navigate_home(e: me.ClickEvent):
        me.navigate("/")
        state = me.state(State)
        state.conversations = []

    with me.box(
        on_click=navigate_home,
        style=me.Style(
            cursor="pointer",
            padding=me.Padding.all(16),
        ),
    ):
        me.text(
            "DuoChat",
            style=me.Style(
                font_weight=500,
                font_size=24,
                color="#3D3929",
                letter_spacing="0.3px",
            ),
        )

Finalizing the Chat Experience

Now that we have a dedicated conversation page with a multi-column layout, let's make some final improvements to enhance the user experience:

  1. Navigate to the conversations page at the start of a conversation.
  2. Implement auto-scrolling to keep the latest messages in view.

Update the send_prompt function in main.py:

python title="main.py"
def send_prompt(e: me.ClickEvent):
    state = me.state(State)
    if not state.conversations:
        me.navigate("/conversation")
        for model in state.models:
            state.conversations.append(Conversation(model=model, messages=[]))
    input = state.input
    state.input = ""

    for conversation in state.conversations:
        model = conversation.model
        messages = conversation.messages
        history = messages[:]
        messages.append(ChatMessage(role="user", content=input))
        messages.append(ChatMessage(role="model", in_progress=True))
        yield
        me.scroll_into_view(key="end_of_messages")
        if model == Models.GEMINI_1_5_FLASH.value:
            llm_response = gemini.send_prompt_flash(input, history)
        elif model == Models.GEMINI_1_5_PRO.value:
            llm_response = gemini.send_prompt_pro(input, history)
        elif model == Models.CLAUDE_3_5_SONNET.value:
            llm_response = claude.call_claude_sonnet(input, history)
        else:
            raise Exception("Unhandled model", model)
        for chunk in llm_response:
            messages[-1].content += chunk
            yield
        messages[-1].in_progress = False
        yield

Running the Final Application

Run the application with mesop main.py and navigate to http://localhost:32123. You should now have a fully functional DuoChat application with the following features:

  1. A home page with example prompts and a model picker.
  2. A conversation page with a multi-column layout for different model responses.
  3. Real-time streaming of model responses with loading indicators.
  4. Auto-scrolling to keep the latest message in view.

Troubleshooting

If you're having trouble, compare your code to the solution.

Conclusion

Congratulations! You've successfully built DuoChat, a Mesop application that allows users to interact with multiple AI models simultaneously. This project demonstrates Mesop's capabilities for creating responsive UIs, managing complex state, and integrating with external APIs.

Some potential next steps to further improve the application:

  1. Deploy your app.
  2. Add the ability to save and load conversations.
  3. Add support for additional AI models or services.

Feel free to experiment with these ideas or come up with your own improvements to enhance the DuoChat experience!


Codelab Cloud Run (`docs/codelab/cloud-run.md`)

This is a video tutorial that goes through the process of building and deploying a Mesop
app to Cloud Run.

Here is a screenshot of the app you will be building.

Part 1: Building a Gemini powered Mesop app

This video describes how to build a Gemini powered Mesop app.

Part 2: Deploying to Cloud Run

This video describes how deploy the Gemini powered Mesop app to Cloud Run.

GitHub Code

View the source code.


Components Audio (`docs/components/audio.md`)

Overview

Audio is the equivalent of an <audio> HTML element. Audio displays the browser's native audio controls.

Examples

python
--8<-- "demo/audio.py"

API

::: mesop.components.audio.audio.audio


Components Autocomplete (`docs/components/autocomplete.md`)

Overview

Autocomplete allows the user to enter free text or select from a list of dynamic values
and is based on the Angular Material autocomplete component.

This components only renders text labels and values.

The autocomplete filters by case-insensitively matching substrings of the option label.

Currently, there is no on blur event with this component since the blur event does not
get the selected value on the first blur. Due to this ambiguous behavior, the blur event
has been left out.

Examples

python
--8<-- "demo/autocomplete.py"

API

::: mesop.components.autocomplete.autocomplete.autocomplete
::: mesop.components.autocomplete.autocomplete.AutocompleteOption
::: mesop.components.autocomplete.autocomplete.AutocompleteOptionGroup
::: mesop.components.autocomplete.autocomplete.AutocompleteEnterEvent
::: mesop.components.autocomplete.autocomplete.AutocompleteSelectionChangeEvent
::: mesop.events.InputEvent


Components Badge (`docs/components/badge.md`)

Overview

Badge decorates a UI component and is oftentimes used for unread message count and is based on the Angular Material badge component.

Examples

python
--8<-- "demo/badge.py"

API

::: mesop.components.badge.badge.badge


Components Box (`docs/components/box.md`)

Overview

Box is a content component which acts as a container to group children components and styling them.

Examples

python
--8<-- "demo/box.py"

API

::: mesop.components.box.box.box


Components Button (`docs/components/button.md`)

Overview

Button is based on the Angular Material button component.

Examples

python
--8<-- "demo/button.py"

API

::: mesop.components.button.button.button
::: mesop.components.button.button.content_button
::: mesop.events.ClickEvent


Components Button Toggle (`docs/components/button_toggle.md`)

Overview

Button toggle is based on the Angular Material button toggle component.

Examples

python
--8<-- "demo/button_toggle.py"

API

::: mesop.components.button_toggle.button_toggle.button_toggle
::: mesop.components.button_toggle.button_toggle.ButtonToggleButton
::: mesop.components.button_toggle.button_toggle.ButtonToggleChangeEvent


Components Card (`docs/components/card.md`)

Overview

Card is based on the Angular Material card component.

Examples

python
--8<-- "demo/card.py"

API

::: mesop.components.card.card.card
::: mesop.components.card_header.card_header.card_header
::: mesop.components.card_content.card_content.card_content
::: mesop.components.card_actions.card_actions.card_actions


Components Chat (`docs/components/chat.md`)

Overview

Chat component is a quick way to create a simple chat interface. Chat is part of Mesop Labs.

Examples

python
--8<-- "demo/chat.py"

API

::: mesop.labs.chat.chat


Components Checkbox (`docs/components/checkbox.md`)

Overview

Checkbox is a multi-selection form control and is based on the Angular Material checkbox component.

Examples

python
--8<-- "demo/checkbox.py"

API

::: mesop.components.checkbox.checkbox.checkbox
::: mesop.components.checkbox.checkbox.content_checkbox
::: mesop.components.checkbox.checkbox.CheckboxChangeEvent
::: mesop.components.checkbox.checkbox.CheckboxIndeterminateChangeEvent


Components Code (`docs/components/code.md`)

Overview

Code is used to render code with syntax highlighting. code is a simple wrapper around markdown.

Examples

python
--8<-- "demo/code_demo.py"

API

::: mesop.components.code.code.code


Components Date Picker (`docs/components/date_picker.md`)

Overview

Date picker allows the user to enter free text or select a date from a calendar widget.
and is based on the Angular Material datapicker component.

Examples

python
--8<-- "demo/date_picker.py"

API

::: mesop.components.datepicker.datepicker.date_picker
::: mesop.components.datepicker.datepicker.DatePickerChangeEvent


Components Date Range Picker (`docs/components/date_range_picker.md`)

Overview

Date range picker allows the user to enter free text or select a dates from a calendar widget.
and is based on the Angular Material datapicker component.

Examples

python
--8<-- "demo/date_range_picker.py"

API

::: mesop.components.date_range_picker.date_range_picker.date_range_picker
::: mesop.components.date_range_picker.date_range_picker.DateRangePickerChangeEvent


Components Divider (`docs/components/divider.md`)

Overview

Divider is used to provide visual separation and is based on the Angular Material divider component.

Examples

python
--8<-- "demo/divider.py"

API

::: mesop.components.divider.divider.divider


Components Embed (`docs/components/embed.md`)

Overview

Embed allows you to embed/iframe another web site in your Mesop app.

Examples

python
--8<-- "demo/embed.py"

API

::: mesop.components.embed.embed.embed


Components Expansion Panel (`docs/components/expansion-panel.md`)

Overview

Expansion panel and is based on the Angular Material expansion panel component.

This is a useful component for showing a summary header which can be expanded into a more detailed card/panel.

The expansion panels can also be grouped together to create an accordion.

Examples

python
--8<-- "demo/expansion_panel.py"

API

::: mesop.components.accordion.accordion.accordion
::: mesop.components.expansion_panel.expansion_panel.expansion_panel
::: mesop.components.expansion_panel.expansion_panel.ExpansionPanelToggleEvent


Components Html (`docs/components/html.md`)

Overview

The HTML component allows you to add custom HTML to your Mesop app.

There are two modes for rendering HTML components:

  • sanitized (default), where the HTML is sanitized by Angular to remove potentially unsafe code like <script> and <style> for web security reasons.
  • sandboxed, which allows rendering of <script> and <style> HTML content by using an iframe sandbox.

Examples

python
--8<-- "demo/html_demo.py"

API

::: mesop.components.html.html.html


Components Icon (`docs/components/icon.md`)

Overview

Icon displays a Material icon/symbol and is based on the Angular Material icon component.

Examples

python
--8<-- "demo/icon.py"

API

::: mesop.components.icon.icon.icon


Components Image (`docs/components/image.md`)

Overview

Image is the equivalent of an [`` HTML element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img).

Examples

python
--8<-- "demo/image.py"

API

::: mesop.components.image.image.image


Components Input (`docs/components/input.md`)

Overview

Input allows the user to type in a value and is based on the Angular Material input component.

For longer text inputs, also see Textarea

Examples

python
--8<-- "demo/input.py"

API

::: mesop.components.input.input.input
::: mesop.components.input.input.InputBlurEvent
::: mesop.components.input.input.InputEnterEvent
::: mesop.events.InputEvent


Overview

Link creates an HTML anchor element (i.e. <a>) which links to another page.

Examples

python
--8<-- "demo/link.py"

API

::: mesop.components.link.link.link


Components Markdown (`docs/components/markdown.md`)

Overview

Markdown is used to render markdown text.

Examples

python
--8<-- "demo/markdown_demo.py"

API

::: mesop.components.markdown.markdown.markdown


Components Plot (`docs/components/plot.md`)

Overview

Plot provides a convenient way to render Matplotlib figures as an image.

Examples

python
--8<-- "demo/plot.py"

API

::: mesop.components.plot.plot.plot


Components Progress Bar (`docs/components/progress-bar.md`)

Overview

Progress Bar is used to indicate something is in progress and is based on the Angular Material progress bar component.

Examples

python
--8<-- "demo/progress_bar.py"

API

::: mesop.components.progress_bar.progress_bar.progress_bar
::: mesop.components.progress_bar.progress_bar.ProgressBarAnimationEndEvent


Components Progress Spinner (`docs/components/progress-spinner.md`)

Overview

Progress Spinner is used to indicate something is in progress and is based on the Angular Material progress spinner component.

Examples

python
--8<-- "demo/progress_spinner.py"

API

::: mesop.components.progress_spinner.progress_spinner.progress_spinner


Components Radio (`docs/components/radio.md`)

Overview

Radio is a single selection form control based on the Angular Material radio component.

Examples

python
--8<-- "demo/radio.py"

API

::: mesop.components.radio.radio.radio

::: mesop.components.radio.radio.RadioOption

::: mesop.components.radio.radio.RadioChangeEvent


Components Select (`docs/components/select.md`)

Overview

Select allows the user to choose from a list of values and is based on the Angular Material select component.

Examples

python
--8<-- "demo/select_demo.py"

API

::: mesop.components.select.select.select
::: mesop.components.select.select.SelectOption
::: mesop.components.select.select.SelectSelectionChangeEvent
::: mesop.components.select.select.SelectOpenedChangeEvent


Components Sidenav (`docs/components/sidenav.md`)

Overview

Sidenav is a sidebar typically used for navigation and is based on the Angular Material sidenav component.

Examples

python
--8<-- "demo/sidenav.py"

API

::: mesop.components.sidenav.sidenav.sidenav


Components Slide Toggle (`docs/components/slide-toggle.md`)

Overview

Slide Toggle allows the user to toggle on and off and is based on the Angular Material slide toggle component.

Examples

python
--8<-- "demo/slide_toggle.py"

API

::: mesop.components.slide_toggle.slide_toggle.slide_toggle
::: mesop.components.slide_toggle.slide_toggle.content_slide_toggle
::: mesop.components.slide_toggle.slide_toggle.SlideToggleChangeEvent


Components Slider (`docs/components/slider.md`)

Overview

Slider allows the user to select from a range of values and is based on the Angular Material slider component.

Examples

python
--8<-- "demo/slider.py"

API

::: mesop.components.slider.slider.slider
::: mesop.components.slider.slider.SliderValueChangeEvent


Components Table (`docs/components/table.md`)

Overview

Table allows the user to render an Angular Material table component from a Pandas data frame.

Examples

python
--8<-- "demo/table.py"

API

::: mesop.components.table.table.table


Components Text To Image (`docs/components/text-to-image.md`)

Overview

Text To Image component is a quick and simple way of getting started with Mesop. Text To Image is part of Mesop Labs.

Examples

python
--8<-- "demo/text_to_image.py"

API

::: mesop.labs.text_to_image.text_to_image


Components Text To Text (`docs/components/text-to-text.md`)

Overview

Text to text component allows you to take in user inputted text and return a transformed text. This is part of Mesop Labs.

Examples

python
--8<-- "demo/text_to_text.py"

API

::: mesop.labs.text_to_text.text_to_text


Components Text (`docs/components/text.md`)

Overview

Text displays text as-is. If you have markdown text, use the Markdown component.

Examples

python
--8<-- "demo/text.py"

API

::: mesop.components.text.text.text


Components Textarea (`docs/components/textarea.md`)

Overview

Textarea allows the user to type in a value and is based on the Angular Material input component for <textarea>.

This is similar to Input, but Textarea is better suited for long text inputs.

Examples

python
--8<-- "demo/textarea.py"

API

::: mesop.components.input.input.textarea
::: mesop.components.input.input.native_textarea
::: mesop.components.input.input.InputBlurEvent
::: mesop.events.InputEvent


Components Tooltip (`docs/components/tooltip.md`)

Overview

Tooltip is based on the Angular Material tooltip component.

Examples

python
--8<-- "demo/tooltip.py"

API

::: mesop.components.tooltip.tooltip.tooltip


Components Uploader (`docs/components/uploader.md`)

Overview

Uploader is the equivalent of an [`` HTML element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file) except it uses a custom UI that better
matches the look of Angular Material Components.

Examples

python
--8<-- "demo/uploader.py"

API

::: mesop.components.uploader.uploader.uploader
::: mesop.components.uploader.uploader.content_uploader
::: mesop.components.uploader.uploader.UploadEvent
::: mesop.components.uploader.uploader.UploadedFile


Components Video (`docs/components/video.md`)

Overview

Video is the equivalent of an <video> HTML element. Video displays the browser's native video controls.

Examples

python
--8<-- "demo/video.py"

API

::: mesop.components.video.video.video


Getting Started Core Concepts (`docs/getting-started/core-concepts.md`)

Core Concepts

This doc will explain the core concepts of building a Mesop app.

Hello World app

Let's start by creating a simple Hello World app in Mesop:

python
--8<-- "mesop/examples/docs/hello_world.py"

This simple example demonstrates a few things:

  • Every Mesop app starts with import mesop as me. This is the only recommended way to import mesop, otherwise your app may break in the future because you may be relying on internal implementation details.
  • @me.page is a function decorator which makes a function a root component for a particular path. If you omit the path parameter, this is the equivalent of @me.page(path="/").
  • app is a Python function that we will call a component because it's creating Mesop components in the body.

Components

Components are the building blocks of a Mesop application. A Mesop application is essentially a tree of components.

Let's explain the different kinds of components in Mesop:

  • Mesop comes built-in with native components. These are components implemented using Angular/Javascript. Many of these components wrap Angular Material components.
  • You can also create your own components which are called user-defined components. These are essentially Python functions like app in the previous example.

Counter app

Let's build a more complex app to demonstrate Mesop's interactivity features.

python
--8<-- "mesop/examples/docs/counter.py"

This app allows the user to click on a button and increment a counter, which is shown to the user as "Clicks: #".

Let's walk through this step-by-step.

State

The State class represents the application state for a particular browser session. This means every user session has its own instance of State.

@me.stateclass is a class decorator which is similar to Python's dataclass but also sets default values based on type hints and allows Mesop to inject the class as shown next.

Note: Everything in a state class must be serializable because it's sent between the server and browser.

Event handler

The button_click function is an event handler. An event handler has a single parameter, event, which can contain a value (this will be shown in the next example). An event handler is responsible for updating state based on the incoming event.

me.state(State) retrieves the instance of the state class for the current session.

Component

Like the previous example, main is a Mesop component function which is decorated with page to mark it as a root component for a path.

Similar to the event handler, we can retrieve the state in a component function by calling me.state(State).

Note: it's not safe to mutate state inside a component function. All mutations must be done in an event handler.

Rendering dynamic values in Mesop is simple because you can use standard Python string interpolation use f-strings:

python
me.text(f"Clicks: {state.clicks}")

The button component demonstrates connecting an event handler to a component. Whenever a click event is triggered by the component, the registered event handler function is called:

python
me.button("Increment", on_click=button_click)

In summary, you've learned how to define a state class, an event handler and wire them together using interactive components.

What's next

At this point, you've learned all the basics of building a Mesop app. For a step-by-step guide for building a real-world Mesop application, check out the DuoChat Codelab:

DuoChat Codelab

Getting Started Installing (`docs/getting-started/installing.md`)

Installing

If you are familiar with setting up a Python environment, then run the following command in your terminal:

shell
pip install mesop

If you're not familiar with setting up a Python environment, follow one of the options below.

Colab is a free hosted Jupyter notebook product provided by Google.

Try Mesop on Colab:

B. Command-line

If you'd like to run Mesop locally on the command-line, follow these steps.

Pre-requisites: Make sure you have Python version 3.10 or later installed by running:

sh
python --version

If you don't, please download Python.

Create a venv environment

  1. Open the terminal and navigate to a directory: cd foo

  2. Create a virtual environment by using venv, which will avoid Python environment issues. Run:

sh
python -m venv .venv
  1. Activate your virtual environment:

    === "macOS and Linux"

    text
    ```sh
     source .venv/bin/activate
     ```

    === "Windows command prompt"

    text
    ```sh
     .venv\Scripts\activate.bat
     ```

    === "Windows PowerShell"

    text
    ```sh
     .venv\Scripts\Activate.ps1
     ```

Once you've activated the virtual environment, you will see ".venv" at the start of your terminal prompt.

  1. Install mesop:
shell
pip install mesop

Upgrading

To upgrade Mesop, run the following command:

sh
pip install --upgrade mesop

If you are using requirements.txt or pyproject.toml to manage your dependency versions, then you should update those.

Next steps

Follow the quickstart guide to learn how to create and run a Mesop app:

Quickstart

Guides Auth (`docs/guides/auth.md`)

Auth

Mesop is designed to be auth provider agnostic. You can integrate any auth library you prefer, whether it's on the client-side (JavaScript) or server-side (Python). This guide covers several approaches so you can choose the one that fits your deployment.

Approach Best for
Google Cloud IAP Apps already on App Engine or Cloud Run
Firebase Authentication Apps that need social sign-in or multi-provider auth
Cookies Persisting session tokens or structured data across requests; includes a username/password login example (experimental)
HTTP Basic Auth Internal tools where a browser pop-up is acceptable and no login UI is needed

Important: Regardless of which approach you choose, always enforce authorization on the server side in your event handlers. Never rely solely on client-visible state to gate privileged actions. See the state management security guide for more details.

Google Cloud IAP

Google Cloud Identity-Aware Proxy (IAP) handles authentication entirely at the infrastructure level before any request reaches your Mesop app. This is the simplest approach if you are already deploying to App Engine or Cloud Run — there is no login UI to build and no tokens to manage in your app.

How it works:

  1. You enable IAP on your App Engine or Cloud Run service in the Google Cloud Console.
  2. Google handles the sign-in flow and only forwards authenticated requests to your app.
  3. Each forwarded request includes a signed X-Goog-IAP-JWT-Assertion header containing the user's identity.
  4. Your app verifies this header and trusts the identity inside it.

Prerequisites:

  • A Google Cloud project with App Engine or Cloud Run
  • IAP enabled on your service (see the IAP docs for setup)
  • google-auth Python package (pip install google-auth)

Finding your audience string:

The audience value for JWT verification depends on your service type:

  • App Engine: /projects/PROJECT_NUMBER/apps/PROJECT_ID
  • Cloud Run: /projects/PROJECT_NUMBER/global/backendServices/SERVICE_ID

You can find PROJECT_NUMBER and PROJECT_ID in the Google Cloud Console. For Cloud Run, get SERVICE_ID from the IAP settings page.

Example:

python
# requirements.txt: google-auth

import os
import mesop as me
import google.auth.transport.requests
import google.oauth2.id_token
from flask import request

# Set via environment variable in production.
# Format for App Engine:  /projects/PROJECT_NUMBER/apps/PROJECT_ID
# Format for Cloud Run:   /projects/PROJECT_NUMBER/global/backendServices/SERVICE_ID
IAP_AUDIENCE = os.environ["IAP_AUDIENCE"]

_http_request = google.auth.transport.requests.Request()


def verify_iap_jwt(iap_jwt: str) -> dict:
    return google.oauth2.id_token.verify_token(
        iap_jwt,
        _http_request,
        audience=IAP_AUDIENCE,
        certs_url="https://www.gstatic.com/iap/verify/public_key",
    )


@me.stateclass
class State:
    user_email: str


def on_load(e: me.LoadEvent):
    iap_jwt = request.headers.get("X-Goog-IAP-JWT-Assertion")
    if not iap_jwt:
        # Header is absent — IAP is not in front of this request.
        # This can happen during local development. Gate accordingly.
        me.navigate("/unauthorized")
        return
    try:
        payload = verify_iap_jwt(iap_jwt)
    except Exception:
        me.navigate("/unauthorized")
        return
    me.state(State).user_email = payload["email"]


@me.page(path="/", on_load=on_load)
def page():
    state = me.state(State)
    me.text(f"Hello, {state.user_email}")


@me.page(path="/unauthorized")
def unauthorized_page():
    me.text("Access denied.")

Local development tip: The IAP header is absent when running locally. You can detect this and either skip the check (during development only) or inject a fake header via a local reverse proxy like Cloud Run proxy.

Firebase Authentication

This guide will walk you through the process of integrating Firebase Authentication with Mesop using a custom web component.

Pre-requisites: You will need to create a Firebase account and project. It's free to get started with Firebase and use Firebase auth for small projects, but refer to the pricing page for the most up-to-date information.

We will be using three libraries from Firebase to build an end-to-end auth flow:

  • Firebase Web SDK: Allows you to call Firebase services from your client-side JavaScript code.
  • FirebaseUI Web: Provides a simple, customizable auth UI integrated with the Firebase Web SDK.
  • Firebase Admin SDK (Python): Provides server-side libraries to integrate Firebase services, including Authentication, into your Python applications.

Let's dive into how we will use each one in our Mesop app.

Web component

The Firebase Authentication web component is a custom component built for handling the user authentication process. It's implemented using Lit, a simple library for building lightweight web components.

JS code

javascript title="firebase_auth_component.js"
--8<-- "mesop/examples/web_component/firebase_auth/firebase_auth_component.js"

What you need to do:

  • Replace firebaseConfig with your Firebase project's config. Read the Firebase docs to learn how to get yours.
  • Replace the URLs signInSuccessUrl with your Mesop page path and tosUrl and privacyPolicyUrl to your terms and services and privacy policy page respectively.

How it works:

  • This creates a simple and configurable auth UI using FirebaseUI Web.
  • Once the user has signed in, then a sign out button is shown.
  • Whenever the user signs in or out, the web component dispatches an event to the Mesop server with the auth token, or absence of it.
  • See our web component docs for more details.

Python code

python title="firebase_auth_component.py"
--8<-- "mesop/examples/web_component/firebase_auth/firebase_auth_component.py"

How it works:

  • Implements the Python side of the Mesop web component. See our web component docs for more details.

Integrating into the app

Let's put it all together:

python title="firebase_auth_app.py"
--8<-- "mesop/examples/web_component/firebase_auth/firebase_auth_app.py"

Note You must add firebase-admin to your Mesop app's requirements.txt file

How it works:

  • The firebase_auth_app.py module integrates the Firebase Auth web component into the Mesop app. It initializes the Firebase app, defines the page where the Firebase Auth web component will be used, and sets up the state to store the user's email.
  • The on_auth_changed function is triggered whenever the user's authentication state changes. If the user is signed in, it verifies the user's ID token and stores the user's email in the state. If the user is not signed in, it clears the email from the state.

Next steps

Congrats! You've now built an authenticated app with Mesop from start to finish. Read the Firebase Auth docs to learn how to configure additional sign-in options and much more.

Cookies

!!! warning "Experimental"
The cookie API is experimental. The interface may change in future releases.

Mesop provides a first-class cookie API so you can persist small pieces of data (session tokens, user preferences, etc.) in the browser without managing raw Set-Cookie headers yourself.

!!! note "MESOP_COOKIE_SECRET_KEY required"
Set MESOP_COOKIE_SECRET_KEY before starting Mesop:

text
```sh
MESOP_COOKIE_SECRET_KEY=your-random-secret mesop main.py
```

Generate a strong key:

```sh
python -c "import secrets; print(secrets.token_hex(32))"
```

How it works

Mesop event handlers run inside a streaming response (SSE or WebSockets). HTTP response headers — including Set-Cookie — are committed to the client before the event-handler body executes, so cookies cannot be set directly on the handler response. Instead, cookies are applied via a lightweight two-step protocol:

  1. Your event handler calls me.set_cookie().
  2. Mesop encodes the pending cookie operations into a short-lived itsdangerous-signed token and sends an ApplyCookiesCommand to the browser. Any server worker that holds the same MESOP_COOKIE_SECRET_KEY can verify and redeem the token.
  3. The Mesop client POSTs the token to /__apply-cookies, which verifies the signature, marks the token's nonce as used (single-use within a process), and responds with the Set-Cookie headers.

This is transparent to your application code.

`@me.cookieclass` — structured cookies

The recommended API is @me.cookieclass, which works like @me.stateclass but for cookies. Fields are JSON-serialised automatically.

python
import mesop as me

@me.cookieclass
class SessionCookie:
    username: str = ""
    role: str = "guest"

Call me.cookie(SessionCookie) inside on_load or any event handler. If the cookie is absent or unparseable, a fresh default instance is returned — no exception is raised.

python
def on_load(e: me.LoadEvent):
    session = me.cookie(SessionCookie)
    if session.username:
        state = me.state(State)
        state.logged_in = True
        state.username = session.username

Pass a cookieclass instance to me.set_cookie(). The cookie name is derived from the class, and the fields are JSON-serialised automatically.

python
def on_login(e: me.ClickEvent):
    me.set_cookie(
        SessionCookie(username="alice", role="admin"),
        max_age=3600,   # seconds; omit for a session cookie
    )

When secure is omitted it auto-detects HTTPS — so the same code works in local HTTP development and in production HTTPS deployments without any extra configuration.

python
def on_logout(e: me.ClickEvent):
    me.delete_cookie(SessionCookie)   # pass the class, not a string

Signed and encrypted cookies

By default @me.cookieclass stores the JSON value in plain text — readable in browser DevTools. For cookies that carry sensitive data you can add tamper-protection or full encryption by setting signed=True or encrypted=True on the decorator.

Option Protection Contents visible? Extra dependency
(default) None Yes (plain JSON) —
signed=True HMAC — tampering detected on read Yes (Base64) —
encrypted=True Fernet — contents hidden No pip install cryptography
python
# Tamper-proof — contents still visible in DevTools, but any modification
# is detected and the cookie is silently discarded on the next read.
@me.cookieclass(signed=True)
class SessionCookie:
    username: str = ""

# Fully encrypted — contents hidden; requires pip install cryptography.
@me.cookieclass(encrypted=True)
class SessionCookie:
    username: str = ""

The read (me.cookie()) and write (me.set_cookie()) calls are identical regardless of the protection level — the signing or encryption is applied transparently.

Low-level API

If you need full control over the cookie name and value format — for example when storing an opaque session token generated by an external library — use me.set_cookie() with explicit string arguments.

When to use which form: prefer the cookieclass form for structured data. Use the low-level form only for raw opaque values.

python
token = me.cookie("session_id")         # low-level: raw string value (or "" if absent)
session = me.cookie(SessionCookie)      # high-level: typed cookieclass instance
python
# Low-level: explicit name + raw string value.
me.set_cookie(
    "session_id",
    "abc123",
    max_age=3600,        # None → session cookie
    path="/",
    domain=None,         # None → current domain
    secure=None,         # None → auto-detect HTTPS (recommended default)
    httponly=True,       # hidden from JavaScript
    samesite="Lax",      # "Lax" | "Strict" | "None"
)

# High-level: cookieclass instance — name and serialisation handled automatically.
me.set_cookie(SessionCookie(username="alice"), max_age=3600)
python
me.delete_cookie("session_id")      # low-level: explicit string name
me.delete_cookie(SessionCookie)     # high-level: class lookup

Username and Password

This example combines a username/password login form with cookie-backed sessions. After a successful login the verified username is written to a signed cookie, so the session persists across page refreshes and new tabs — unlike a state-only approach, which is cleared on any hard navigation.

Warning: Rolling your own auth is error-prone. For production apps with many users, prefer a managed solution like Google Cloud IAP or Firebase Authentication.

Prerequisites:

  • werkzeug (already installed with Mesop — used for password hashing)

Step 1 — Store hashed passwords

Never store plaintext passwords. Use werkzeug.security to hash them at setup time and store only the hash.

python
from werkzeug.security import generate_password_hash

# Run this once to generate the hash, then store it securely.
print(generate_password_hash("my-secret-password"))

Step 2 — Full example

text
/* Detailed source-code truncated for AI context efficiency. */

Multiple protected pages

For apps with several protected pages, add on_load=on_load to each page and check state.username at the top:

python
def _require_login() -> bool:
    state = me.state(State)
    if not state.username:
        _render_login_form()
        return False
    return True

@me.page(path="/dashboard", on_load=on_load)
def dashboard():
    if not _require_login():
        return
    # Protected dashboard content...

@me.page(path="/settings", on_load=on_load)
def settings():
    if not _require_login():
        return
    # Protected settings content...

Security checklist:

  • Never store plaintext passwords — use generate_password_hash / check_password_hash.
  • Never store password hashes in source code or committed files — use a secret manager (e.g. GCP Secret Manager, AWS Secrets Manager) or a .env file listed in .gitignore for local development.
  • Use @me.cookieclass(signed=True) (or encrypted=True) to prevent cookie tampering or content inspection.
  • Validate and sanitize all user inputs (see the state management guide).
  • For genuine access control, use Google Cloud IAP or Firebase Authentication.

Security notes

  • httponly=True (default): The cookie is not accessible to JavaScript, which mitigates XSS-based session theft. It is still visible in browser DevTools.
  • secure=None (default): Auto-detects HTTPS — True in production, False on local HTTP. Pass secure=True to enforce HTTPS-only regardless of environment.
  • samesite="Lax" (default): Protects against most CSRF attacks. Use "Strict" for additional protection at the cost of some UX friction on cross-site navigations.
  • Cookie value size: Browsers limit individual cookies to ~4 KB. Use server-side session storage (database, Redis, etc.) for larger payloads and store only an opaque session ID in the cookie.
  • Sensitive data: Use @me.cookieclass(encrypted=True) to hide cookie contents, or store only an opaque token and keep sensitive data server-side.
  • Cookie value visibility during apply: When me.set_cookie() is called, the pending cookie values are embedded in the ApplyCookiesCommand token that passes through browser JavaScript before being POSTed to /__apply-cookies. The token is HMAC-signed but not encrypted, so the values are Base64-readable in that brief window. For cookies that must never be visible to JavaScript (e.g. an httponly session token whose raw value is sensitive), use @me.cookieclass(encrypted=True) — the value is Fernet-encrypted before it ever leaves the server, so the token carries only ciphertext.

HTTP Basic Auth

HTTP Basic Auth lets the browser display a native username/password pop-up with no login page required. It is a good fit for internal tools where:

  • The entire app should be protected (no public pages)
  • You want the simplest possible setup with zero UI code
  • The app is served over HTTPS (required — Basic Auth sends credentials in plaintext otherwise)

Warning: Do not use HTTP Basic Auth over plain HTTP. Always deploy behind HTTPS.

How it works:

A WSGI middleware wrapper intercepts every request before it reaches Mesop. If the Authorization header is missing or the credentials are wrong, the middleware returns a 401 response immediately and the browser shows its built-in sign-in dialog. Valid requests are passed through to Mesop as normal.

Prerequisites:

  • werkzeug (already installed with Mesop)

Example:

text
/* Detailed source-code truncated for AI context efficiency. */

Run with Gunicorn:

text
gunicorn --bind 0.0.0.0:8080 'your_module:app'

Security checklist:

  • Only use HTTP Basic Auth behind HTTPS — credentials are only Base64-encoded, not encrypted.
  • Store only hashed passwords, never plaintext.
  • Never put credentials in source code or committed files. Use your platform's secret management solution — for example GCP Secret Manager, AWS Secrets Manager, or a .env file that is listed in .gitignore for local development.
  • Consider combining with an IP allowlist at the network/load-balancer level for extra protection.

Guides Debugging (`docs/guides/debugging.md`)

Debugging

This guide will show you several ways of debugging your Mesop app:

You can use the first two methods to debug your Mesop app both locally and in production, and the last one to debug your Mesop app locally.

Debugging with server logs

If your Mesop app is not working properly, we recommend checking the server logs first.

If you're running Mesop locally, you can check the terminal. If you're running Mesop in production, you will need to use your cloud provider's console to check the logs.

Debugging with Chrome DevTools

Chrome DevTools is a powerful set of web developer tools built directly into the Google Chrome browser. It can be incredibly useful for debugging Mesop applications, especially when it comes to inspecting the client-server interactions.

Here's how you can use Chrome DevTools to debug your Mesop app:

  1. Open your Mesop app in Google Chrome.

  2. Right-click anywhere on the page and select "Inspect" or use the keyboard shortcut to open Chrome DevTools:

    • Windows/Linux: Ctrl + Shift + I
    • macOS: Cmd + Option + I
  3. To debug general errors:

    • Go to the Console tab.
    • Look for any console error messages.
    • You can also modify the log levels to display Mesop debug logs by clicking on "Default levels" and selecting "Verbose".
  4. To debug network issues:

    • Go to the Network tab.
    • Reload your page to see all network requests.
    • Look for any failed requests (they'll be in red).
    • Click on a request to see detailed information about headers, response, etc.
  5. For JavaScript errors:

    • Check the Console tab for any error messages.
    • You can set breakpoints in your JavaScript code using the Sources tab.

Remember, while Mesop abstracts away much of the frontend complexity, using these tools can still be valuable for debugging and optimizing your app's performance.

Debugging with VS Code

VS Code is recommended for debugging your Mesop app, but you can also debug Mesop apps in other IDEs.

Pre-requisite: Ensure VS Code is downloaded.

  1. Install the Python Debugger VS Code extension.

  2. Include the following in your .vscode/launch.json:

json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Python: Remote Attach",
      "type": "python",
      "request": "attach",
      "connect": { "host": "localhost", "port": 5678 },
      "pathMappings": [
        { "localRoot": "${workspaceFolder}", "remoteRoot": "." }
      ],
      "justMyCode": true
    }
  ]
}
  1. At the top of your Mesop app (e.g. main.py), including the following snippet to start the debug server:
py
import debugpy

debugpy.listen(5678)
  1. Connect to your debug server by going to the Run & Debug tab in VS Code and selecting "Python: Remote Attach".

Congrats you are now debugging your Mesop app!

To learn more about Python debugging in VS code, read VS Code's Python debugging guide.


Guides Deployment (`docs/guides/deployment.md`)

Deployment

We recommend Google Cloud Run or Hugging Face Spaces, which both have a free tier.

This section describes how to run your Mesop application using the following
platforms:

If you can run your Mesop app on Docker, you should be able
to run it on many other cloud platforms, such as
Hugging Face Spaces.

Example application

Let's start with an example application which will consist of the following files:

  • main.py
  • requirements.txt

main.py

This file contains your Mesop application code:

py title="main.py"
import mesop as me

@me.page(title="Home")
def home():
  me.text("Hello, world")

requirements.txt

This file specifies the Python dependencies needed. You may need to add additional
dependencies depending on your use case.

title="requirements.txt"
mesop
gunicorn

Cloud Run

We recommend using Google Cloud Run because it's easy to
get started and there's a free tier.

Pre-requisites

You will need to create a Google Cloud account and install the gcloud CLI. See the official documentation for detailed instructions.

Procfile

Create Procfile to configure gunicorn to run Mesop.

title="procfile"
web: gunicorn --bind :8080 main:me

The --bind: 8080 will run Mesop on port 8080.

The main:me syntax is $(MODULE_NAME):$(VARIABLE_NAME): (see Gunicorn docs for more details):

  • Because the Mesop python file is main.py, the module name is main.
  • By convention, we do import mesop as me so the me refers to the main Mesop
    library module which is also a callable (e.g. a function) that conforms to WSGI.

Deploy to Google Cloud Run

In your terminal, go to the application directory, which has the files listed above.

Run the following command:

sh
gcloud run deploy

Follow the instructions and then you should be able to access your deployed app.

Session Affinity

If you're running Mesop with MESOP_STATE_SESSION_BACKEND=memory,
then you will want to enable session affinity in order to utilize the memory backend efficiently.

The command should be:

sh
gcloud run services update $YOUR_SERVICE --session-affinity

By default gunicorn allocates one worker, but you should double check that gunicorn is
configured correctly for the memory backend.

App Engine

This section describes deployment to Google App Engine using
their flexible environments feature.

Pre-requisites

You will need to create a Google Cloud account and install the gcloud CLI. See the official documentation for detailed instructions.

You will also need to run:

sh
gcloud app create --project=[YOUR_PROJECT_ID]
gcloud components install app-engine-python

app.yaml

Create app.yaml to configure App Engine to run Mesop.

yaml title="app.yaml"
runtime: python
env: flex
entrypoint: gunicorn -b :$PORT main:me

runtime_config:
  operating_system: ubuntu22
  runtime_version: "3.10"

manual_scaling:
  instances: 1

resources:
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

Deploy to App Engine

In your terminal, go to the application directory, which has the files listed above.

Run the following command:

sh
gcloud app deploy

Follow the instructions and then you should be able to access your deployed app.

Docker

If you can run your Mesop app on Docker, you should be able
to run it on many other cloud platforms.

Pre-requisites

Make sure Docker and Docker Compose are installed.

Dockerfile

docker title="dockerfile"
--8<-- "docs/assets/hf/example.Dockerfile"

docker-compose.yaml

title="docker-compose.yaml"
services:
  mesop-app:
    build: .
    ports:
      - "8080:8080"

Run Docker image

In your terminal, go to the application directory, which has the files listed above.

Run the following command:

sh
docker-compose up -d

Alternatively, if you do not want to use Docker Compose, you can run:

sh
docker build -t mesop-app . && docker run -d -p 8080:8080 mesop-app

You should now be able to view your Mesop app at http://localhost:8080.

Deploying behind a reverse proxy

If you serve your Mesop app behind a reverse proxy at a custom path, set MESOP_BASE_URL_PATH to that path. For example, if the proxy routes requests under /myapppath to Mesop, run the server with:

sh
MESOP_BASE_URL_PATH=/myapppath docker-compose up -d

All routes, including the UI endpoint and static assets, will then be served from /myapppath.

Trusting proxy forwarding headers

When Mesop runs behind a reverse proxy (e.g. nginx, a cloud load balancer), the proxy typically communicates the original client's protocol and host via X-Forwarded-Proto and X-Forwarded-Host headers. Mesop needs to trust these headers so that its CSRF/CSWSH origin checks compare against the correct external URL.

Mesop automatically enables proxy-header trust on the following managed platforms:

  • Google Cloud Run (detected via K_SERVICE)
  • Google App Engine (detected via GAE_APPLICATION)
  • Kubernetes (detected via KUBERNETES_SERVICE_HOST)

For other reverse-proxy setups (e.g. nginx on a VPS), set the environment variable explicitly:

sh
MESOP_TRUST_PROXY_HEADERS=true docker-compose up -d

Security warning: Only set MESOP_TRUST_PROXY_HEADERS=true when Mesop is genuinely behind a trusted reverse proxy. In a direct (non-proxied) deployment, enabling this setting allows callers to spoof X-Forwarded-* headers and bypass origin-based security checks.

Hugging Face Spaces

Hugging Face Spaces has a free tier that gives you 2 vCPU and 16GB RAM, which is plenty
for running Mesop applications that leverage generative AI APIs.

Pre-requisites

This section assumes you already have a free Hugging Face Space account.

Create new Space

Go to https://huggingface.co/spaces and click
Create new Space.

Name your app and use Docker SDK

Name the Space mesop-hello-world you want and select the apache-2.0 license.

Next select the Docker SDK with a blank template.

CPU Basic and Create Space

Next make sure that you are using the free CPU Basic plan. Then click Create Space.

Clone your Hugging Face Space Git Repository

Example command using Git over SSH:

sh
git clone [email protected]:spaces/<user-name>/mesop-hello-world
cd mesop-hello-world

Note: You'll need to have an SSH key configured on Hugging Face. See https://huggingface.co/docs/hub/en/security-git-ssh.

Create main.py

This is the same main.py file shown earlier, except we need to allow Hugging Face to
iframe our Mesop app.

py title="main.py"
import mesop as me

@me.page(
  title="Home",
  security_policy=me.SecurityPolicy(
    allowed_iframe_parents=["https://huggingface.co"]
  ),
)
def home():
  me.text("Hello, world")

Create requirements.txt

This file is the same as the generic Docker setup:

title="requirements.txt"
mesop
gunicorn

Create Dockerfile

This file is the same as the generic Docker setup:

docker title="dockerfile"
--8<-- "docs/assets/hf/example.Dockerfile"

Add app_port in README.md

Next we will need to open port 8080 which we specified in the Dockerfile. This is
done through a config section in the README.md.

md title="readme.md"
---
title: Mesop Hello World
emoji: 🐠
colorFrom: blue
colorTo: purple
sdk: docker
pinned: false
license: apache-2.0
app_port: 8080
---

Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference

Deploy to Hugging Face Spaces

The commands to commit your changes and push to the Hugging Face Spaces git repository
are:

sh
git add -A
git commit -m "Add hello world Mesop app"
git push origin main

View deployed app

Congratulations! You should now be able to view your app on Hugging Face Spaces.

The URL should be something like this:

text
https://huggingface.co/spaces/<user-name>/mesop-hello-world

Guides Event Handlers (`docs/guides/event-handlers.md`)

Event Handlers

Event handlers are a core part of Mesop and enables you to handle user interactions by writing Python functions which are called by the Mesop framework when a user event is received.

How it works

Let's take a look at a simple example of an event handler:

py title="simple event handler"
def counter():
    me.button("Increment", on_click=on_click)

def on_click(event: me.ClickEvent):
    state = me.state(State)
    state.count += 1

@me.stateclass
class State:
    count: int = 0

Although this example looks simple, there's a lot going on under the hood.

When the counter function is called, it creates an instance of the button component and binds the on_click event handler to it. Because components (and the entire Mesop UI) is serialized and sent to the client, we need a way of serializing the event handler so that when the button is clicked, the correct event handler is called on the server.

We don't actually need to serialize the entire event handler, rather we just need to compute a unique id for the event handler function.

Because Mesop has a stateless architecture, we need a way of computing an id for the event handler function that's stable across Python runtimes. For example, the initial page may be rendered by one Python server, but another server may be used to respond to the user event. This stateless architecture allows Mesop apps to be fault-tolerant and enables simple scaling.

Types of event handlers

Regular functions

These are the simplest and most common type of event handlers used. It's essentially a regular Python function which is called by the Mesop framework when a user event is received.

py title="regular function"
def on_click(event: me.ClickEvent):
    state = me.state(State)
    state.count += 1

Generator functions

Python Generator functions are a powerful tool, which allow you to yield multiple times in a single event handler. This allows you to render intermediate UI states.

py title="generator function"
def on_click(event: me.ClickEvent):
    state = me.state(State)
    state.count += 1
    yield
    time.sleep(1)
    state.count += 1
    yield

You can learn more about real-world use cases of the generator functions in the Interactivity guide.

???+ info "Always yield at the end of a generator function"
If you use a yield statement in your event handler, then the event handler will be a generator function. You must have a yield statement at the end of the event handler (or each return point), otherwise not all of your code will be executed.

Async generator functions

Python async generator functions allow you to do concurrent work using Python's async and await language features. If you are using async Python libraries, you can use these types of event handlers.

py title="async generator function"
async def on_click(event: me.ClickEvent):
    state = me.state(State)
    state.count += 1
    yield
    await asyncio.sleep(1)
    state.count += 1
    yield

For a more complete example, please refer to the Async section of the Interactivity guide.

???+ info "Always yield at the end of an async generator function"
Similar to a regular generator function, an async generator function must have a yield statement at the end of the event handler (or each return point), otherwise not all of your code will be executed.

Patterns

Reusing event handler logic

You can share event handler logic by extracting the common logic into a separate function. For example, you will often want to use the same logic for the on_enter event handler for an input component and the on_click event handler for a "send" button component.

py title="reusing event handler logic"
def on_enter(event: me.InputEnterEvent):
    state = me.state(State)
    state.value = event.value
    call_api()

def on_click(event: me.ClickEvent):
    # Assumes that state.value has been set by an on_blur event handler
    call_api()

def call_api():
    # Put your common event handler logic here
    pass

If you want to reuse event handler logic between generator functions, you can use the yield from syntax. For example, let's say call_api in the above example is a generator function. You can use yield from to reuse the event handler logic:

py title="reusing event handler logic for generator functions"
def on_enter(event: me.InputEnterEvent):
    state = me.state(State)
    state.value = event.value
    yield from call_api()

def on_click(event: me.ClickEvent):
    yield from call_api()

def call_api():
    # Do initial work
    yield
    # Do more work
    yield

Boilerplate-free event handlers

If you're building a form-like UI, it can be tedious to write a separate event handler for each form field. Instead, you can use this pattern which utilizes the key attribute that's available in most events and uses Python's built-in setattr function to dynamically update the state:

py title="boilerplate-free event handlers"
def app():
  me.input(label="Name", key="name", on_blur=update_state)
  me.input(label="Address", key="address", on_blur=update_state)

@me.stateclass
class State:
  name: str
  address: str

def update_state(event: me.InputBlurEvent):
  state = me.state(State)
  setattr(state, event.key, event.value)

The downside of this approach is that you lose type safety. Generally, defining a separate event handler, although more verbose, is easier to maintain.

Troubleshooting

Avoid using closure variables in event handler

One subtle mistake when building a reusable component is having the event handler use a closure variable, as shown in the following example:

py title="bad example of using closure variable"
@me.component
def link_component(url: str):
   def on_click(event: me.ClickEvent):
     me.navigate(url)
  return me.button(url, on_click=on_click)

def app():
    link_component("/1")
    link_component("/2")

The problem with this above example is that Mesop only stores the last event handler. This is because each event handler has the same id which means that Mesop cannot differentiate between the two instances of the same event handler.

This means that both instances of the link_component will refer to the last on_click instance which references the same url closure variable set to "/2". This almost always produces the wrong behavior.

Instead, you will want to use the pattern of relying on the key in the event handler as demonstrated in the following example:

py title="good example of using key"
@me.component
def link_component(url: str):
   def on_click(event: me.ClickEvent):
     me.navigate(event.key)
  return me.button(url, key=url, on_click=on_click)

For more info on using component keys, please refer to the Component Key docs.

Next steps

Explore advanced interactivity patterns like streaming and async:

Interactivity

Guides Interactivity (`docs/guides/interactivity.md`)

Interactivity

This guide continues from the event handlers guide and explains advanced interactivity patterns for dealing with common use cases such as calling a slow blocking API call or a streaming API call.

Intermediate loading state

If you are calling a slow blocking API (e.g. several seconds) to provide a better user experience, you may want to introduce a custom loading indicator for a specific event.

Note: Mesop has a built-in loading indicator at the top of the page for all events.

python
--8<-- "mesop/examples/docs/loading.py"

In this example, our event handler is a Python generator function. Each yield statement yields control back to the Mesop framework and executes a render loop which results in a UI update.

Before the first yield statement, we set is_loading to True on state so we can show a spinner while the user is waiting for the slow API call to complete.

Before the second (and final) yield statement, we set is_loading to False, so we can hide the spinner and then we add the result of the API call to state so we can display that to the user.

Tip: you must have a yield statement as the last line of a generator event handler function. Otherwise, any code after the final yield will not be executed.

Streaming

This example builds off the previous Loading example and makes our event handler a generator function so we can incrementally update the UI.

python
--8<-- "mesop/examples/docs/streaming.py"

Async

If you want to do multiple long-running operations concurrently, then we recommend you to use Python's async and await.

python
--8<-- "mesop/examples/async_await.py"

Troubleshooting

User input race condition

If you notice a race condition with user input (e.g. input or textarea) where sometimes the last few characters typed by the user is lost, you are probably unnecessarily setting the value of the component.

See the following example using this anti-pattern :warning::

py title="bad example: setting the value and using on_input"
@me.stateclass
class State:
  input_value: str

def app():
  state = me.state(State)
  me.input(value=state.input_value, on_input=on_input)

def on_input(event: me.InputEvent):
  state = me.state(State)
  state.input_value = event.value

The problem is that the input value now has a race condition because it's being set by two sources:

  1. The server is setting the input value based on state.
  2. The client is setting the input value based on what the user is typing.

There's several ways to fix this which are shown below.

Option 1: Use `on_blur` instead of `on_input`

You can use the on_blur event instead of on_input to only update the input value when the user loses focus on the input field.

This is also more performant because it sends much fewer network requests.

py title="good example: setting the value and using on_input"
@me.stateclass
class State:
  input_value: str

def app():
  state = me.state(State)
  me.input(value=state.input_value, on_input=on_input)

def on_input(event: me.InputEvent):
  state = me.state(State)
  state.input_value = event.value

Option 2: Do not set the input value from the server

If you don't need to set the input value from the server, then you can remove the value attribute from the input component.

py title="good example: not setting the value" hl_lines="7"
@me.stateclass
class State:
  input_value: str

def app():
  state = me.state(State)
  me.input(on_input=on_input)

def on_input(event: me.InputEvent):
  state = me.state(State)
  state.input_value = event.value

Option 3: Use two separate variables for initial and current input value

If you need set the input value from the server and you need to use on_input, then you can use two separate variables for the initial and current input value.

py title="good example: using two separate variables for initial and current input value" hl_lines="9"
@me.stateclass
class State:
  initial_input_value: str = "initial_value"
  current_input_value: str

@me.page()
def app():
  state = me.state(State)
  me.input(value=state.initial_input_value, on_input=on_input)

def on_input(event: me.InputEvent):
  state = me.state(State)
  state.current_input_value = event.value

Next steps

Learn about layouts to build a customized UI.

Layouts

Guides Labs (`docs/guides/labs.md`)

Labs

Mesop Labs is built on top of the core Mesop framework and may evolve in the future.

Using Labs

You will need to add an import statement to use labs:

python
import mesop.labs as mel

The code inside Mesop Labs is intended to be simple to understand so you can copy and customize it as needed.


Guides Layouts (`docs/guides/layouts.md`)

Layouts

Mesop takes an unopinionated approach to layout. It does not impose a specific layout on your app so you can build custom layouts. The crux of doing layouts in Mesop is the Box component and using the Style API which are Pythonic wrappers around the CSS layout model.

For most Mesop apps, you will use some combination of these types of layouts:

Common layout examples

To interact with the examples below, open the Mesop Layouts Colab:

Rows and Columns

Basic Row

python title="basic row"
def row():
    with me.box(style=me.Style(display="flex", flex_direction="row")):
        me.text("Left")
        me.text("Right")

Row with Spacing

python title="row with spacing"
def row():
    with me.box(style=me.Style(display="flex", flex_direction="row", justify_content="space-around")):
        me.text("Left")
        me.text("Right")

Row with Alignment

python title="row with alignment"
def row():
    with me.box(style=me.Style(display="flex", flex_direction="row", align_items="center")):
        me.box(style=me.Style(background="red", height=50, width="50%"))
        me.box(style=me.Style(background="blue", height=100, width="50%"))

Rows and Columns

python title="rows and columns"
def app():
    with me.box(style=me.Style(display="flex", flex_direction="row", gap=16, height="100%")):
        column(1)
        column(2)
        column(3)

def column(num: int):
    with me.box(style=me.Style(
        flex_grow=1,
        background="#e0e0e0",
        padding=me.Padding.all(16),
        display="flex",
        flex_direction="column",
    )):
        me.box(style=me.Style(background="red", height=100))
        me.box(style=me.Style(background="blue", flex_grow=1))

Grids

Side-by-side Grid

python title="side-by-side grid"
def grid():
    # 1fr means 1 fraction, so each side is the same size.
    # Try changing one of the 1fr to 2fr and see what it looks like
    with me.box(style=me.Style(display="grid", grid_template_columns="1fr 1fr")):
        me.text("A bunch of text")
        me.text("Some more text")
python title="header body footer grid"
def app():
    with me.box(style=me.Style(
        display="grid",
        grid_template_rows="auto 1fr auto",
        height="100%"
    )):
        # Header
        with me.box(style=me.Style(
            background="#f0f0f0",
            padding=me.Padding.all(24)
        )):
            me.text("Header")

        # Body
        with me.box(style=me.Style(
            padding=me.Padding.all(24),
            overflow_y="auto"
        )):
            me.text("Body Content")
            # Add more body content here

        # Footer
        with me.box(style=me.Style(
            background="#f0f0f0",
            padding=me.Padding.all(24)
        )):
            me.text("Footer")
python title="sidebar layout"
def app():
    with me.box(style=me.Style(
        display="grid",
        grid_template_columns="250px 1fr",
        height="100%"
    )):
        # Sidebar
        with me.box(style=me.Style(
            background="#f0f0f0",
            padding=me.Padding.all(24),
            overflow_y="auto"
        )):
            me.text("Sidebar")

        # Main content
        with me.box(style=me.Style(
            padding=me.Padding.all(24),
            overflow_y="auto"
        )):
            me.text("Main Content")

Responsive UI

This is similar to the Grid Sidebar layout above, except on smaller screens, we will hide the sidebar. Try resizing the browser window and see how the UI changes.

Learn more about responsive UI in the viewport size docs.

python
def app():
    is_desktop = me.viewport_size().width > 640
    with me.box(style=me.Style(
        display="grid",
        grid_template_columns="250px 1fr" if is_desktop else "1fr",
        height="100%"
    )):
        if is_desktop:
          # Sidebar
          with me.box(style=me.Style(
              background="#f0f0f0",
              padding=me.Padding.all(24),
              overflow_y="auto"
          )):
              me.text("Sidebar")

        # Main content
        with me.box(style=me.Style(
            padding=me.Padding.all(24),
            overflow_y="auto"
        )):
            me.text("Main Content")

Learn more

For a real-world example of using these types of layouts, check out the Mesop Showcase app:

To learn more about flexbox layouts (rows and columns), check out:

To learn more about grid layouts, check out:


Guides Multi Pages (`docs/guides/multi-pages.md`)

Multi-Pages

You can define multi-page Mesop applications by using the page feature you learned from Core Concepts.

Multi-page setup

python
import mesop as me

@me.page(path="/1")
def page1():
    me.text("page 1")

@me.page(path="/2")
def page2():
    me.text("page 2")

Learn more about page configuration in the page API doc.

If you have multiple pages, you will typically want to navigate from one page to another when the user clicks a button. You can use me.navigate("/to/path") to navigate to another page.

Example:

python
--8<-- "mesop/examples/docs/multi_page_nav.py"

Note: you can re-use state across pages. See how the above example uses the State#count value across pages.


Guides Performance (`docs/guides/performance.md`)

Performance

Occasionally, you may run into performance issues with your Mesop app. Here are some tips to help you improve your app's performance.

Determine the root cause

The first step in debugging performance issues is to identify the cause of the issue. Follow the Debugging with DevTools guide and use the Console and Network tab to identify the issue.

Common performance bottlenecks and solutions

Optimizing state size

If you notice with Chrome DevTools that you're sending very large network payloads between client and server, it's likely that your state is too large.

Because the state object is serialized and sent back and forth between the client and server, you should try to keep the state object reasonably sized. For example, if you store a very large string (e.g. base64-encoded image) in state, then it will degrade performance of your Mesop app.

The following are recommendations to help you avoid large state payloads:

Store state in memory

Mesop has a feature that allows you to store state in memory rather than passing the
full state on every request. This may help improve performance when dealing with large
state objects. The caveat is that, storing state in memory contains its own set of
problems that you must carefully consider. See the config section
for details on how to use this feature.

If you are running Mesop on a single replica or you can enable session affinity, then this is a good option.

Store state externally

You can also store state outside of Mesop using a database or a storage service. This is a good option if you have a large amount of state data. For example, rather than storing images in the state, you can store them in a bucket service like Google Cloud Storage and send signed URLs to the client so that it can directly fetch the images without going through the Mesop server.

Handling high user load

If you notice that your Mesop app is running slowly when you have many concurrent users, you can try to scale your Mesop app.

Increase the number of replicas

To handle more concurrent users, you can scale your Mesop app horizontally by increasing the number of replicas (instances) running your application. This can be achieved through various cloud services that offer autoscaling features:

  1. Use a managed service like Google Cloud Run, which automatically scales your app based on traffic. Follow Mesop's Cloud Run deployment guide for details.

  2. Manually adjust the number of replicas to a higher number.

  3. Tune gunicorn settings. If you're using gunicorn to serve your Mesop app, you can adjust gunicorn settings to increase the number of workers. This can help to increase the number of concurrent users your Mesop app can handle.

Whichever platform you choose, make sure to configure the replica settings to match your app's performance requirements and budget constraints.


Guides Server Integration (`docs/guides/server-integration.md`)

Server integration

Mesop allows you to integrate Mesop with other Python web servers like FastAPI or Flask by mounting the Mesop app which is a WSGI app.

This enables you to do things like:

  • Serve local files (e.g. images)
  • Provide API endpoints (which can be called by the web component, etc.)

API

The main API for doing this integration is the create_wsgi_app function.

::: mesop.server.wsgi_app.create_wsgi_app

FastAPI example

For a working example of using Mesop with FastAPI, please take a look at this repo:
https://github.com/wwwillchen/mesop-fastapi

Note: you can apply similar steps to use any other web framework that allows you to mount a WSGI app.


Guides State Management (`docs/guides/state-management.md`)

State Management

State management is a critical element of building interactive apps because it allows you store information about what the user did in a structured way.

Basic usage

You can register a class using the class decorator me.stateclass which is like a dataclass with special powers:

python
@me.stateclass
class State:
  val: str

You can get an instance of the state class inside any of your Mesop component functions by using me.state:

py
@me.page()
def page():
    state = me.state(State)
    me.text(state.val)

Use immutable default values

Similar to regular dataclasses which disallow mutable default values, you need to avoid mutable default values such as list and dict for state classes. Using mutable default values can result in leaking state across sessions which can be a serious privacy issue.

You MUST use immutable default values or use dataclasses field initializer or not set a default value.

???+ success "Good: immutable default value"
Setting a default value to an immutable type like str is OK.

text
```py
  @me.stateclass
  class State:
    a: str = "abc"
  ```

???+ failure "Bad: mutable default value"

text
The following will raise an exception because dataclasses prevents you from using mutable collection types like `list` as the default value because this is a common footgun.

```py
@me.stateclass
class State:
  a: list[str] = ["abc"]
```

If you set a default value to an instance of a custom type, an exception will not be raised, but you will be dangerously sharing the same mutable instance across sessions which could cause a serious privacy issue.

```py
@me.stateclass
class State:
  a: MutableClass = MutableClass()
```

???+ success "Good: default factory"

text
If you want to set a field to a mutable default value, use default_factory in the `field`  function from the dataclasses module to create a new instance of the mutable default value for each instance of the state class.

```py
from dataclasses import field

@me.stateclass
class State:
  a: list[str] = field(default_factory=lambda: ["abc"])
```

???+ success "Good: no default value"

text
If you want a default of an empty list, you can just not define a default value and Mesop will automatically define an empty list default value.

For example, if you write the following:

```py
@me.stateclass
class State:
  a: list[str]
```

It's the equivalent of:

```py
@me.stateclass
class State:
  a: list[str] = field(default_factory=list)
```

How State Works

me.stateclass is a class decorator which tells Mesop that this class can be retrieved using the me.state method, which will return the state instance for the current user session.

If you are familiar with the dependency injection pattern, Mesop's stateclass and state API is essentially a minimalist dependency injection system which scopes the state object to the lifetime of a user session.

Under the hood, Mesop is sending the state back and forth between the server and browser client so everything in a state class must be serializable.

Serialization

Understanding what can and cannot be serialized in Mesop state is critical to avoiding runtime errors. This section explains which types are supported and provides guidance on handling complex objects.

Serializable Types

Mesop supports serialization for the following types:

Primitive Types

  • int - Integer numbers
  • float - Floating-point numbers
  • str - Strings
  • bool - Boolean values
python
@me.stateclass
class State:
  count: int
  temperature: float
  name: str
  is_enabled: bool

Collections

  • list[T] - Lists (including nested lists)
  • dict[K, V] - Dictionaries (including nested dicts)
  • set[T] - Sets
python
@me.stateclass
class State:
  items: list[str]
  scores: dict[str, int]
  unique_ids: set[int]
  nested_list: list[list[str]]
  nested_dict: dict[str, dict[str, bool]]

Date and Time

  • datetime.datetime - Date and time objects
  • datetime.date - Date objects
python
from datetime import datetime, date

@me.stateclass
class State:
  created_at: datetime
  birthday: date

Binary Data

  • bytes - Binary data
python
@me.stateclass
class State:
  file_content: bytes

Special Types

  • pandas.DataFrame - Pandas DataFrames (requires pandas to be installed)
  • pydantic.BaseModel - Pydantic models and subclasses
  • mesop.components.uploader.UploadedFile - Uploaded files from the uploader component
python
import pandas as pd
from pydantic import BaseModel

class UserModel(BaseModel):
  name: str
  age: int

@me.stateclass
class State:
  data_frame: pd.DataFrame
  user: UserModel

Nested State Classes

You can nest dataclasses within your state class, and they will be automatically serialized:

python
from dataclasses import dataclass

@dataclass
class Address:
  street: str
  city: str

@me.stateclass
class State:
  address: Address
  addresses: list[Address]

Non-Serializable Types

Most types that are not in the serializable types list above cannot be serialized. This includes (but is not limited to):

  • Functions, lambdas, and methods
  • File handles, sockets, and I/O objects
  • Database connections and cursors
  • Thread and lock objects
  • Protocol buffers and custom complex objects

If you get a serialization error, refer to the serializable types section above for supported types.

Workarounds for Unsupported Types

If you need to work with types that aren't directly serializable, here are some strategies:

1. Break down into primitives

Extract the data you need into serializable primitive types:

python
# ❌ Bad: storing complex object
@me.stateclass
class State:
  api_response: requests.Response  # Not serializable

# ✅ Good: extract the data you need
@me.stateclass
class State:
  response_text: str
  status_code: int
  headers: dict[str, str]

def on_click(e: me.ClickEvent):
  state = me.state(State)
  response = requests.get("https://api.example.com")
  state.response_text = response.text
  state.status_code = response.status_code
  state.headers = dict(response.headers)

2. Serialize to JSON or bytes

Convert complex objects to JSON strings or bytes:

python
@me.stateclass
class State:
  config_json: str  # Store as JSON string

def on_load(e: me.LoadEvent):
  state = me.state(State)
  complex_config = load_complex_config()
  state.config_json = json.dumps(complex_config)

def use_config():
  state = me.state(State)
  config = json.loads(state.config_json)
  # Use config...

3. Use Pydantic models

Pydantic models are serializable and can handle complex nested structures:

python
from pydantic import BaseModel

class UserProfile(BaseModel):
  name: str
  email: str
  settings: dict[str, str]
  created_at: datetime

@me.stateclass
class State:
  profile: UserProfile  # Pydantic models are serializable

def on_login(e: me.ClickEvent):
  state = me.state(State)
  # Pydantic can serialize most Python types
  state.profile = UserProfile(
    name="Alice",
    email="[email protected]",
    settings={"theme": "dark"},
    created_at=datetime.now()
  )

Note: When using Pydantic models, you get less granular state diffing compared to using primitives directly. This is fine if you don't store a large amount of state data.

Known Issues

Be aware of these known serialization limitations:

Multiple state classes

You can use multiple classes to store state for the current user session.

Using different state classes for different pages or components can help make your app easier to maintain and more modular.

py
@me.stateclass
class PageAState:
    ...

@me.stateclass
class PageBState:
    ...

@me.page(path="/a")
def page_a():
    state = me.state(PageAState)
    ...

@me.page(path="/b")
def page_b():
    state = me.state(PageBState)
    ...

Under the hood, Mesop is managing state classes based on the identity (e.g. module name and class name) of the state class, which means that you could have two state classes named "State", but if they are in different modules, then they will be treated as separate state, which is what you would expect.

Nested State

You can also have classes inside of a state class as long as everything is serializable:

python
class NestedState:
  val: str

@me.stateclass
class State:
  nested: NestedState

def app():
  state = me.state(State)

Note: you only need to decorate the top-level state class with @me.stateclass. All the nested state classes will automatically be wrapped.

Nested State and dataclass

Sometimes, you may want to explicitly decorate the nested state class with dataclass because in the previous example, you couldn't directly instantiate NestedState.

If you wanted to use NestedState as a general dataclass, you can do the following:

python
@dataclass
class NestedState:
  val: str = ""

@me.stateclass
class State:
  nested: NestedState

def app():
  state = me.state(State)

Reminder: because dataclasses do not have default values, you will need to explicitly set default values, otherwise Mesop will not be able to instantiate an empty version of the class.

Now, if you have an event handler function, you can do the following:

py
def on_click(e):
    response = call_api()
    state = me.state(State)
    state.nested = NestedState(val=response.text)

If you didn't explicitly annotate NestedState as a dataclass, then you would get an error instantiating NestedState because there's no initializer defined.

Security & Best Practices

Validate input before updating state

Event handlers receive data directly from the browser. Always validate and sanitize that data before writing it into state. Trusting it blindly can lead to corrupt state or unexpected behaviour.

???+ failure "Bad: storing unvalidated input"

text
```python
def on_submit(e: me.ClickEvent):
    state = me.state(State)
    # Directly copying user-supplied value with no checks
    state.age = int(state.age_input)   # crashes if input is not a number
    state.email = state.email_input    # no format validation
```

???+ success "Good: validate before writing to state"

text
```python
def on_submit(e: me.ClickEvent):
    state = me.state(State)

    # Validate age
    try:
        age = int(state.age_input)
    except ValueError:
        state.error = "Age must be a number."
        return
    if age < 0 or age > 150:
        state.error = "Age must be between 0 and 150."
        return

    # Validate email (basic check)
    if "@" not in state.email_input or "." not in state.email_input:
        state.error = "Enter a valid email address."
        return

    state.age = age
    state.email = state.email_input
    state.error = ""
```

This is especially important for fields that end up in database queries, file paths, or external API calls.

Validate state before sending to external services

State holds what the user submitted across one or more interactions. Before passing state values to a database, API, or any external service, validate them again at the point of use. Never assume that because data is in state it is already safe.

???+ failure "Bad: using state values directly in a database call"

text
```python
def on_save(e: me.ClickEvent):
    state = me.state(State)
    # No validation — anything could be in state.username
    db.execute("INSERT INTO users (name) VALUES (?)", (state.username,))
```

???+ success "Good: re-validate at the service boundary"

text
```python
import re

def on_save(e: me.ClickEvent):
    state = me.state(State)

    username = state.username.strip()
    if not username or not re.match(r"^[a-zA-Z0-9_]{3,30}$", username):
        state.error = "Username must be 3–30 alphanumeric characters."
        return

    db.execute("INSERT INTO users (name) VALUES (?)", (username,))
```

Use an ORM or parameterised queries to avoid SQL injection. If you call an external HTTP API, validate that URLs or IDs embedded in state are well-formed before constructing the request.

Be careful what you store in state

Mesop serializes state and sends it between the server and the browser on every interaction. This has two important consequences:

State is visible to the client. The serialized state travels over the network and can be inspected by a determined user. Do not store secrets, private keys, raw passwords, or other sensitive credentials in state.

???+ failure "Bad: storing a secret in state"

text
```python
@me.stateclass
class State:
    api_key: str          # ❌ Will be sent to the browser
    password_hash: str    # ❌ Leaks implementation details
```

???+ success "Good: keep secrets server-side"

text
```python
import os

# Read once at startup and keep in server memory, not in state
_API_KEY = os.environ["MY_API_KEY"]

@me.stateclass
class State:
    is_authenticated: bool   # ✅ Only the outcome goes in state

def call_api():
    # Use the module-level constant, never state
    return requests.get("https://api.example.com", headers={"Authorization": _API_KEY})
```

State size affects performance. Everything in state is serialized and sent on every round-trip. Avoid storing large blobs, full API responses, or entire datasets. Store only the minimum data needed to render the UI. See the performance guide for details.

???+ failure "Bad: storing a full API response"

text
```python
@me.stateclass
class State:
    raw_response: str   # ❌ Could be megabytes of JSON
```

???+ success "Good: store only what the UI needs"

text
```python
@me.stateclass
class State:
    result_count: int
    result_titles: list[str]   # ✅ Just the fields displayed
```

State is not a substitute for authentication. State is scoped to a user session, but Mesop itself does not authenticate users. Do not rely on a flag like state.is_admin = True as your sole access control mechanism. Enforce authorization on the server side (e.g., check the session token in your event handler) before performing privileged operations.

???+ failure "Bad: trusting state for authorization"

text
```python
def on_delete(e: me.ClickEvent):
    state = me.state(State)
    if state.is_admin:   # ❌ Client can manipulate this
        db.delete_all()
```

???+ success "Good: verify authorization server-side"

text
```python
def on_delete(e: me.ClickEvent):
    state = me.state(State)
    user = get_current_user(state.session_token)
    if not user or not user.has_permission("delete"):
        state.error = "Permission denied."
        return
    db.delete_all()
```

State does not persist across sessions. When a user's session ends (e.g., they close the tab or the server restarts), state is lost. Do not use state as durable storage. Persist anything important — user preferences, saved work, etc. — to a database or other backend store before the session ends.

Tips

State performance issues

Take a look at the performance guide to learn how to identify and fix State-related performance issues.

Next steps

Event handlers complement state management by providing a way to update your state in response to user interactions.

Event handlers

Guides Static Assets (`docs/guides/static-assets.md`)

Static Assets

Mesop allows you to specify a folder for storing static assets that will be served by
the Mesop server.

This feature provides a simple way to serving images, favicons, CSS stylesheets, and
other files without having to rely on CDNs, external servers, or mounting Mesop onto
FastAPI/Flask.

Enable a static folder

This feature can be enabled using environment variables.

Full descriptions of these two settings can be found on the config page.

Enabling a static folder named "assets"

This will make the files in the assets directory accessible from the Mesop server
at /static.

Mesop will look for the assets directory relative to your current working directory.
In this case, /some/path/mesop-app/assets.

bash
cd /some/path/mesop-app
MESOP_STATIC_FOLDER=assets mesop main.py

Here is another example:

Mesop will look for the assets directory relative to your current working directory.
In this case, /some/path/assets.

bash
cd /some/path
MESOP_STATIC_FOLDER=assets mesop mesop-app/main.py

Enabling a static folder named "assets" and URL path of /assets

This will make the files in the assets directory accessible from the Mesop server
at /assets. For example: https://example.com/assets.

bash
MESOP_STATIC_FOLDER=assets MESOP_STATIC_URL_PATH=/assets mesop main.py

Using a .env file

You can also specify the environment variables in a .env file. This file should be
placed in the same directory as the main.py file.

title=".env"
MESOP_STATIC_FOLDER=assets
MESOP_STATIC_URL_PATH=/assets

Then you can run the Mesop command like this:

bash
mesop main.py

Example use cases

Here are a couple examples that use the static assets feature.

This example shows you how to load an image to use as a logo for your app.

Let's assume you have a directory like this:

  • static/logo.png
  • main.py
  • requirements.txt

Then you can reference your logo in your Mesop app like this:

python title="main.py"
import mesop as me

@me.page()
def foo():
  me.image(src="/static/logo.png")

Use a custom favicon

This example shows you how to use a custom favicon in your Mesop app.

Let's assume you have a directory like this:

  • static/favicon.ico
  • main.py
  • requirements.txt

If you have a static folder enabled, Mesop will look for a favicon.ico file in your
static folder. If the file exists, Mesop will use your custom favicon instead of the
default Mesop favicon.

Load a Tailwind stylesheet

This example shows you how to use Tailwind CSS with Mesop.

Let's assume you have a directory like this:

  • static/tailwind.css
  • tailwind_input.css
  • tailwind.config.js
  • main.py
  • requirements.txt

You can import the CSS into your page using the stylesheets parameter on @me.page.

python title="main.py"
import mesop as me

@me.page(stylesheets=["/static/tailwind.css"])
def foo():
  with me.box(classes="bg-gray-800"):
    me.text("Mesop with Tailwind CSS.")

Tailwind is able to extract the CSS properties from your Mesop main.py file. This does
not work for all cases. If you are dynamically generating CSS properties using string concatenation/formatting, then Tailwind may not be able to determine which properties
to include. In that case, you may need to manually add these classes to the safelist.

js title="tailwind.config.js"
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["main.py"],
  theme: {
    extend: {},
  },
  plugins: [],
  safelist: [],
};

This is just the base Tailwind input file.

css title="tailwind_input.css"
@tailwind base;
@tailwind components;
@tailwind utilities;

The command to generate the output Tailwind CSS is:

bash
# This assumes you have the tailwindcss CLI installed. If not, see
# https://tailwindcss.com/docs/installation
npx tailwindcss -i ./tailwind_input.css -o ./static/tailwind.css

Guides Testing (`docs/guides/testing.md`)

Testing

This guide covers the recommended approach for testing Mesop applications using Playwright, a popular browser automation and testing framework.

However, you can use any other testing framework and there's no official testing framework for Mesop.

Testing Philosophy

Because Mesop is a full-stack UI framework we recommend doing integration tests to cover critical app functionality. We also recommend separating core business logic into separate Python modules, which do not depend on Mesop. This way you can easily unit test your business logic as well as reuse the business logic as part of scripts or other binaries besides Mesop.

Playwright Example

We will walk through mesop-playwright-testing repo which contains a simple Mesop counter app and a Playwright test written in TypeScript (node.js). Although Playwright has a Python flavor, we recommend using the Playwright node.js flavor because it has better testing support. Even if you're not familiar with JavaScript or TypeScript, it's extremely easy to write Playwright tests because you can generate your tests by clicking through the UI.

The testing example repo's README.md contains instructions for setting up your environment and running the tests.

Playwright config

The playwright configuration used is similar to the default one, however we change a few configurations specific for Mesop.

For example, in playwright.config.ts, we configure Mesop as the local dev server:

ts
webServer: {
    command: "mesop app.py",
    url: "http://127.0.0.1:32123",
    reuseExistingServer: !process.env.CI,
  },

This will launch the Mesop app server at the start of the tests.

We also added the following configurations to make writing and debugging tests easier:

ts
use: {
    /* Base URL to use in actions like `await page.goto('/')`. */
    baseURL: "http://127.0.0.1:32123",

    /* See https://playwright.dev/docs/trace-viewer */
    trace: "retain-on-failure",

    // Capture screenshot after each test failure.
    screenshot: "on",

    // Retain video on test failure.
    video: "retain-on-failure",
  },

Running and debugging a test

Try changing the test so that it fails. For example, in app.spec.ts change "Count=1" to "Count=2" and then run the tests: npx playwright test.

The test will fail (as expected) and a new browser page should be opened with the test failure information. You can click on the failing test and view the screenshot, video and trace. This is very helpful in figuring out why a test failed.

Writing a test

As mentioned above, it's very easy to write Playwright tests because you can generate your tests by clicking through the UI. Even if you're not familiar with JavaScript/TypeScript, you will be able to generate most of the test code by clicking through the UI and copying the generated test code.

???+ tip "Use the Playwright VS Code extension"

text
You can use the Playwright VS Code extension to directly generate test code in your file. You can also run and debug tests from VS Code as well.

Learn more

We recommend reading Playwright's docs which are easy to follow and contain much more information on writing browser tests.

You can also look at Mesop's own tests written with Playwright.


Guides Theming (`docs/guides/theming.md`)

Theming

Mesop has early-stage support for theming so you can support light theme and dark theme in a Mesop application.

Dark Theming

For an actual example of using Mesop's theming API to support light theme and dark theme, we will look at the labs chat component which itself is written all in Python built on top of lower-level Mesop components.

Theme toggle button

Inside the chat component, we've defined an icon button to toggle the theme so users can switch between light and dark theme:

py
def toggle_theme(e: me.ClickEvent):
    if me.theme_brightness() == "light":
      me.set_theme_mode("dark")
    else:
      me.set_theme_mode("light")

with me.content_button(
    type="icon",
    style=me.Style(position="absolute", right=0),
    on_click=toggle_theme,
):
    me.icon("light_mode" if me.theme_brightness() == "dark" else "dark_mode")

Using theme colors

You could define custom style logic to explicitly set the color based on the theme:

py
def container():
  me.box(
    style=me.Style(
      background="white" if me.theme_brightness() == "light" else "black"
    )
  )

But this would be pretty tedious, so you can use theme variables like this:

py
def container():
  me.box(style=me.Style(background=me.theme_var("background")))

This will use the appropriate background color for light theme and dark theme.

Default theme mode

Finally, we want to use the default theme mode to "system" which means we will use the user's preferences for whether they want dark theme or light theme. For many users, their operating systems will automatically switch to dark theme during night time.

Note: Mesop currently defaults to light theme mode but will eventually default to system theme mode in the future.

On our demo page with the chat component, we have a page on_load event handler defined like this:

py
def on_load(e: me.LoadEvent):
  me.set_theme_mode("system")

Theme Density

You can set the visual density of the Material components. By default, Mesop uses the least visually dense setting, i.e.

py
me.set_theme_density(0) # 0 is the least dense

You can configure the density as an integer from 0 (least dense) to -4 (most dense). For example, if you want a medium-dense UI, you can do the following:

py
def on_load(e: me.LoadEvent):
  me.set_theme_density(-2) # -2 is more dense than the default


@me.page(on_load=on_load)
def page():
  ...

API

::: mesop.features.theme.set_theme_density
::: mesop.features.theme.set_theme_mode
::: mesop.features.theme.theme_brightness
::: mesop.features.theme.theme_var
::: mesop.features.theme.ThemeVar


Guides Web Security (`docs/guides/web-security.md`)

Web Security

Static file serving

Mesop allows serving JS and CSS files located within the Mesop app's file subtree to support web components.

Security Warning: Do not place any sensitive or confidential JS and CSS files in your Mesop project directory. These files may be inadvertently exposed and served by the Mesop web server, potentially compromising your application's security.

JavaScript Security

At a high-level, Mesop is built on top of Angular which provides built-in security protections and Mesop configures a strict Content Security Policy.

Specifics:

  • Mesop APIs do not allow arbitrary JavaScript execution in the main execution context. For example, the markdown component sanitizes the markdown content and removes active HTML content like JavaScript.
  • Mesop's default Content Security Policy prevents arbitrary JavaScript code from executing on the page unless it passes Angular's Trusted Types polices.

Iframe Security

To prevent clickjacking, Mesop apps, when running in prod mode (the default mode used when deployed), do not allow sites from any other origins to iframe the Mesop app.

Note: pages from the same origin as the Mesop app can always iframe the Mesop app.

If you want to allow a trusted site to iframe your Mesop app, you can explicitly allow list the sources which can iframe your app by configuring the security policy for a particular page.

Example

py
import mesop as me


@me.page(
  path="/allows_iframed",
  security_policy=me.SecurityPolicy(
    allowed_iframe_parents=["https://google.com"],
  ),
)
def app():
  me.text("Test CSP")

You can also use wildcards to allow-list multiple subdomains from the same site, such as: https://*.example.com.

Cross Origin Opener Policy

Mesop sets this value to unsafe-none, which is the default value. It is recommended to set this to same-origin to ensure process isolation from random domains. In most cases, your Mesop app should run without any issues.

For more information, see MDN and XS Leaks Wiki.

Cross-site WebSocket Hijacking (CSWSH)

Unlike regular HTTP requests, browsers do not enforce same-origin policy for WebSocket connections. This means a malicious page on another origin could open a WebSocket to your Mesop server and interact with it on behalf of a logged-in user.

To prevent this, Mesop validates the Origin header on every WebSocket upgrade request and rejects connections whose origin does not match the server's own URL. This check mirrors the existing CSRF protection on the SSE (POST /__ui__) endpoint.

The check is skipped in debug mode to support environments like Colab where the UI and the server may run on different origins.

Deployments behind a reverse proxy

If your app is deployed behind a reverse proxy (Cloud Run, App Engine, Kubernetes, nginx, etc.), the server's apparent URL may differ from the external-facing URL seen by browsers. In that case the origin check could incorrectly reject legitimate connections.

Mesop solves this by reading the X-Forwarded-Proto and X-Forwarded-Host headers set by the proxy, but only when proxy-header trust is enabled. See Trusting proxy forwarding headers in the deployment guide for details.

API

You can configure the security policy at the page level. See SecurityPolicy on the Page API docs.


Internal Architecture (`docs/internal/architecture.md`)

Architecture

This doc is meant to provide an overview of how Mesop is structured as a framework. It's not necessary to know this information as a developer using Mesop, but if you're developing Mesop's codebase, then this is helpful in laying out the lay of the land.

At the heart of Mesop is two subsystems:

Terminology

  • Downstream - This refers to the synced version of Mesop inside of Google ("google3 third-party"). Although almost all the code is shared between the open-source and internal version of Mesop, there's many considerations in maintaining parity between these two versions, particularly with regards to toolchain.
  • Component vs component instance - A component typically refers to the Python factory function that creates a component instance (e.g. me.box()). A component instance refers to a specific component created by a component function and is represented as a Component proto. Other UI frameworks oftentimes give a different name for an instance (i.e. Element) of a component, but for simplicity and explicitness, I will refer to these instances as component instance or component tree (for the entire tree of component instances) in this doc.

Life of a Mesop request

Initial page load

When a user visits a Mesop application, the following happens:

  1. The user visits a path on the Mesop application, e.g. "/" (root path), in their browser.
  2. The Mesop client-side web application (Angular) is bootstrapped and sends an InitRequest to the server.
  3. The Mesop server responds with a RenderEvent which contains a fully instantiated component tree.
  4. The Mesop client renders the component tree. Every Mesop component instance corresponds to 1 or more Angular component instance.

User interactions

If the user interacts with the Mesop application (e.g. click a button), the following happens:

  1. The user triggers a UserEvent which is sent to the server. The UserEvent includes: the application state (represented by the States proto), the event handler id to trigger, the key of the component interacted with (if any), and the payload value (e.g. for checkbox, it's a bool value which represents the checked state of the checkbox).
  2. The server does the following:
    1. Runs a first render loop in tracing mode (i.e. instantiate the component tree from the root component of the requested path). This discovers any event handler functions. In the future, this trace can also be used to calculate the before component tree so we can calculate the diff of the component tree to minimize the network payload.
    2. Updates the state by feeding the user event to the event handler function discovered in the previous step.

    Note: there's a mapping layer between the UserEvent proto and the granular Python event type. This provides a nicer API for Mesop developers then the internal proto representation.

    1. Runs a second render loop to generate the new component tree given the new state. After the first render loop, each render loop results in a RenderEvent sent to the client.
    2. In the streaming case, we may run the render loop and flush it down via Server-Sent Events many times.
  3. The client re-renders the Angular application after receiving each RenderEvent.

Python Server

Flask is a minimalist Python server framework that conforms to WSGI (Web Server Gateway Interface), which is a Python standard that makes it easy for web servers (oftentimes written in other languages like C++) to delegate requests to a Python web framework. This is particularly important in the downstream case because we rely on an internal HTTP server to serve Mesop applications.

For development purposes (i.e. using the CLI), we use Werkzeug, which is a WSGI library included with Flask.

Web Client

Mesop's Web client consists of three main parts:

  • Core: Includes the root Angular component and singleton services like Channel. This part is fairly small and is the critical glue between the rest of the client layer and the server.
  • Mesop Components: Every Mesop component has its own directory under /components

Note: this includes both the Python API and the Angular implementation for developer convenience.

  • Dev Tools: Mesop also comes with a basic set of developer tools, namely the components and log panels. The components panel allows Mesop developers to visualize the component tree. The log panel allows Mesop developers to inspect the application state and component tree values.

Static assets

  • Using the regular CLI, the web client static assets (i.e. JS binary, CSS, images) are served from the Python server. This simplifies deployment of Mesop applications by reducing version skew issues between the client and server.
  • In uncompiled mode (using the dev CLI), the web client is served from the web devserver. This is convenient because it builds faster than the regular/compiled mode and it allows live-reloading when developing the client codebase.

Tooling

Outside of the mesop/ directory are various tools used to build, test and document the Mesop framework. However, anything needed to actually run a Mesop application should be located within mesop/. The three main tools inside the codebase are:

  • Build tooling - these are in build_defs/ which contains various Bazel bzl files and tools which is forked from the Angular codebase. The build toolchain is described in more detail on the toolchain doc.
  • Component generator - inside generator/ is a mini-library and CLI tool to generate Mesop components from existing Angular components, specifically Angular Material, although with some modifications it could support more generic Angular components. The generator modifies the codebase so that nothing in generator/ is actually needed when running a Mesop applications.
  • Docs - Mesop's doc site is built using Material for Mkdocs and is what you are looking at right now.

Internal Ci (`docs/internal/ci.md`)

CI

We use GitHub actions. For all third-party GitHub actions, we must pin it to a specific hash to comply with internal policies.


Internal Codespaces (`docs/internal/codespaces.md`)

Development on Github Codespaces

Github Codespaces is a quick way to get
started with internal Mesop development. All you need to do is a click a button and a
fully configured workspace will be created for you. No need to spend time debugging
installation issues.

Github Free and Pro plans also provide a free tier,
so Codespaces is useful for writing and testing quick patches.

If using the free tier, the Codespace setup takes 20-30 minutes due to the limited
CPU available.

Create Github Codespace

You can create a Github Codespace from the Mesop Github repository page.

Wait for postCreateCommand to run

The Codespace will not be usable until the postCreateCommand has completed. You can
view the CLI output by pressing Cmd/Ctrl + Shift + P and then finding the View Creation Log option.

Set the Python environment for the Codespace

During the postCreateCommand step, you'll see a pop up asking if you want to set a new
environment for the Codespace. Select Yes here to use the virtual env that is created
as part of the postCreateCommand set up.

Run Mesop for development

Once the postCreateCommand has finished, you can now start Mesop in the terminal.

text
./scripts/cli.sh

This step takes some time for the first run.

You will see some warning messages, but it is OK to ignore them. You can also ignore the
message shown in the screenshot.

View Mesop demos

Once ./scripts/cli.sh has started the Mesop dev server, you can view the demos from
the PORTS tab.


Internal Contributing (`docs/internal/contributing.md`)

How-to Contribute

Thanks for looking into contributing to Mesop. There's many ways to contribute to Mesop:

  • Filing new issues and providing feedback on existing issues
  • Improving our docs
  • Contributing examples
  • Contributing code

All types of contributions are welcome and are a key piece to making Mesop work well as a project.

Before you begin

Sign our Contributor License Agreement

Contributions to this project must be accompanied by a
Contributor License Agreement (CLA).
You (or your employer) retain the copyright to your contribution; this simply
gives us permission to use and redistribute your contributions as part of the
project.

If you or your current employer have already signed the Google CLA (even if it
was for a different project), you probably don't need to do it again.

Visit https://cla.developers.google.com/ to see your current agreements or to
sign a new one.

Review our community guidelines

This project follows
Google's Open Source Community Guidelines.

Contributing to docs

If you want to contribute to our docs, please take a look at our docs issues. If you find any of our existing docs confusing or missing key information, please file an issue and we will see how we can improve things. We regularly spend time improving our docs because we know it's a key part of the developer experience.

Contributing examples

One of the best way of helping the Mesop project is to share what you've built! You can either add an example to our demo gallery by adding it to the demo/ directory or you can send us a link to your app running and we will include it in our docs.

Contributing code

If you'd like to contribute code, I recommend taking a look at one of our existing "starter" issues. These are issues that are good for first-time contributors as they are well-specified.

I recommend reading through the various pages in the contributing section as it will give you a sense of our project's goals.

One thing that we focus on is providing an easy-to-understand API with minimal breaking changes so we ask that any API changes are first discussed in an issue. This will help prevent wasted work because we are conservative with changing our APIs.


Internal Development (`docs/internal/development.md`)

Development

I recommend following (or at least reading) all the steps in this doc if you plan on actively developing Mesop.

Setup

Bazel/ibazel

We use Bazel as our build system. Use bazelisk which ensures the right version of Bazel is used for this project.

If ibazel breaks, but bazel works, try following these steps

TIP: If your build mysteriously fails due to an npm-related error, try running bazel clean --expunge && rm -rf node_modules. Bazel and Yarn have a cache bug when upgrading npm modules.

uv

We use uv. Follow the instructions here to install uv.

Commit hooks

  1. Install pre-commit
  2. Install pre-commit hooks for this repo: pre-commit install

Run local development

We recommend using this for most Mesop framework development.

sh
./scripts/cli.sh

NOTE: this automatically serves the angular app.

Python

Third-party packages (PIP)

If you update //build_defs/requirements.txt, run:

sh
bazel run //build_defs:pip_requirements.update

venv

To support IDE type-checking (Pylance) in VS Code, we use Aspect's rules_py which generates a venv target.

sh
bazel run //mesop/cli:cli.venv

Then, you can activate the venv:

sh
source .cli.venv/bin/activate

You will need to setup a symlink to have Python IDE support for protos:

sh
./scripts/setup_proto_py_modules.sh

Check that you're using venv's python:

sh
which python

Copy the python interpreter path and paste it into VS Code.

Finally, install third-party dependencies.

sh
pip install -r build_defs/requirements_lock.txt

NOTE: You may need to run the command with sudo if you get a permission denied error, particularly with "_distutils_hack".

Commit hooks

We use pre-commit to automatically format, lint code before committing.

Setup:

  1. Install pre-commit.
  2. Setup git hook: pre-commit install

Docs

We use Mkdocs Material to generate our docs site.

  1. Activate venv
  2. mkdocs serve

Internal Hot Reload (`docs/internal/hot-reload.md`)

Hot Reload

One of Mesop's key benefits is that it provides a fast iteration cycle through hot reload. This means whenever a Mesop developer changes their Mesop app code, their browser window will automaticall reload and execute the new app code while preserving the existing state. This isn't guaranteed to work, for example, if the State class is modified in an incompatible way, but it should work for >90% of the build-edit loops (e.g. tweaking the UI, calling new components).

How it works

See: https://github.com/mesop-dev/mesop/pull/211

Design decisions

What to reload

Right now we reload all the modules loaded by the Mesop application. However, this results in a lot of unnecessary modules being reloaded and can be quite slow if there's a heavy set of transitive dependencies.

Instead, I'm thinking we can use a heuristic where we calculate the existing package based on the file path passed in and only reload modules which are in the current package or a sub-package. Effectively this is only reloading modules within the target file's subtree.

This seems like a pretty reasonable heuristic where it reloads all the application modules without reloading the entire dependency graph. Previously I tried reloading only the module passed in via --path, however this was too limiting as it meant shared code (e.g. a navmenu) would not get hot-reloaded.

When to reload

With the previous design decision, re-executing a module should be much faster, but we still need to guard against the case where the live reload occurs too quickly in the client side. Options:

  • Wait a fixed timeout - A simple heuristic could just be to wait 500ms since in theory, all the application code (with the non-application dependnecies cached) should re-execute fairly quickly.
  • Client retry/reload - Another approach could be to retry a client-side reload N times (e.g. 3) if we get an error. The pattern could be: 1. save state to local storage, 2. trigger reload, 3. if reload results in a successful render, we clear the state OR if reload results in an error, we trigger a reload (and persist in local storage which retry attempt this is).
  • Server loop - In the common error case where the server is still re-executing the module and the client reloads, it will hit path not found because the path hasn't been registered yet. One way of mitigating this is to simply do a sleep in debug mode. We can even do an exponential backoff for the sleep (e.g. wait 300ms, 900ms, 2700ms).
  • Preferred appproach - given the trade-offs, I think Server loop is the best option as it's relatively simple to implement, robust and doesn't incur a significant delay in the happy case.

Abstracting ibazel-specific details

Since Google's internal equivalent of ibazel doesn't work exactly the same, we should treat HotReloadService as an abstract base class and then extend it for Ibazel (and the internal variant).


Internal Modes (`docs/internal/modes.md`)

Modes

There are two modes that you can run Mesop in.

Development mode (aka debug mode or editor mode)

Recommended for developers using Mesop when they are developing the apps locally. This provides good error messages and hot reloading.

  • How to run: ibazel run //mesop/cli -- --path=mesop/mesop/example_index.py
  • Angular should run in dev mode.
  • Developer Tools and Visual Editor are available.

Prod mode

Recommended when developers deploy applications built with Mesop for public serving. This is optimized for performance and provides less-detailed error messages.

  • Developer tools aren't available.
  • Angular doesn't run in dev mode.
  • How to run: bazel run //mesop/cli -- --path=mesop/mesop/example_index.py --prod

Internal New Component (`docs/internal/new-component.md`)

New Component

How-to

sh
python scripts/scaffold_component.py $component_name

API Guidelines

  • Make all arguments keyword only by putting * as the initial argument. Keyword argument is more readable, particularly for UI components which will have increasingly more optional arguments over time.
  • Model after existing APIs. For example, if we are wrapping an existing @angular/material component, we will try to mirror their API (within reason). If we are wrapping a native HTML element, we should try to expose a similar API. In some cases, we will look at other UI frameworks like Flutter for inspiration, even though we are not directly wrapping them.
  • Prefer small components. We should try to provide small native components that can be composed by content components in Python. This enables a wider range of use cases.

New events

Try to reuse the existing events when possible, but you may need to sometimes create a new event.

  1. Define the event class in //mesop/events/{event_name}.py
  2. In the same file, define an event mapper and register it: runtime().register_event(EventClass, event_mapper)

Potential exploration areas

  • Code-gen component_renderer using a shell/Python script. Initially, just run the script as-needed, but eventually can run it as part of a BUILD rule (a la @angular/components examples)

Internal Publishing (`docs/internal/publishing.md`)

Publishing

Follow these instructions for releasing a new version of Mesop publicly via PyPI (e.g. pip install mesop).

Check main branch

Before, cutting a release, you'll want to check two things:

  1. The main branch should be healthy (e.g. latest commit is green).

  2. Check the snyk dashboard to review security issues:

    • It only runs weekly so you need to click "Retest now". If there's any High security issues for a core Mesop file (e.g. anything in mesop/*), then you should address it before publishing a release.

Update version to RC

Update mesop/version.py by incrementing the version number. We follow semver.

You want to first create an RC (release candidate) to ensure that it works.

For example, if the current version is: 0.7.0, then you should increment the version to 0.8.0rc1 which will create an RC, which is treated as a pre-release by PyPI.

Merge the PR that bumps the version and then go to the next step to publish a GitHub release which will in turn publish to PyPI.

Publish GitHub release

After you've submitted the PR which bumps the version and then publish a GitHub release.

  1. Click "Choose a tag" and type in the version you just released. This will create a new Git tag.
  2. Click "Generate release notes".
  3. If this is a an RC: Click "Set as a pre-release", otherwise leave the "Set as the latest release" checked.
  4. If this is a regular (non-RC) release, click "Create a discussion for this release".
  5. Click "Publish release".

Testing locally

Pre-requisite: you will need to install uv before doing the following steps.

Dev CLI

Run the following command but replace 1.0.0 with the version that you just published:

sh
cd scripts/smoketest_app/ && uvx --refresh mesop==1.0.0 main.py

This will start the Mesop dev server and you can test that hot reload works. Double-check that the version shown is the version that you just released.

Gunicorn integration

Run the following command but replace 1.0.0 with the version that you just published:

sh
cd scripts/smoketest_app/ && uvx  --refresh --with mesop==1.0.0 gunicorn@latest main:me

This will launch Mesop under the Gunicorn server so you can make sure it works as expected.

Test on Colab

Because Colab installs from PyPI, you will need to test the RC on Colab after uploading to PyPI.

Open our Mesop Colab notebook. You will need to explicitly pip install the RC version as pip will not automatically install a pre-release version, even if it's the newest version. So change the first cell to something like:

sh
!pip install mesop==0.X.Yrc1

Tip: sometimes it takes a minute for the PyPI registry to be updated after upload, so just try again.

Then, run all the cells and make sure it works. Usually if something breaks in Colab, it's pretty obvious because the output isn't displayed, etc.

Change the version from RC to regular release

If you find an issue, then redo the above steps and create another RC candidate: 0.8.0rc1 -> 0.8.0rc2.

If all the testing looks good, then you can update mesop/version.py and change the version from RC to a regular release, for example:

0.8.0rc1 -> 0.8.0

Re-do the steps above to publish and test the release.


Internal Testing (`docs/internal/testing.md`)

Testing

Unit tests

You can run our unit tests using Bazel.

Run tests

sh
bazel test //mesop/...

E2E tests

We use Playwright as our e2e test framework. Unlike most of the stack, this isn't Bazel-ified although we'd like to eventually do this.

Run tests

shell
yarn playwright test

Debug tests

shell
yarn playwright test --debug

Internal Toolchain (`docs/internal/toolchain.md`)

Build / Toolchain

Context

Because Mesop is a Google open-source project and we want to provide a good integration with Google's internal codebase, Mesop uses Google's build system Bazel.

Although Bazel is similar to the internal tool, there's numerous differences, particularly around the ecosystems, which makes it quite a challenge to maintain Mesop for both open-source and internal builds. Nevertheless, it's important that we do this to serve both communities well.

Differences

We try to isolate as much of the differences between these two environments into the build_defs/ directory. Different versions of the same files inside build_defs/ are maintained for each environment. In particular, build_defs/defaults.bzl is meant to wrap all external rules/macros used by Mesop so we can swap it between the internal and external variants as needed.

Finally, all external dependencies, e.g. Python's requirement('$package') or NPM's @npm//$package, are referenced via an indirection to build_defs/defaults.bzl. This is because Google has a special approach to handling third-party dependencies.

Gotchas

Here's a quick list of gotchas to watch out for:

  • Do not use import * as when importing protos from TS. This prevents tree-shaking downstream.
  • Do not use any external Bazel references (e.g. @) within mesop/. Instead, reference them indirectly using a wrapper in build_defs/.
  • Avoid relying on implicit transitive dependencies, particularly for TS/NG modules.
  • Do not use raw JSON.parse, instead use jsonParse in strict_types.ts.

Angular

We rely heavily on Angular's toolchain, particularly around Bazel integration. Many of the Web-related Bazel rules, particularly for Angular/TS code was forked from github.com/angular/components.


Internal Type Checking (`docs/internal/type-checking.md`)

Type Checking

Python Type Checking

For our Python code, we use pyright as our static type checker because it has excellent IDE support in VS Code via pylance.

To run Python type-checking, run:

shell
./scripts/run_py_typecheck.sh

This will setup the pre-requisites needed for type-checking.


Internal Vs Code Remote Container (`docs/internal/vs-code-remote-container.md`)

VS Code Remote Container

VS Code Remote Containers is a quick way to get started with internal Mesop
development if you have VS Code and
Docker Desktop installed.

This approach will create a fully configured workspace, saving you time from
debugging installation issues and allowing you to start development right away.

Pre-requistes: Install VS Code and Docker

In order to use VS Code remote containers, you will need VS Code installed. You will
also need Docker Desktop (which will install Docker Engine and Docker Compose) to run
the remote containers.

Fork and Clone the Mesop repository

It is not recommended to develop locally and on remote containers using the same
folder since this may cause unexpected conflicts. Instead you should clone the
repository in a separate directory.

You can follow the instructions here on how to fork and clone a Github repository.

Share Git credentials with your container

The VS Code Dev Containers extension provides a few ways to share your local Git
credentials with your remote container.

If you cloned the Mesop repo using HTTPS, you can use a Github CLI or Git Credential Manager.

If you used SSH, then your local ssh agent will automatically be forwarded into your
remote container. All you need do is run the ssh-add command to add the ssh key you've
configured for GitHub access.

See the Sharing Git credentials with your container page for full details.

Open folder in container

Open VS Code, press Cmd/Ctrl + Shift + P, and select the Dev Containers: Open Folder in Container...
option. This will create a new workspace inside a remote container.

Wait for postCreateCommand to run

The workspace will not be usable until the postCreateCommand has completed.

Run Mesop for development

Once the postCreateCommand has finished, you can now start Mesop in the terminal.

text
./scripts/cli.sh

You will see some warning messages, but it is OK to ignore them.

You should see this message once the Mesop server is ready.

View Mesop demos

Once ./scripts/cli.sh has started the Mesop dev server, you can view the demos at
http://localhost:32123.


Web Components Api (`docs/web-components/api.md`)

Web Components API

Note: Web components were previously launched under the labs namespace (i.e. import mesop.lab as mel). As part of the v1 release, all of the web component APIs are now exposed in the main mesop namespace (i.e. import mesop as me). You can continue to use the labs namespace for the web components API until v2 (which will happen much later).

Example usage:

python
import mesop as me

@me.web_component(...)
def a_web_component():
    me.insert_web_component(...)

API

::: mesop.component_helpers.web_component.web_component

::: mesop.insert_web_component

::: mesop.WebEvent

::: mesop.slot


Web Components Troubleshooting (`docs/web-components/troubleshooting.md`)

Web Components Troubleshooting

Security policy

One of the most common issues when using web components is that you will need to relax the stringent security policy Mesop uses by default.

If you use the mesop command-line tool to run your app, you will see a detailed error message printed like this that will tell you how to fix the error:

If you are using Colab or another tool to run your Mesop app, and you can't see the terminal messages, then you can use your browser developer tools to view the console error messages.

Content security policy error messages

In your browser developer tools, you may see the following console error messages (the exact wording may differ):

script-src Error

???+ failure "script-src Console Error"
Refused to load the script 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js' because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-X-_ZR64fycojGBCDQbjpLA'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

If you see this error message, then you will need to update your page's Security Policy allowed_script_srcs property. In this example, because the "script-src" directive was violated, you will need to add the script's URL to the Security Policy like this:

py
@me.page(
    security_policy=me.SecurityPolicy(
        allowed_script_srcs=["https://cdn.jsdelivr.net"]
    )
)

???+ tip "Allow-listing sites"
You can allow-list the full URL including the path, but it's usually more convenient
to allow-list the entire site. This depends on how trustworthy the site is.

connect-src Error

???+ failure "connect-src Console Error"
zone.umd.js:2767 Refused to connect to 'https://identitytoolkit.googleapis.com/v1/projects' because it violates the following Content Security Policy directive: "default-src 'self'". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.

If you see this error message, then you will need to update your page's Security Policy allowed_connect_srcs property. In this example, because the "connect-src" directive was violated, you will need to add the URL you are trying to connect to (e.g. XHR, fetch) to the Security Policy like this:

py
@me.page(
    security_policy=me.SecurityPolicy(
        allowed_connect_srcs=["https://*.googleapis.com"]
    )
)

???+ tip "Allow-listing domains using wildcard"
You can wildcard all the subdomains for a site by using the wildcard character *.

Trusted Types Error

Trusted Types errors can come in various forms. If you see a console error message that contains TrustedHTML, TrustedScriptURL or some other variation, then you are likely hitting a trusted types error. Trusted Types is a powerful web security feature which prevents untrusted code from using sensitive browser APIs.

Unfortunately, many third-party libraries are incompatible with trusted types which means you need to disable this web security defense protection for the Mesop page which uses these libraries via web components.

???+ failure "TrustedHTML Console Error"

text
TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.

You can fix this Trusted Types error by disabling Trusted Types in the security policy like this:

py
@me.page(
    security_policy=me.SecurityPolicy(
        dangerously_disable_trusted_types=True
    )
)

Colab

Site level user permissions

Some APIs like navigator.mediaDevices.getUserMedia() require that users grant permission through a browser prompt. Colab attempts to detect if a code cell requires user permission, but this detection does not work for Mesop apps running in Colab using me.colab_run().

As a workaround, use the IPython %%javascript cell magic to trigger a user permission prompt. Once permission is granted, it applies to all cells in the notebook. For example, to request the microphone permission, create a new code cell and run the following code:

text
%%javascript
navigator.mediaDevices.getUserMedia({audio: true, video: false});

Api Commands Focus Component (`docs/api/commands/focus-component.md`)

Focus component

If you want to focus on a component, you can use me.focus_component which focuses the component with the specified key if it is focusable.

Example

python
--8<-- "mesop/examples/focus_component.py"

API

::: mesop.commands.focus_component.focus_component


Api Commands Navigate (`docs/api/commands/navigate.md`)

To navigate to another page, you can use me.navigate. This is particularly useful for navigating across a multi-page app.

Example

python
--8<-- "mesop/examples/navigate.py"

API

::: mesop.commands.navigate.navigate


Api Commands Scroll Into View (`docs/api/commands/scroll-into-view.md`)

Scroll into view

If you want to scroll a component into the viewport, you can use me.scroll_into_view which scrolls the component with the specified key into the viewport.

Example

python
--8<-- "mesop/examples/scroll_into_view.py"

API

::: mesop.commands.scroll_into_view.scroll_into_view


Api Commands Set Page Title (`docs/api/commands/set-page-title.md`)

Set page title

If you want to set the page title, you can use me.set_page_title which will
set the page title displayed on the browser tab.

This change does not persist if you navigate to a new page. The title will be
reset to the title configured in me.page.

Example

python
--8<-- "mesop/examples/set_page_title.py"

API

::: mesop.commands.set_page_title.set_page_title


Blog Posts Hello Mesop (`docs/blog/posts/hello-mesop.md`)


date: 2023-12-25

Hello, Mesop

After working on Mesop for the last two months, I'm excited to finally announce the first version of Mesop, v0.1. This is still early days for Mesop, but it's an important milestone because it represents a minimum viable tool for building UIs in Python. In case you haven't read Mesop's home page, Mesop is a Python-based UI framework that allows you to rapidly build web demos. Engineers without frontend experience can build web UIs by writing idiomatic Python code.

Why Mesop?

Mesop is in many ways a remix of many existing ideas packaged into a single cohesive UI framework, designed for Python developers. I've documented some of these goals previously, but I'll quickly recap the benefits of Mesop here:

  • Allows non-frontend engineers to rapidly build UIs for internal use cases like demos.
  • Provides a fast build-edit-refresh loop through hot reload.
  • Enables developers to benefit from the mature Angular web framework and Angular Material components.
  • Provides a flexible and composable components API that's idiomatic to Python.
  • Easy to deploy by using standard HTTP technologies like Server-Sent Events.

What's next for Mesop?

I see a few broad themes of work in the coming year or so.

Expand Mesop's component library

Mesop's current component library is a solid start but there's still gaps to support common use cases.

Areas of work:

  • Complete Angular Material component coverage. We support 13+ Angular Material components today, however there's many more that we don't support. Some of it is because I haven't had time to wrap their components, but in other cases (e.g. sidenav), I'd like to spend more time exploring the design space as it will probably require supporting some kind of multi-slot component API. Getting this API designed correctly, for not just this component but also future components, is important in the long run.

  • Support more native HTML elements/browser APIs. Right now, only Box and Text are thin wrappers around native HTML elements. However, there are other HTML elements like ``, <audio>
    and <video> that I'd like to also support. The flip side of supporting these components is enabling a way to allow Mesop end-users to upload these media contents, which there are also native browser APIs for.

  • Custom components. Some components won't belong in the standard Mesop package because it's either too experimental or too use-case specific. It would be nice to have a complete story for supporting custom components. Today, all of the components use the component helper API which wraps internal framework details like runtime. However, there still isn't a very good story for loading custom components in the Angular frontend (e.g. ComponentRenderer's type to component map) and testing them.

Make it easy to get started with Mesop

Using Mesop today requires following our internal development setup which requires dependencies like Bazel/iBazel which makes it easy to interoperate with our downstream sync, but these dependencies aren't commonly used in the Python ecosystem. Eventually, I'd like make using Mesop as simple as pip install mesop and then using Mesop's built-in CLI: mesop serve for local development and mesop deploy to deploy on a Cloud service.

Areas of work:

  • Find a suitable ibazel replacement for Hot Reload. Instead of requiring Mesop developers to sync the entire repo and building the project with Bazel and iBazel, we should distribute a ready-to-use pip package of Mesop. However, this leaves an open question of how we support hot reload without iBazel which provides: 1) a filesystem watching mechanism and 2) live reload. We'll need to investigate good open-source equivalents for each of these capabilities.

  • Provide web-based interactive demos. Many JavaScript UI frameworks provide a playground (e.g. Angular) or interactive tutorial (e.g. Solid) so that prospective developers can use the framework before going through the hassle of setting up their own local dev environment. This would also be very helpful to provide for each component as it's a lot easier to understand a component by tinkering with a live example.

Explore power use cases

Today Mesop is good for internal apps with relatively un-stringent demands in terms of UI customizability and performance. For production-grade external apps, there's several areas that Mesop would need to advance in, before it's ready.

Areas of work:

  • Optimize network payload. Right now the client sends the entire state to the server, and the server responds with the entire state and component tree. For large UIs/apps, this can result in sizable network payloads. We can optimize this by sending deltas as much as possible. For example, the server can send a delta of the state and component tree to the client. In addition, if we use POST instead of GET, we can stop using base-64 encoding which adds a significant overhead on top of Protobuf binary serialization.

  • Stateful server. Even with the above optimizations, we'd essentially preserve the current architecture, but there's some limitations in how much improvements we can make as long as we assume servers are stateless. However, if we allow stateful servers (i.e. long-lived connections between the client and server), we can use things like WebSockets and always send deltas bi-directionally, in particular from client to server which isn't possible with a stateless server. The problem with this direction, though, is that it makes deployment more complex as scaling a WebSocket-based server can be hard depending on the cloud infrastructure used. In addition, we'll need to handle new edge cases like authentication and broken WebSockets connections.

  • Optimistic UI. One of the drawbacks for server-driven UI frameworks like Mesop is that it introduces significant latency to simple user interactions. For example, if you click a button, it requires a network roundtrip before the UI is (meaningfully) updated. One way of dealing with this shortcoming is by pre-fetching the next UI state based on a user hint. For example, if a user is hovering over a button, we could optimistically calculate the state change and component tree change ahead of time before the actual click. The obvious downside to this is that optimistically executing an action is inappropriate in many cases, for example, a non-reversible action (e.g. delete) should never be optimistically done. To safely introduce this concept, we could provide an (optional) annotation for event handlers like @me.optimistic(events=[me.HoverEvent]) so develpers could opt-in.

Some of these directions are potentially mutually exclusive. For example, having a stateful server may make optimistic UI practically more difficult because a stateful server means that non-serializable state could start to creep in to Mesop applications which makes undoing optimistic UI updates tricky

There's, of course, even more directions than what I've listed here. For example, it's technically possible to compile Python into WebAssembly and run it in the browser and this could be another way of tackling latency to user interactions. However, this seems like a longer-term exploration, which is why I've left it out for now.

Interested in contributing?

If any of this excites you, please reach out. The easiest way is to raise a GitHub issue and let me know if there's something specific you'd like to contribute.


Blog Posts V1 (`docs/blog/posts/v1.md`)


date: 2025-03-19

Mesop v1

After developing Mesop for nearly a year and a half and publicly launching it on Hacker News 9 months ago, we are excited to release Mesop v1.

It's been an amazing journey working on Mesop, especially with richard-to@ who's been my 20% collaborator at Google. Despite being an extremely small core team (working on this as a 20% project!), we've been able to foster a thriving community both within Google where hundreds of Googlers have used it to build internal apps and outside of Google where it's been downloaded more than 180k times on PyPI.

What v1 means

Mesop v1 signifies our commitment to prioritizing stability and avoiding backwards-incompatible changes unless absolutely necessary—such as important security fixes or critical bug fixes.

Since we publicly launched 9 months ago, we have taken a conservative approach to making API changes, avoiding backwards-incompatible changes as much as possible. This has been key to Mesop's successful adoption both within Google and externally. In addition, Google's reputation for churn has made us careful in designing our APIs.

Almost all Mesop apps should be able to seamlessly upgrade to v1. The only backwards-incompatible change we made from v0.14 is to drop mel.text_io which has been deprecated for many months and can be replaced with mel.text_to_text as a simple one-line change.

Future of Mesop

You might have also noticed that Mesop has graduated from the Google GitHub org into its own GitHub org. Becoming a community-driven project is beneficial for the longevity of Mesop. Rest assured, Mesop continues to have the same core maintainers and the roadmap remains the same.

v1 doesn't mean the end of Mesop's development. We plan to add new components over time, maintain our dependencies, provide more examples, and generally improve the overall developer experience.

While we can’t make firm commitments, our goal is to remain on v1 for as long as possible, avoiding a jump to v2.

— wwwillchen@


Blog Posts Visual Editor (`docs/blog/posts/visual-editor.md`)


date: 2024-01-12

Visual Editor

Why?

As I began discussing Mesop with friends and colleagues, one thing that has come up is the difficulty of teaching and persuading non-frontend engineers to build UIs, even simple ones. CSS, particularly the rules around layout, can be quite challenging and off-putting.

I've developed a new visual editor for Mesop that aims to make UI building more approachable for beginners and more productive for experts.

What?

Let's take a look at the visual editor:

With the visual editor, you can:

  • Add new components into your app
  • Modify existing components
  • Visualize the component tree hierarchy
  • You can inspect existing components on the page by hovering over them and then change them in the editor panel
  • Bring Your Own components. By decorating a Python function with me.component, you've turned it into a Mesop component and you can now add it with the visual editor.

What's exciting about the visual editor is that you aren't locked into it - everytime you change a component with the visual editor, it's modifying the source code directly so you can seamlessly go back forth between a regular text editor and the visual editor to build your Mesop app.

Prior Art

Visual editors (aka WYSIWYG builders) have been around for a long time. Puck is one of the most interesting ones because of a few reasons: 1) it's open-source, 2) it's flexible (e.g. bring your own components) and 3) it's intuitive and easy-to-use.

The main issues I saw with Puck, particularly for Mesop's use case, is that it currently only supports React (and Mesop uses Angular) and Puck saves data whereas I would like Mesop's Visual Editor to directly emit/update code, which I'll explain next.

Principles

Hybrid code (not low-code)

One of the reasons why WYSIWYG builders have not gotten much traction with engineers is that they're often good for simple applications, but then you hit a wall building more complex applications.

To avoid this issue, I'm focusing on making the Visual Editor actually emit code and not just data. Essentially, the UI code that you produce from the Visual Editor should be the same as the code that you would write by hand.

Unobtrustive UI

I want Mesop app developers to do most of their work (except for the final finetuning for deployment) in the Visual Editior which means that it's important the Editor UI is un-obtrusive. Chrome DevTools is a great example of a low-key tool that many web developers keep open throughout their development - it's helpful for debugging, but then it's out of your way as you're interacting with the application.

Concretely, this means:

  • Editor UI should be collapsible
  • You should be able to "disable" the editor mode and interact with the application as a normal user.

Contextual

The visual editor should provide only the information that you need when you need it.

For example, rather than showing all the style properties in the editor panel, which would be quite overwhelming, we only show the style properties that you're using for the selected component.

Local-only

Because the Visual Editor relies on editing files in your local filesystem, I want to avoid any accidental usages out in the wild. Concretely, this means that you can only use the Visual Editor in localhost, otherwise the Mesop server will reject the editor edit requests.

What's next

There's still a lot of improvements and polishes I would like to make to the visual editor, but a few high-level ideas that I have are:

  1. Build example applications using the visual editor with a video walkthrough.
  2. Create more high-level components in Mesop Labs, which I'll introduce in an upcoming blog post, to make it even easier to build apps with the visual editor.
  3. Drag and drop components onto the page and within the page. This will provide an intuitive experience for building the UI, literally block by block.

Blog Posts Web Components (`docs/blog/posts/web-components.md`)


date: 2024-07-12

Is Mesop + Web Components the cure to Front-end fatigue?

I saw this tweet the other day and couldn't help but chuckle:

is this the thing that will finally save me from ever learning front end?https://t.co/eDgY0AfG6U— xlr8harder (@xlr8harder) June 6, 2024

At first, I thought of it as joke, but now that Mesop has launched experimental support for Web Components, I think it's plausible that Mesop with Web Components can save you from front-end fatigue.

What is Mesop?

Before we dive in, let me explain what Mesop is. Mesop is a Python UI framework focused on rapidly building AI apps. You can write a lot of kinds of apps all in Python as you can see from the examples on our home page, but when you need to, Mesop provides the flexibility of dropping down into web components so you can have fine-grained UI control and use existing JS libraries.

Avoid the builds

DHH, creator of Rails, recently gave an interview saying how he's "done with bundling" and the overall complexity of modern front-end build toolchains.

As someone who's done front-end for almost a decade, I can attest to the sentiment of feeling the pain of compiling JavaScript options. Setting up compiler configs and options can easily take hours. I want to be clear, I think a lot of these tools like TypeScript are fantastic, and the core Mesop framework itself is compiled using TypeScript and Angular's compilers.

But when it comes to rapid prototyping, I want to avoid that overhead. In our design proposal, we intentionally designed a lightweight model where you don't need to set up a complex build chain to start writing JavaScript.

Sometimes a build step is unavoidable, e.g. you're writing TypeScript, and you can still compile your JavaScript as needed.

Framework churn

The front-end ecosystem is infamous for its steady and constant churn. The good thing about building on top of web components is that it's based on web standards supported by all modern browsers. This means, that given browser makers' focus on "not breaking the web", this will be there for many years, if not decades to come.

For years, web components had a reputation of being an immature technology due to inconsistent support across browsers, but fast forward to 2024, and web components are well-supported in modern browsers and libraries built on web components like Lit, which is downloaded millions of times a week.

Minimizing front-end fatigue in Mesop

FE developers are so used to the pain and complexity of front-end development that they can forget how steep the learning curve is until someone from another domain tries to build a simple web app, and struggles with just getting the web app up and started.

Mesop app developers are mostly not front-end developers which means that reducing the complexity, especially learning curve, of building custom components is very important. In Mesop, we've designed a smooth pathway where you can get started with a little front-end knowledge and build simple custom components without learning a complex front-end framework.

What's next

Follow our X/Twitter account, @mesop_dev for more updates. We're working on improving our web component support, in particular by:

  • Creating guides for wrapping React components into Mesop web components
  • Fostering an ecosystem of open-source Mesop web components by making it easy to discover and reuse web components that other people have built.

We're excited about the potential of Mesop and Web Components to simplify front-end development. Whether it's the cure for front-end fatigue remains to be seen, but I think it offers a promising alternative to the complexity of traditional front-end development.


Blog Posts Why Mesop (`docs/blog/posts/why-mesop.md`)


date: 2024-05-13
hide:

  • toc

Why Mesop?

Mesop is a new UI framework that enables Python developers to quickly build delightful web apps in a scalable way.

Many Python UI frameworks are easy to get started with, but customizing beyond the defaults often requires diving into JavaScript, CSS, and HTML — a steep learning curve for many developers.

Mesop provides a different approach, offering a framework that's both easy to learn and enables flexible UI building, all within Python.

I want to share a couple concrete ways in which Mesop achieves this.

Build UIs with Functions (i.e. Components)

Mesop embraces a component-based philosophy where the entire UI is composed of reusable, building blocks which are called components. Using a component is as simple as calling a Python function. This approach offers several benefits:

  • Simplicity: You can use your existing Python knowledge to build UIs quickly and intuitively since components are just functions.
  • Maintainability: Complex UIs become easier to manage and understand by breaking them down into smaller, focused components.
  • Modularity: Components are self-contained, enabling easy reuse within a project or across different projects.

Here's an example of a reusable icon button component:

python
def icon_button(*, icon: str, label: str, tooltip: str, on_click: Callable):
  """Icon button with text and tooltip."""
  with me.content_button(on_click=on_click):
    with me.tooltip(message=tooltip):
      with me.box(style=me.Style(display="flex")):
        me.icon(icon=icon)
        me.text(
          label, style=me.Style(line_height="24px", margin=me.Margin(left=5))
        )

Flexibility through Layered Building Blocks

Mesop provides a range of UI building blocks, from low-level native components to high-level components.

  • Low-level components: like box, offer granular control over layout and styling. They empower you to create custom UI elements through flexible layouts like flexbox and grid.
  • High-level components: like chat, are built from low-level components and provide ready-to-use elements for common use cases, enabling rapid development.

This layered approach makes deep customization possible. This means that if you want to customize the chat component, you can fork the chat implementation because it's written entirely in Python using Mesop's public APIs.

See Mesop in Action

To demonstrate the range of UIs possible with Mesop, we built a demo gallery to showcase the types of applications you can build and the components that are available:

The demo gallery itself is a Mesop app and implemented in a few hundred lines of Python code. It demonstrates how Mesop can be used to create polished, custom UIs in a maintainable way.

Try Mesop

If this sounds intriguing, read the Getting Started guide and try building your own Mesop app. Share your feedback and contribute as we continue developing Mesop.


--- METRICS ---