What is Flet?
Flet is an open-source Python framework that enables developers to build interactive, cross-platform desktop, web, and mobile applications from a single codebase. It wraps Flutter's rendering engine and widget system behind a familiar Python API, so you get the performance and visual polish of Flutter without having to learn Dart. Instead of wrestling with HTML, CSS, and JavaScript for a web frontend, or learning Swift/Kotlin for mobile, you write pure Python and get a reactive, state-driven UI that compiles down to native code via the Flutter engine.
Under the hood, Flet communicates with a Flutter app running in a headless or embedded mode. Every control you place — buttons, text fields, images, lists, dialogs — is mapped to a corresponding Flutter widget. The framework handles the serialization, event dispatching, and layout management transparently. This architecture means your Python code remains clean and focused on business logic, while the Flutter engine handles rendering, animations, and platform-specific behavior.
Why Flet Matters
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Traditional Python GUI toolkits like Tkinter, PyQt, or wxPython require significant boilerplate, have dated visual aesthetics, and often struggle with cross-platform deployment. Web frameworks like Flask or Django are great for backend logic but demand separate frontend skills. Flet bridges this gap by offering:
- Single language stack: Write everything in Python — no JavaScript, HTML, or CSS required.
- Modern, polished UI: Inherits Flutter's Material Design and Cupertino widgets, giving apps a professional look out of the box.
- True cross-platform output: Deploy as a desktop app (Windows, macOS, Linux), a web app (single-page application), or a mobile app (iOS, Android) — all from the same code.
- Reactive programming model: UI updates automatically when state changes, reducing the need for manual refresh logic.
- Rapid prototyping: Flet's hot-reload-like development experience (live updates on save) speeds up iteration dramatically.
- Easy deployment: Package as a standalone executable, deploy to a web server, or publish to app stores with minimal configuration.
Setting Up Your Environment
Before diving into code, you need a working Python environment (3.8 or later recommended). Install Flet via pip:
pip install flet
To verify the installation, create a minimal script that opens a window:
# hello_test.py
import flet as ft
def main(page: ft.Page):
page.title = "Hello Flet"
page.add(ft.Text("Installation successful!"))
ft.app(target=main)
Run it with:
python hello_test.py
You should see a desktop window appear with the title "Hello Flet" and the text displayed. If you prefer a browser-based development experience, change the last line to:
ft.app(target=main, view=ft.WEB_BROWSER)
This opens the app directly in your default browser at localhost:8550, with automatic reloading when you save changes to the Python file.
Building Your First Flet App
Let's build a counter app — the "Hello World" of reactive UI frameworks. This example demonstrates the core pattern: define controls, handle events, and update state.
import flet as ft
def main(page: ft.Page):
page.title = "Counter App"
page.vertical_alignment = ft.MainAxisAlignment.CENTER
counter_text = ft.Text("0", size=50, weight="bold")
def increment_click(e):
current = int(counter_text.value)
counter_text.value = str(current + 1)
page.update()
def decrement_click(e):
current = int(counter_text.value)
counter_text.value = str(current - 1)
page.update()
page.add(
ft.Row(
controls=[
ft.IconButton(ft.icons.REMOVE, on_click=decrement_click),
counter_text,
ft.IconButton(ft.icons.ADD, on_click=increment_click),
],
alignment=ft.MainAxisAlignment.CENTER,
)
)
ft.app(target=main)
Key observations from this example:
- The
mainfunction receives aPageobject — the root container for your UI. - Controls like
TextandIconButtonare created as Python objects with configurable properties. - Event handlers receive an
Eventparameter (heree) that contains context about the triggering action. page.update()tells the framework to re-render the UI with the latest state. Without this call, changes to control properties won't appear.
Understanding Controls and Layouts
Flet organizes UI elements into a hierarchy of controls. Every visual element — text, buttons, images, containers, rows, columns — is a control. Controls fall into three broad categories:
Display Controls
These present information to the user. Common ones include:
Text— displays a string with configurable size, color, weight, and alignment.Image— renders an image from a URL, local file, or base64 data.Icon— shows a Material Design icon.ProgressBarandProgressRing— indicate loading or completion states.
page.add(
ft.Text("Welcome!", size=24, color=ft.colors.BLUE, italic=True),
ft.Image(src="https://picsum.photos/200", width=200, height=200),
ft.ProgressBar(value=0.65, width=300),
)
Input Controls
These capture user interaction:
TextField— single or multi-line text input with validation and formatting.Dropdown— a select menu with options.CheckboxandSwitch— boolean toggles.Slider— numeric range selection.ElevatedButton,FilledButton,OutlinedButton— various button styles.
name_field = ft.TextField(label="Your name", hint_text="Enter name here")
age_slider = ft.Slider(min=0, max=120, divisions=120, label="Age: {value}")
subscribe_check = ft.Checkbox(label="Subscribe to newsletter")
submit_btn = ft.ElevatedButton("Submit", on_click=handle_submit)
page.add(name_field, age_slider, subscribe_check, submit_btn)
Layout Controls
These arrange other controls spatially:
Row— horizontal arrangement with configurable alignment and spacing.Column— vertical arrangement, scrollable by default if content overflows.Container— a box with background, border, padding, margin, and optional child control.Stack— overlapping positioning, similar to absolute positioning in CSS.GridViewandListView— scrollable grids and lists for large datasets.
page.add(
ft.Container(
content=ft.Column([
ft.Text("Card Title", size=18, weight="bold"),
ft.Text("This is the card body content."),
ft.ElevatedButton("Action"),
]),
padding=20,
border_radius=10,
bgcolor=ft.colors.GREY_200,
width=300,
)
)
Handling Events and State
Flet uses a reactive, state-driven model. Instead of manually manipulating the DOM or widget tree, you modify control properties and call page.update(). The framework diffs the changes and applies only what's necessary to the rendered UI.
Event Handlers
Every interactive control accepts an on_click, on_change, on_submit, or similar callback. These receive an event object with useful properties:
e.control— a reference to the control that fired the event.e.data— string data associated with the event (e.g., the selected dropdown value).e.name— the event name, useful for distinguishing multiple event types on a single control.
def dropdown_changed(e):
selected_value = e.control.value
result_text.value = f"You selected: {selected_value}"
page.update()
dropdown = ft.Dropdown(
options=[
ft.dropdown.Option("Python"),
ft.dropdown.Option("JavaScript"),
ft.dropdown.Option("Rust"),
],
on_change=dropdown_changed,
)
result_text = ft.Text()
page.add(dropdown, result_text)
Managing Application State
For simple apps, state can live in local variables within the main function. As complexity grows, consider using a dedicated state class:
class AppState:
def __init__(self):
self.tasks = []
self.filter = "all"
def add_task(self, name: str):
self.tasks.append({"name": name, "done": False})
def toggle_task(self, index: int):
self.tasks[index]["done"] = not self.tasks[index]["done"]
state = AppState()
def main(page: ft.Page):
# Use state throughout your event handlers
...
For persistent state across sessions, Flet provides page.client_storage — a key-value store that survives app restarts (uses localStorage in web mode, file storage in desktop mode):
# Save
page.client_storage.set("username", "Alice")
# Retrieve
username = page.client_storage.get("username")
Building a Real-World App: Task Manager
Let's combine everything into a functional task manager. This app demonstrates layout composition, event handling, state management, and dynamic control updates.
import flet as ft
class TaskManager:
def __init__(self):
self.tasks = []
def add(self, name):
self.tasks.append({"name": name, "done": False})
def toggle(self, index):
self.tasks[index]["done"] = not self.tasks[index]["done"]
def delete(self, index):
self.tasks.pop(index)
def main(page: ft.Page):
page.title = "Task Manager"
page.window_width = 500
page.window_height = 600
app = TaskManager()
# Pre-populate with sample tasks
app.add("Learn Flet basics")
app.add("Build a demo app")
app.add("Deploy to production")
task_list = ft.Column(scroll=ft.ScrollMode.AUTO)
new_task_field = ft.TextField(hint_text="What needs to be done?", expand=True)
def refresh_tasks():
task_list.controls.clear()
if not app.tasks:
task_list.controls.append(
ft.Text("No tasks yet! Add one above.", italic=True, color=ft.colors.GREY)
)
for i, task in enumerate(app.tasks):
task_list.controls.append(
ft.Container(
content=ft.Row([
ft.Checkbox(
value=task["done"],
on_change=lambda e, idx=i: toggle_task(idx),
),
ft.Text(
task["name"],
style=ft.TextStyle(
decoration=ft.TextDecoration.LINE_THROUGH if task["done"] else None
),
expand=True,
),
ft.IconButton(
ft.icons.DELETE_OUTLINE,
icon_color=ft.colors.RED_400,
on_click=lambda e, idx=i: delete_task(idx),
tooltip="Delete task",
),
]),
padding=10,
border_radius=8,
bgcolor=ft.colors.GREY_100 if i % 2 == 0 else ft.colors.WHITE,
)
)
page.update()
def add_task(e):
if new_task_field.value.strip():
app.add(new_task_field.value.strip())
new_task_field.value = ""
new_task_field.focus()
refresh_tasks()
def toggle_task(index):
app.toggle(index)
refresh_tasks()
def delete_task(index):
app.delete(index)
refresh_tasks()
# Build the UI
page.add(
ft.Column([
ft.Text("My Tasks", size=28, weight="bold"),
ft.Row([
new_task_field,
ft.FloatingActionButton(
icon=ft.icons.ADD,
on_click=add_task,
),
]),
ft.Divider(),
task_list,
], expand=True)
)
refresh_tasks()
ft.app(target=main)
This example showcases several important patterns:
- Lambda closures for index capture: In loops, event handlers need to capture the current index. Using
lambda e, idx=i: ...ensures each handler references the correct task. - Dynamic control rebuilding:
refresh_tasks()clears and rebuilds the task list from the current state — a clean, predictable approach. - Conditional styling: The task text gets a line-through decoration when marked done, demonstrating how control properties react to state changes.
- Expand property: Controls with
expand=Truefill available space, creating responsive layouts without fixed pixel calculations.
Navigation and Multiple Pages
For apps with multiple screens, Flet offers several navigation approaches. The simplest uses page.views — a stack of views that behaves like a browser history:
def main(page: ft.Page):
def go_to_settings(e):
page.views.append(
ft.View(
route="/settings",
controls=[
ft.AppBar(title=ft.Text("Settings")),
ft.Text("Settings content here"),
ft.ElevatedButton("Back", on_click=lambda e: page.views.pop()),
],
)
)
page.update()
page.views.append(
ft.View(
route="/",
controls=[
ft.AppBar(title=ft.Text("Home")),
ft.ElevatedButton("Go to Settings", on_click=go_to_settings),
],
)
)
page.update()
ft.app(target=main)
For larger applications, consider using page.go() with a router function that maps routes to view builders:
def main(page: ft.Page):
def route_change(e):
route = e.route
page.views.clear()
if route == "/":
page.views.append(
ft.View("/", [ft.AppBar(title=ft.Text("Home")), ft.Text("Welcome")])
)
elif route == "/about":
page.views.append(
ft.View("/about", [ft.AppBar(title=ft.Text("About")), ft.Text("About us")])
)
page.update()
page.on_route_change = route_change
page.go("/")
ft.app(target=main)
Best Practices
1. Keep UI and Logic Separate
Extract business logic into dedicated classes or functions. The main function should focus on wiring controls to handlers, not implementing algorithms. This makes testing and maintenance easier.
# Good: separated concerns
class Calculator:
@staticmethod
def compute(expression: str) -> float:
return eval(expression) # simplified for example
def main(page: ft.Page):
calc = Calculator()
# UI wiring only...
2. Batch Updates with page.update()
Call page.update() once after a group of property changes rather than after each individual modification. This reduces unnecessary rendering cycles:
# Less efficient
counter_text.value = "5"
page.update()
counter_text.color = ft.colors.RED
page.update()
# Better
counter_text.value = "5"
counter_text.color = ft.colors.RED
page.update()
3. Use Meaningful Control References
Store references to controls you'll need to modify later as variables or in data structures. Avoid repeatedly querying the control tree:
# Store reference
status_text = ft.Text("Ready")
page.add(status_text)
# Later, modify directly
status_text.value = "Processing..."
page.update()
4. Leverage Container for Card-Like UI
Container with border_radius, bgcolor, padding, and a shadow creates Material Design card effects without external dependencies:
ft.Container(
content=ft.Column([...]),
padding=20,
border_radius=12,
bgcolor=ft.colors.WHITE,
shadow=ft.BoxShadow(
blur_radius=10,
color=ft.colors.with_opacity(0.3, ft.colors.BLACK),
),
)
5. Handle Async Operations Gracefully
For network calls or long-running operations, use page.run_task() or async handlers to keep the UI responsive:
import asyncio
async def fetch_data():
await asyncio.sleep(2) # Simulate network request
return {"result": "success"}
def on_button_click(e):
page.run_task(fetch_data)
6. Test on Multiple Platforms Early
Run your app with different view modes during development to catch platform-specific issues:
# Desktop
ft.app(target=main)
# Web browser
ft.app(target=main, view=ft.WEB_BROWSER)
# In a browser tab, but opened from desktop
ft.app(target=main, view=ft.APP_BROWSER)
7. Mind the Control Lifecycle
Controls have will_unmount and did_mount hooks for cleanup and initialization. Use these to cancel subscriptions or release resources when a control is removed from the page:
class TimedControl(ft.Text):
def did_mount(self):
self.timer = asyncio.get_event_loop().call_later(5, self.time_up)
def will_unmount(self):
if self.timer:
self.timer.cancel()
def time_up(self):
self.value = "Time's up!"
self.page.update()
Deployment Options
Flet supports multiple deployment targets with minimal configuration changes:
- Desktop executable: Use
flet packto bundle your app into a standalone executable for Windows, macOS, or Linux. This wraps your Python code, dependencies, and the Flutter engine into a single distributable file. - Web deployment: Run
flet webto generate static assets you can host on any web server. The output is a single-page application with all Python logic compiled to JavaScript via Pyodide or running server-side. - Mobile: Package as an iOS/Android app using the Flet mobile build tools, which embed the Flutter engine alongside your Python runtime.
# Package for desktop
flet pack main.py --name "MyApp" --icon app_icon.png
# Build web assets
flet web main.py --output-dir dist
Conclusion
Flet represents a significant evolution in Python UI development. By marrying Flutter's world-class rendering engine with Python's readability and ecosystem, it eliminates the traditional friction between backend logic and frontend presentation. You've learned how to set up a Flet environment, build interactive interfaces with controls and layouts, manage state reactively, handle events, navigate between views, and apply best practices for maintainable code. The framework's cross-platform nature means the skills you develop today apply equally to desktop, web, and mobile projects. Whether you're prototyping an internal tool, building a customer-facing application, or creating a hobby project, Flet offers a productive, enjoyable development experience that keeps you in Python from start to finish. The best way to deepen your understanding is to build something real — take the task manager example, extend it with persistent storage, add search functionality, or integrate a REST API, and you'll quickly appreciate how Flet handles complexity while keeping your code clean and readable.