Flet позволяет собрать интерфейс из Python-контролов и запускать его как desktop или web-приложение. Сделаем трекер расходов с названием, суммой, списком и итогом. Разберём обработчик on_click, обновление контролов, Decimal и ошибку ввода без перехода на HTML и JavaScript.
Консольный скрипт считает сумму, но не показывает жизненный цикл интерфейса. Flet соединяет знакомые функции Python с полями и кнопками. В этой статье вы соберёте окно трекера с добавлением и удалением расходов. Финальный результат можно проверить без догадок: валидная покупка появляется в списке, неверная сумма показывает ошибку, удаление пересчитывает total.
TextField, Button, ListView и Text являются Python-объектами.
on_click вызывает функцию с событием и меняет состояние.
После изменения controls страница получает новый вид через update.
Как Flet превращает Python в интерфейс
main получает Page и создаёт controls. Список expenses хранит данные отдельно от визуальных Row. render_list пересобирает ListView и total. Обработчики add_expense и delete_expense меняют список, затем вызывают render_list. Для Flet держите этапы раздельно: ввод, проверка, основное действие и вывод. Такой маршрут позволяет остановиться в конкретной точке, увидеть фактические данные и исправить одну причину вместо случайной перестановки строк.
| Этап | Действие | Проверяемый результат |
|---|---|---|
| Запуск | ft.run вызывает main | Page готова к controls |
| Ввод | Пользователь заполняет два TextField | Есть name и amount |
| Событие | Button вызывает add_expense | Данные проверены и добавлены |
| Рендер | render_list обновляет controls | Список и total совпадают |
Данные являются источником истины, а элементы списка каждый раз строятся из expenses. У Flet каждый этап оставляет наблюдаемый след, поэтому следующий шаг начинается только после проверки предыдущего.

От входных данных до видимого результата. Каждый event меняет data до UI
Пишем трекер расходов
Flet требует поддерживаемый Python. Создайте проект через uv, добавьте runtime и инструменты по актуальной установке Flet, затем запустите uv run flet run main.py. Для браузерного режима используйте uv run flet run --web main.py. Первый desktop-запуск может загрузить клиентские компоненты.
from decimal import Decimal, InvalidOperation
from uuid import uuid4
import flet as ft
def main(page: ft.Page):
page.title = "Expense tracker"
page.padding = 24
expenses: list[dict] = []
name = ft.TextField(label="Purchase", expand=True)
amount = ft.TextField(label="Amount", width=160)
message = ft.Text(color=ft.Colors.RED_600)
total = ft.Text("Total: 0.00", size=24, weight=ft.FontWeight.BOLD)
items = ft.ListView(spacing=8, height=320)
def render_list():
items.controls.clear()
for expense in expenses:
items.controls.append(
ft.ListTile(
title=ft.Text(expense["name"]),
subtitle=ft.Text(f'{expense["amount"]:.2f}'),
trailing=ft.IconButton(
icon=ft.Icons.DELETE_OUTLINE,
data=expense["id"],
on_click=delete_expense,
),
)
)
value = sum(item["amount"] for item in expenses)
total.value = f"Total: {value:.2f}"
page.update()
def delete_expense(event: ft.Event[ft.IconButton]):
expenses[:] = [item for item in expenses if item["id"] != event.control.data]
render_list()
def add_expense(_event):
message.value = ""
try:
value = Decimal(amount.value.replace(",", "."))
if value <= 0:
raise ValueError
except (InvalidOperation, ValueError, AttributeError):
message.value = "Enter a positive amount"
page.update()
return
clean_name = (name.value or "").strip()
if not clean_name:
message.value = "Enter a purchase name"
page.update()
return
expenses.append({"id": str(uuid4()), "name": clean_name, "amount": value})
name.value = ""
amount.value = ""
render_list()
page.add(
ft.Text("Expenses", size=34, weight=ft.FontWeight.BOLD),
ft.Row([name, amount, ft.FilledButton("Add", on_click=add_expense)]),
message,
items,
total,
)
ft.run(main)- Покупка и положительная Decimal добавляются в expenses.
- Ошибка остаётся рядом с формой и не падает в terminal.
- Delete использует стабильный uuid записи.
- Total каждый раз считается из актуального списка.
Decimal сохраняет понятное денежное значение, а строка с запятой нормализуется до преобразования. Пример Flet специально ограничен одним пользовательским сценарием, чтобы назначение каждой строки оставалось видимым.

Что отличает устойчивый проект от случайного запуска. Сериализуемые записи не зависят от Flet
Где хранить state и когда вызывать update
Flet создаёт дерево controls на стороне Python и синхронизирует изменения с Flutter-клиентом. Событие приходит в обработчик с event.control, поэтому data удобно связывает кнопку удаления с id. Изменение свойства value само по себе не гарантирует моментальный вид: page.update отправляет накопленные изменения. Для маленького локального приложения список в замыкании main достаточен, но после перезапуска он пуст. Постоянство требует файла, SQLite или client storage и отдельной загрузки.
| Часть | За что отвечает | Что проверить |
|---|---|---|
| Page | Хранит экран и controls | Title и padding заданы |
| TextField | Собирает строки пользователя | Пустые значения обработаны |
| ListView | Показывает расходы | Controls пересобраны из data |
| Event | Связывает click и handler | Delete получает id |
| Decimal | Хранит сумму | Положительность проверена |
Полное items.controls.clear удобно для десятков строк, но большой список лучше обновлять точечно. Обработчик не должен выполнять долгую сеть синхронно, иначе интерфейс замрёт. Асинхронные задачи запускают через поддерживаемый механизм Page. Типы control и event меняются между версиями библиотеки, поэтому сверяйтесь с актуальными docs. Для мобильной сборки потребуется platform toolchain, подпись и отдельные permissions, а flet run на desktop является лишь этапом разработки.
Проверяем суммы, удаление и пустой ввод
Добавьте три расхода, один удалите и сверните total вручную. Введите abc, -5, пустую строку и число с запятой. Добавьте category через Dropdown и покажите её в subtitle. Затем сохраните expenses в JSON при каждом изменении и загрузите при старте, преобразуя amount обратно в Decimal. Создайте кнопку Clear all с подтверждением. После этого запустите тот же код с --web и сравните layout на узком окне.
- Добавьте две валидные покупки.
- Проверьте сумму с копейками.
- Отклоните abc и отрицательное число.
- Удалите одну запись.
- Добавьте category.
- Перезапустите web-режим.
- Форма валидируется.
- Список строится из data.
- Удаление пересчитывает total.
- Desktop и web запускаются.
Интерфейс остаётся простым, пока список расходов можно проверить отдельно от Flet controls. После этой проверки измените одно правило Flet и заранее запишите ожидаемый результат. Так копирование превращается в понимание.

Четыре проверки перед следующим шагом. Сумма проверяется после каждого удаления
Как развивать Flet-приложение дальше
Следующим шагом вынесите Expense в dataclass, сохранение в отдельный repository и добавьте тесты чистых функций. Для постоянного хранения выберите SQLite или JSON по объёму и риску одновременной записи. Не записывайте файл на каждый символ TextField. На мобильном устройстве учитывайте путь приложения и permissions. Перед упаковкой проверьте актуальную команду flet build, иконки и подпись для каждой платформы.
Как изучить тему в Кодике
Повторите функции, списки словарей, Decimal и исключения Python, затем свяжите их с controls и events. В Кодике сначала восстановите чистую логику без фреймворка, затем верните библиотеку и интерфейс Flet.
| Шаг | Что разобрать | Мини-проверка |
|---|---|---|
| 1 | Списки и словари | Добавить Expense |
| 2 | Decimal | Проверить сумму |
| 3 | Функции | Вынести render_list |
| 4 | События | Удалить по data |
| 5 | Хранение | Сохранить JSON или SQLite |
Сначала тестируйте расчёт total как чистую функцию, потом подключайте визуальный слой. Финальная проверка для Flet: закрыть статью, объяснить путь данных и добавить одну функцию без копирования готового блока.
Пересчитывайте её из списка данных.
Для учебных денег берите Decimal и явный формат.
Храните сериализуемые записи отдельно от UI.
Не блокируйте интерфейс синхронной сетью или тяжёлым циклом.
Что получится в итоге

Форма принимает покупку, список обновляется, удаление работает по id, а итоговая Decimal-сумма остаётся согласованной.
Как запустить Flet-приложение в браузере?
Используйте flet run --web для того же Python-файла.
Когда Flet показывает изменённые controls?
После обновления свойств вызовите page.update или обновление соответствующего control.
Почему расходы хранятся отдельно от ListView?
Отдельные данные легко считать, тестировать и сохранять без визуальных объектов.
Как понять, что учебный проект Flet действительно работает?
Проверьте основной сценарий Flet, неверный ввод, повторный запуск и один граничный случай. Затем объясните путь данных своими словами.
Можно ли начать с готового кода Flet из статьи?
Да. Сначала добейтесь результата Flet, затем измените одно правило и восстановите ключевой файл без копирования.
Python, списки и функции закрепите в курсе Python. Сравните подход с калькулятором на Tkinter.
Проверку ввода дополните материалом про Pydantic, а генерацию безопасных значений разберите в генераторе паролей.