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.
If you're interested in learning how to use Mesop, please read our main docs.
If you're interested in contributing to the core Mesop framework, please read our contributing guide.
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...
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
pip install mesopStep 2: Copy the example above into main.py
Step 3: Run the app
mesop main.pyLearn 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
yarn policies set-version latest- Remove the previous version
Overview (`demo/README.md`)
title: Mesop Demo Gallery emoji: 👓 colorFrom: blue colorTo: purple sdk: docker pinned: false license: apache-2.0 app_port: 8080
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:
rm -rf demo/venv && \
virtualenv --python python3 demo/venv && \
source demo/venv/bin/activate && \
pip install -r demo/requirements.txtRun
cd demomesop main.py
Generate screenshots
If you add more demos and want to re-generate screenshots, do the following steps:
- in
demo/screenshot.tschangetest.skiptotest - Run:
yarn playwright test demo/screenshot.ts - Install cwebp using
brew install webpor download from here. - From the workspace root, run:
`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.
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.
./demo/deploy_to_hf.sh ../hf_demoYou 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:
scripts/${script_name}.shExample Python script usage:
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
gcloud config set project mesop-auth-testgcloud auth application-default loginIndex (`docs/index.md`)
hide:
- navigation
- toc
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>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 tabWrite 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"
``` python
@me.stateclass
class State:
val: str
@me.page()
def page():
state = me.state(State)
me.text(state.val)
```=== "Control flow"
``` 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"
```python
--8<-- "mesop/examples/docs/streaming.py"
```=== "Composable"
``` 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(...)
```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.@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)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 any user interface you can imagineWith 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.
Seamlessly integrate JS with web components
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"
```python
--8<-- "mesop/examples/web_component/quickstart/counter_component.py"
```=== "counter_component.js"
```python
--8<-- "mesop/examples/web_component/quickstart/counter_component.js"
```Deploy your app and share in minutesMesop 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.
Google Cloud Run
Free for small apps
</a>Delightful developer experienceMesop streamlines app development with features like hot reload and strong IDE support with static types, eliminating friction and boosting productivity.
Instant hot reload
IDE support with static typesSee what others are sayingJoin developers around the world who are building AI apps in Mesop.
is this the thing that will finally save me from ever learning front
end?[https://t.co/eDgY0AfG6U](https://t.co/eDgY0AfG6U)
— 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)
— 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'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…
— 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:
- Create a new directory for your project:
mkdir duochat
cd duochatFollow the Mesop command-line installation guide and create a virtual environment and activate it.
Create a
requirements.txtfile with the following content:
mesop
gunicorn
anthropic
google-generativeai- Install the dependencies:
pip install -r requirements.txtSetting Up the Main Application
Let's start by creating a basic Mesop application. Create main.py and add the following code:
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:
mesop main.pyNavigate 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:
- Get a Google Gemini API Key and use the Gemini API free tier.
- Get an Anthropic API Key and setup billing. Check their docs for pricing.
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 UIComponents 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:
with me.button():
me.text("Child")You can also have multiple content components nested:
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:
@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:
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:
@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:
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."
```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."
```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:
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:
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:
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"
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.
--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:
mesop main.pyIf you've named it something else, replace
main.pywith 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 ConceptsWeb 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.
--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:
--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
propertiesargument ofinsert_web_component. The property name in JS must match one of thepropertiesdictionary key. You also should make sure the Python and JS types are compatible to avoid issues. - Event properties - these were defined in the
eventsargument ofinsert_web_component. The property name in JS must match one of theeventsdictionary key. Event properties are always typeStringbecause 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:
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.
--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:
import mesop as me
import mesop.labs as melAll 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:
- 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.).
- Rich user interactions - You should be able to build reasonably sophisticated web applications and demos (e.g. LLM chat) without building custom native components.
- 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
staticas 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.
cd /srv
MESOP_STATIC_FOLDER=static mesop app/main.pyHere are some examples of valid paths. Let's assume the current working directory is/srv/
staticbecomes/srv/staticstatic/becomes/srv/staticstatic/assetsbecomes/srv/static/assets./staticbecomes/srv/static./static/becomes/srv/static./static/assetsbecomes/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 usingMESOP_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.
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 andfile 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:
gcloud auth application-default loginIf you have multiple GCP projects, you may need to update the project associated
with the ADC:
GCP_PROJECT=gcp-project
gcloud config set project $GCP_PROJECT
gcloud auth application-default set-quota-project $GCP_PROJECTMesop 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.
COLLECTION_NAME=collection_name
gcloud firestore fields ttls update expiresAt \
--collection-group=$COLLECTION_NAMEBy 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"
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"
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.
MESOP_COOKIE_SECRET_KEY
!!! 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 requirespip install cryptography.
Generate a strong key for production:
python -c "import secrets; print(secrets.token_hex(32))"Set it before starting Mesop:
MESOP_COOKIE_SECRET_KEY=your-random-secret mesop main.pyOr in a .env file:
MESOP_COOKIE_SECRET_KEY=your-random-secretSecurity 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.
MESOP_STATE_SESSION_BACKEND=memory mesop main.pyUse 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.
MESOP_STATE_SESSION_BACKEND=file
MESOP_STATE_SESSION_BACKEND_FILE_BASE_DIR=/tmp/mesop-sessionsWhen you run your Mesop app, the .env file will then be read.
mesop main.pyApi 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:
import mesop as me
@me.page()
def foo():
me.text("bar")NOTE: If you do not provide a
pathargument, 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 "/".
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.
--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.
--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.
@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
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:
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).
all_values = me.query_params.get_all('param_name')Iterate
for key in query_params:
value = query_params[key]Set query param
query_params['new_param'] = 'value'Set repeated query param
query_params['repeated_param'] = ['value1', 'value2']Delete
del query_params['param_to_delete']Patterns
Navigate with existing query params
Here's an example of how to navigate to a new page with query parameters:
def click_navigate_button(e: me.ClickEvent):
me.query_params['q'] = "value"
me.navigate('/search', query_params=me.query_params)Navigate with only new 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.
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:
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.
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:
/* Detailed source-code truncated for AI context efficiency. */Run the Mesop app and look at the changes:
mesop main.pyLet's review the changes:
- We've added a
ROOT_BOX_STYLEto set the overall layout and background color. - We're importing a custom font (Inter) using the
stylesheetsparameter in the@me.pagedecorator. - We've created separate functions for the
headerandchat_inputcomponents. - The main layout uses nested
boxcomponents 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:
@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:
- A
Stateclass to manage the application state, including the user's input. - An
on_blurfunction to update the state when the user switches focus from the textarea. - A
send_promptfunction 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 & dialogsCodelab 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:
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:
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:
/* Detailed source-code truncated for AI context efficiency. */This updated code adds the following features:
- A model picker dialog that allows users to select AI models and enter API keys.
- State management for selected models and API keys.
- A model switcher in the chat input area that opens the model picker dialog.
- 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 APIsCodelab 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:
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.textNow, create a new file called 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 textUpdating 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:
/* Detailed source-code truncated for AI context efficiency. */This updated code adds the following features:
- A
display_conversationsfunction that shows the chat history for each selected model. - A
display_messagefunction that renders individual messages with appropriate styling. - An updated
send_promptfunction 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:
- Select multiple AI models using the model picker dialog.
- Enter API keys for Gemini and Claude.
- Send prompts to the selected models.
- 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 upCodelab 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:
/* 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:
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.keyAnd 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:
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:
- Navigate to the conversations page at the start of a conversation.
- Implement auto-scrolling to keep the latest messages in view.
Update the send_prompt function in 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
yieldRunning 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:
- A home page with example prompts and a model picker.
- A conversation page with a multi-column layout for different model responses.
- Real-time streaming of model responses with loading indicators.
- 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:
- Deploy your app.
- Add the ability to save and load conversations.
- 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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--8<-- "demo/input.py"API
::: mesop.components.input.input.input
::: mesop.components.input.input.InputBlurEvent
::: mesop.components.input.input.InputEnterEvent
::: mesop.events.InputEvent
Components Link (`docs/components/link.md`)
Overview
Link creates an HTML anchor element (i.e. <a>) which links to another page.
Examples
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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
--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:
--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.pageis a function decorator which makes a function a root component for a particular path. If you omit thepathparameter, this is the equivalent of@me.page(path="/").appis 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
appin the previous example.
Counter app
Let's build a more complex app to demonstrate Mesop's interactivity features.
--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:
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:
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 CodelabGetting 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:
pip install mesopIf you're not familiar with setting up a Python environment, follow one of the options below.
A. Colab (Recommended for beginners)
Colab is a free hosted Jupyter notebook product provided by Google.
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:
python --versionIf you don't, please download Python.
Create a venv environment
Open the terminal and navigate to a directory:
cd fooCreate a virtual environment by using venv, which will avoid Python environment issues. Run:
python -m venv .venvActivate 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.
- Install mesop:
pip install mesopUpgrading
To upgrade Mesop, run the following command:
pip install --upgrade mesopIf 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:
QuickstartGuides 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:
- You enable IAP on your App Engine or Cloud Run service in the Google Cloud Console.
- Google handles the sign-in flow and only forwards authenticated requests to your app.
- Each forwarded request includes a signed
X-Goog-IAP-JWT-Assertionheader containing the user's identity. - 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-authPython 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:
# 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
--8<-- "mesop/examples/web_component/firebase_auth/firebase_auth_component.js"What you need to do:
- Replace
firebaseConfigwith your Firebase project's config. Read the Firebase docs to learn how to get yours. - Replace the URLs
signInSuccessUrlwith your Mesop page path andtosUrlandprivacyPolicyUrlto 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
--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:
--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.pymodule 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_changedfunction 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:
```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:
- Your event handler calls
me.set_cookie(). - Mesop encodes the pending cookie operations into a short-lived itsdangerous-signed token and sends an
ApplyCookiesCommandto the browser. Any server worker that holds the sameMESOP_COOKIE_SECRET_KEYcan verify and redeem the token. - 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 theSet-Cookieheaders.
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.
import mesop as me
@me.cookieclass
class SessionCookie:
username: str = ""
role: str = "guest"Reading a cookie
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.
def on_load(e: me.LoadEvent):
session = me.cookie(SessionCookie)
if session.username:
state = me.state(State)
state.logged_in = True
state.username = session.usernameWriting / updating a cookie
Pass a cookieclass instance to me.set_cookie(). The cookie name is derived from the class, and the fields are JSON-serialised automatically.
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.
Deleting a cookie
def on_logout(e: me.ClickEvent):
me.delete_cookie(SessionCookie) # pass the class, not a stringSigned 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 |
# 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.
`me.cookie()`
token = me.cookie("session_id") # low-level: raw string value (or "" if absent)
session = me.cookie(SessionCookie) # high-level: typed cookieclass instance`me.set_cookie()`
# 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)`me.delete_cookie()`
me.delete_cookie("session_id") # low-level: explicit string name
me.delete_cookie(SessionCookie) # high-level: class lookupUsername 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.
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
/* 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:
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
.envfile listed in.gitignorefor local development. - Use
@me.cookieclass(signed=True)(orencrypted=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 —Truein production,Falseon local HTTP. Passsecure=Trueto 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 theApplyCookiesCommandtoken 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. anhttponlysession 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:
/* Detailed source-code truncated for AI context efficiency. */Run with Gunicorn:
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
.envfile that is listed in.gitignorefor 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:
Open your Mesop app in Google Chrome.
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
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".
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.
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.
Install the Python Debugger VS Code extension.
Include the following in your
.vscode/launch.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
}
]
}- At the top of your Mesop app (e.g. main.py), including the following snippet to start the debug server:
import debugpy
debugpy.listen(5678)- 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:
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.
mesop
gunicornCloud 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.
web: gunicorn --bind :8080 main:meThe --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 ismain. - By convention, we do
import mesop as meso themerefers 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:
gcloud run deployFollow 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:
gcloud run services update $YOUR_SERVICE --session-affinityBy 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:
gcloud app create --project=[YOUR_PROJECT_ID]
gcloud components install app-engine-pythonapp.yaml
Create app.yaml to configure App Engine to run Mesop.
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: 10Deploy to App Engine
In your terminal, go to the application directory, which has the files listed above.
Run the following command:
gcloud app deployFollow 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
--8<-- "docs/assets/hf/example.Dockerfile"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:
docker-compose up -dAlternatively, if you do not want to use Docker Compose, you can run:
docker build -t mesop-app . && docker run -d -p 8080:8080 mesop-appYou 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:
MESOP_BASE_URL_PATH=/myapppath docker-compose up -dAll 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:
MESOP_TRUST_PROXY_HEADERS=true docker-compose up -dSecurity warning: Only set
MESOP_TRUST_PROXY_HEADERS=truewhen Mesop is genuinely behind a trusted reverse proxy. In a direct (non-proxied) deployment, enabling this setting allows callers to spoofX-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 clickCreate 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:
git clone [email protected]:spaces/<user-name>/mesop-hello-world
cd mesop-hello-worldNote: 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.
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:
mesop
gunicornCreate Dockerfile
This file is the same as the generic Docker setup:
--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.
---
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-referenceDeploy to Hugging Face Spaces
The commands to commit your changes and push to the Hugging Face Spaces git repository
are:
git add -A
git commit -m "Add hello world Mesop app"
git push origin mainView deployed app
Congratulations! You should now be able to view your app on Hugging Face Spaces.
The URL should be something like this:
https://huggingface.co/spaces/<user-name>/mesop-hello-worldGuides 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:
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 = 0Although 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.
def on_click(event: me.ClickEvent):
state = me.state(State)
state.count += 1Generator 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.
def on_click(event: me.ClickEvent):
state = me.state(State)
state.count += 1
yield
time.sleep(1)
state.count += 1
yieldYou 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.
async def on_click(event: me.ClickEvent):
state = me.state(State)
state.count += 1
yield
await asyncio.sleep(1)
state.count += 1
yieldFor 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.
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
passIf 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:
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
yieldBoilerplate-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:
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:
@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:
@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:
InteractivityGuides 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.
--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.
--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.
--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::
@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.valueThe problem is that the input value now has a race condition because it's being set by two sources:
- The server is setting the input value based on state.
- 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.
@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.valueOption 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.
@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.valueOption 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.
@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.valueNext steps
Learn about layouts to build a customized UI.
LayoutsGuides 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:
import mesop.labs as melThe 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
def row():
with me.box(style=me.Style(display="flex", flex_direction="row")):
me.text("Left")
me.text("Right")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
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
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
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")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")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.
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
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.
Navigation
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:
--8<-- "mesop/examples/docs/multi_page_nav.py"Note: you can re-use state across pages. See how the above example uses the
State#countvalue 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:
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.
Manually adjust the number of replicas to a higher number.
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:
@me.stateclass
class State:
val: strYou can get an instance of the state class inside any of your Mesop component functions by using me.state:
@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.
```py
@me.stateclass
class State:
a: str = "abc"
```???+ failure "Bad: mutable default value"
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"
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"
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 numbersfloat- Floating-point numbersstr- Stringsbool- Boolean values
@me.stateclass
class State:
count: int
temperature: float
name: str
is_enabled: boolCollections
list[T]- Lists (including nested lists)dict[K, V]- Dictionaries (including nested dicts)set[T]- Sets
@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 objectsdatetime.date- Date objects
from datetime import datetime, date
@me.stateclass
class State:
created_at: datetime
birthday: dateBinary Data
bytes- Binary data
@me.stateclass
class State:
file_content: bytesSpecial Types
pandas.DataFrame- Pandas DataFrames (requires pandas to be installed)pydantic.BaseModel- Pydantic models and subclassesmesop.components.uploader.UploadedFile- Uploaded files from the uploader component
import pandas as pd
from pydantic import BaseModel
class UserModel(BaseModel):
name: str
age: int
@me.stateclass
class State:
data_frame: pd.DataFrame
user: UserModelNested State Classes
You can nest dataclasses within your state class, and they will be automatically serialized:
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:
# ❌ 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:
@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:
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:
- https://github.com/mesop-dev/mesop/issues/565
- https://github.com/mesop-dev/mesop/issues/659
- https://github.com/mesop-dev/mesop/issues/814
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.
@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:
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:
@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:
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"
```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"
```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"
```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"
```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"
```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"
```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"
```python
@me.stateclass
class State:
raw_response: str # ❌ Could be megabytes of JSON
```???+ success "Good: store only what the UI needs"
```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"
```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"
```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 handlersGuides 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.
cd /some/path/mesop-app
MESOP_STATIC_FOLDER=assets mesop main.pyHere is another example:
Mesop will look for the assets directory relative to your current working directory.
In this case, /some/path/assets.
cd /some/path
MESOP_STATIC_FOLDER=assets mesop mesop-app/main.pyEnabling 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.
MESOP_STATIC_FOLDER=assets MESOP_STATIC_URL_PATH=/assets mesop main.pyUsing 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.
MESOP_STATIC_FOLDER=assets
MESOP_STATIC_URL_PATH=/assetsThen you can run the Mesop command like this:
mesop main.pyExample use cases
Here are a couple examples that use the static assets feature.
Add a logo
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:
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.
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.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["main.py"],
theme: {
extend: {},
},
plugins: [],
safelist: [],
};This is just the base Tailwind input file.
@tailwind base;
@tailwind components;
@tailwind utilities;The command to generate the output Tailwind CSS is:
# 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.cssGuides 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:
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:
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"
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:
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:
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:
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:
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.
me.set_theme_density(0) # 0 is the least denseYou 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:
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
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:
- A Python server, running on top of Flask.
- A Web client, built on Angular framework, which wraps various Angular components, particularly Angular Material components.
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 aComponentproto. 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:
- The user visits a path on the Mesop application, e.g. "/" (root path), in their browser.
- The Mesop client-side web application (Angular) is bootstrapped and sends an
InitRequestto the server. - The Mesop server responds with a
RenderEventwhich contains a fully instantiated component tree. - 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:
- The user triggers a
UserEventwhich is sent to the server. The UserEvent includes: the application state (represented by theStatesproto), 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). - The server does the following:
- 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.
- 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.
- 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.
- In the streaming case, we may run the render loop and flush it down via Server-Sent Events many times.
- 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 Bazelbzlfiles andtoolswhich 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 ingenerator/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.
./scripts/cli.shThis 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.
- Setup your dev environment
- If you're creating a new component, you can follow the new component guide
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
- Install pre-commit
- Install pre-commit hooks for this repo:
pre-commit install
Run local development
We recommend using this for most Mesop framework development.
./scripts/cli.shNOTE: this automatically serves the angular app.
Python
Third-party packages (PIP)
If you update //build_defs/requirements.txt, run:
bazel run //build_defs:pip_requirements.updatevenv
To support IDE type-checking (Pylance) in VS Code, we use Aspect's rules_py which generates a venv target.
bazel run //mesop/cli:cli.venvThen, you can activate the venv:
source .cli.venv/bin/activateYou will need to setup a symlink to have Python IDE support for protos:
./scripts/setup_proto_py_modules.shCheck that you're using venv's python:
which pythonCopy the python interpreter path and paste it into VS Code.
Finally, install third-party dependencies.
pip install -r build_defs/requirements_lock.txtNOTE: You may need to run the command with
sudoif you get a permission denied error, particularly with "_distutils_hack".
Commit hooks
We use pre-commit to automatically format, lint code before committing.
Setup:
- Install pre-commit.
- Setup git hook:
pre-commit install
Docs
We use Mkdocs Material to generate our docs site.
- Activate venv
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
python scripts/scaffold_component.py $component_nameAPI 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.
- Define the event class in
//mesop/events/{event_name}.py - In the same file, define an event mapper and register it:
runtime().register_event(EventClass, event_mapper)
Potential exploration areas
- Code-gen
component_rendererusing 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:
The
mainbranch should be healthy (e.g. latest commit is green).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.
- 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
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.
- Click "Choose a tag" and type in the version you just released. This will create a new Git tag.
- Click "Generate release notes".
- If this is a an RC: Click "Set as a pre-release", otherwise leave the "Set as the latest release" checked.
- If this is a regular (non-RC) release, click "Create a discussion for this release".
- 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:
cd scripts/smoketest_app/ && uvx --refresh mesop==1.0.0 main.pyThis 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:
cd scripts/smoketest_app/ && uvx --refresh --with mesop==1.0.0 gunicorn@latest main:meThis 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:
!pip install mesop==0.X.Yrc1Tip: 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
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
yarn playwright testDebug tests
yarn playwright test --debugInternal 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 * aswhen importing protos from TS. This prevents tree-shaking downstream. - Do not use any external Bazel references (e.g.
@) withinmesop/. Instead, reference them indirectly using a wrapper inbuild_defs/. - Avoid relying on implicit transitive dependencies, particularly for TS/NG modules.
- Do not use raw
JSON.parse, instead usejsonParseinstrict_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:
./scripts/run_py_typecheck.shThis 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.
./scripts/cli.shYou 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:
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:
@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:
@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"
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:
@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:
%%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
--8<-- "mesop/examples/focus_component.py"API
::: mesop.commands.focus_component.focus_component
Api Commands Navigate (`docs/api/commands/navigate.md`)
Navigate
To navigate to another page, you can use me.navigate. This is particularly useful for navigating across a multi-page app.
Example
--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
--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
--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.
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:
- Build example applications using the visual editor with a video walkthrough.
- 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.
- 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:
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 ---
- Files Extracted: 108
- Estimated Token Budget: ~64042 tokens
- Canonical Reference: https://codewiki.google/github.com/mesop-dev/mesop