← Back to DevBytes

Flet from Scratch: Hands-On Tutorial:

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:

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:

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:

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:

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:

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:

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:

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:

# 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.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles