Tutorial: Build and package a multi-platform desktop app in Python
Flet
Posted on June 28, 2022
In this tutorial we will show you, step-by-step, how to create a Calculator app in Python using Flet framework and package it as a standalone executable for Windows, macOS and Linux, or deploy it as a web app. The app is a a simple console program, yet it is a multi-platform application with similar to iPhone calculator app UI:
You can find the live demo here.
In this tutorial, we will cover all of the basic concepts for creating a multi-platform desktop app: building a page layout, adding controls, handling events, and packaging/deployment options.
The tutorial consists of the following steps:
- Step 1: Getting started with Flet
- Step 2: Adding page controls
- Step 3: Building page layout
- Step 4: Handling events
- Step 5: Packaging as a desktop app
- Step 6: Deploying a web app
- Summary
Step 1: Getting started with Flet
To write a Flet web app you don't need to know HTML, CSS or JavaScript, but you do need a basic knowledge of Python and object-oriented programming.
Flet requires Python 3.7 or above. To create a web app in Python with Flet, you need to install flet
module first:
pip install flet
To start, let's create a simple hello-world app.
Create hello.py
with the following contents:
import flet
from flet import Page, Text
def main(page: Page):
page.add(Text(value="Hello, world!"))
flet.app(target=main)
Run this app and you will see a new window with a greeting:
Step 2: Adding page controls
Now you are ready to create a calculator app.
To start, you'll need a Text control for showing the result of calculation, and a few ElevatedButtons with all the numbers and actions on them.
Create calc.py
with the following contents:
import flet
from flet import ElevatedButton, Page, Text
def main(page: Page):
page.title = "Calc App"
result = Text(value="0")
page.add(
result,
ElevatedButton(text="AC"),
ElevatedButton(text="+/-"),
ElevatedButton(text="%"),
ElevatedButton(text="/"),
ElevatedButton(text="7"),
ElevatedButton(text="8"),
ElevatedButton(text="9"),
ElevatedButton(text="*"),
ElevatedButton(text="4"),
ElevatedButton(text="5"),
ElevatedButton(text="6"),
ElevatedButton(text="-"),
ElevatedButton(text="1"),
ElevatedButton(text="2"),
ElevatedButton(text="3"),
ElevatedButton(text="+"),
ElevatedButton(text="0"),
ElevatedButton(text="."),
ElevatedButton(text="="),
)
flet.app(target=main)
Run the app and you should see a page like this:
Step 3: Building page layout
Now let's arrange the text and buttons in 6 horizontal rows.
Replace calc.py
contents with the following:
import flet
from flet import ElevatedButton, Page, Row, Text
def main(page: Page):
page.title = "Calc App"
result = Text(value="0")
page.add(
Row(controls=[result]),
Row(
controls=[
ElevatedButton(text="AC"),
ElevatedButton(text="+/-"),
ElevatedButton(text="%"),
ElevatedButton(text="/"),
]
),
Row(
controls=[
ElevatedButton(text="7"),
ElevatedButton(text="8"),
ElevatedButton(text="9"),
ElevatedButton(text="*"),
]
),
Row(
controls=[
ElevatedButton(text="4"),
ElevatedButton(text="5"),
ElevatedButton(text="6"),
ElevatedButton(text="-"),
]
),
Row(
controls=[
ElevatedButton(text="1"),
ElevatedButton(text="2"),
ElevatedButton(text="3"),
ElevatedButton(text="+"),
]
),
Row(
controls=[
ElevatedButton(text="0"),
ElevatedButton(text="."),
ElevatedButton(text="="),
]
),
)
flet.app(target=main)
Run the app and you should see a page like this:
Using Container for decoration
To add a black background with rounded border around the calculator, we will be using Container control. Container may decorate only one control, so we will need to wrap all the 6 rows into a single vertical Column that will be used as the container's content
:
To complete the UI portion of the program, update color
and size
properties for the Text, and color
and bgcolor
properties for the buttons. For even alignment of the buttons within the rows, we will be using expand
property as shown on the diagram above.
Replace calc.py
contents with the following:
import flet
from flet import (
Column,
Container,
ElevatedButton,
Page,
Row,
Text,
border_radius,
colors,
)
def main(page: Page):
page.title = "Calc App"
result = Text(value="0", color=colors.WHITE, size=20)
page.add(
Container(
width=300,
bgcolor=colors.BLACK,
border_radius=border_radius.all(20),
padding=20,
content=Column(
controls=[
Row(controls=[result], alignment="end"),
Row(
controls=[
ElevatedButton(
text="AC",
bgcolor=colors.BLUE_GREY_100,
color=colors.BLACK,
expand=1,
),
ElevatedButton(
text="+/-",
bgcolor=colors.BLUE_GREY_100,
color=colors.BLACK,
expand=1,
),
ElevatedButton(
text="%",
bgcolor=colors.BLUE_GREY_100,
color=colors.BLACK,
expand=1,
),
ElevatedButton(
text="/",
bgcolor=colors.ORANGE,
color=colors.WHITE,
expand=1,
),
]
),
Row(
controls=[
ElevatedButton(
text="7",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="8",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="9",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="*",
bgcolor=colors.ORANGE,
color=colors.WHITE,
expand=1,
),
]
),
Row(
controls=[
ElevatedButton(
text="4",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="5",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="6",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="-",
bgcolor=colors.ORANGE,
color=colors.WHITE,
expand=1,
),
]
),
Row(
controls=[
ElevatedButton(
text="1",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="2",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="3",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="+",
bgcolor=colors.ORANGE,
color=colors.WHITE,
expand=1,
),
]
),
Row(
controls=[
ElevatedButton(
text="0",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=2,
),
ElevatedButton(
text=".",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="=",
bgcolor=colors.ORANGE,
color=colors.WHITE,
expand=1,
),
]
),
]
),
)
)
flet.app(target=main)
Run the app and you should see a page like this:
Just what we wanted!
Reusable UI components
While you can continue writing your app in the main
function, the best practice would be to create a reusable UI component.
Imagine you are working on an app header, a side menu, or UI that will be a part of a larger project (for example, at Flet we will be using this Calculator app in a bigger "Gallery" app that will show all the examples for Flet framework).
Even if you can't think of such uses right now, we still recommend creating all your web apps with composability and reusability in mind.
To make a reusable Calc app component, we are going to encapsulate its state and presentation logic in a separate CalculatorApp
class.
Replace calc.py
contents with the following:
import flet
from flet import (
Column,
Container,
ElevatedButton,
Page,
Row,
Text,
UserControl,
border_radius,
colors,
)
class CalculatorApp(UserControl):
def build(self):
result = Text(value="0", color=colors.WHITE, size=20)
# application's root control (i.e. "view") containing all other controls
return Container(
width=300,
bgcolor=colors.BLACK,
border_radius=border_radius.all(20),
padding=20,
content=Column(
controls=[
Row(controls=[result], alignment="end"),
Row(
controls=[
ElevatedButton(
text="AC",
bgcolor=colors.BLUE_GREY_100,
color=colors.BLACK,
expand=1,
),
ElevatedButton(
text="+/-",
bgcolor=colors.BLUE_GREY_100,
color=colors.BLACK,
expand=1,
),
ElevatedButton(
text="%",
bgcolor=colors.BLUE_GREY_100,
color=colors.BLACK,
expand=1,
),
ElevatedButton(
text="/",
bgcolor=colors.ORANGE,
color=colors.WHITE,
expand=1,
),
]
),
Row(
controls=[
ElevatedButton(
text="7",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="8",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="9",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="*",
bgcolor=colors.ORANGE,
color=colors.WHITE,
expand=1,
),
]
),
Row(
controls=[
ElevatedButton(
text="4",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="5",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="6",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="-",
bgcolor=colors.ORANGE,
color=colors.WHITE,
expand=1,
),
]
),
Row(
controls=[
ElevatedButton(
text="1",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="2",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="3",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="+",
bgcolor=colors.ORANGE,
color=colors.WHITE,
expand=1,
),
]
),
Row(
controls=[
ElevatedButton(
text="0",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=2,
),
ElevatedButton(
text=".",
bgcolor=colors.WHITE24,
color=colors.WHITE,
expand=1,
),
ElevatedButton(
text="=",
bgcolor=colors.ORANGE,
color=colors.WHITE,
expand=1,
),
]
),
]
),
)
def main(page: Page):
page.title = "Calc App"
# create application instance
calc = CalculatorApp()
# add application's root control to the page
page.add(calc)
flet.app(target=main)
Read more about creating user controls.
Try something
Try adding twoCalculatorApp
components to the page:
# create application instance
calc1 = CalculatorApp()
calc2 = CalculatorApp()
# add application's root control to the page
page.add(calc1, calc2)
Step 4: Handling events
Now let's make the calculator do its job. We will be using the same event handler for all the buttons and use data
property to differentiate between the actions depending on the button clicked. For each ElevatedButton control, specify on_click=self.button_clicked
event and set data
property equal to button's text, for example:
ElevatedButton(
text="AC",
bgcolor=colors.BLUE_GREY_100,
color=colors.BLACK,
expand=1,
on_click=self.button_clicked,
data="AC",
)
Below is on_click
event handler that will reset the Text value when "AC" button is clicked:
def button_clicked(self, e):
if e.data == "AC":
self.result.value = "0"
With similar approach, specify on_click
event and data
property for each button and add expected action to the button_clicked
event handler depending on e.data
value. Copy the entire code for this step from here.
Run the app and see it in the action:
Step 5: Packaging as a desktop app
Congratulations! You have created your Calculator app with Flet, and it looks awesome! Now it's time to share your app with the world!
Flet Python app and all its dependencies can be packaged into an executable and user can run it on their computer without installing a Python interpreter or any modules.
PyInstaller is used to package Flet Python app and all its dependencies into a single package for Windows, macOS and Linux. To create Windows package, PyInstaller must be run on Windows; to build Linux app, it must be run on Linux; and to build macOS app - on macOS.
Start from installing PyInstaller:
pip install pyinstaller
Navigate to the directory where your .py
file is located and build your app with the following command:
pyinstaller your_program.py
Your bundled Flet app should now be available in dist/your_program
folder. Try running the program to see if it works.
On macOS/Linux:
./dist/your_program/your_program
on Windows:
dist\your_program\your_program.exe
Now you can just zip the contents of dist/your_program
folder and distribute to your users! They don't need Python or Flet installed to run your packaged program - what a great alternative to Electron!
You'll notice though when you run a packaged program from macOS Finder or Windows Explorer a new console window is opened and then a window with app UI on top of it.
You can hide that console window by rebuilding the package with --noconsole
switch:
pyinstaller your_program.py --noconsole --noconfirm
Bundling to one file
Contents of dist/your_program
directory is an app executable plus supporting resources: Python runtime, modules, libraries.
You can package all these in a single executable by using --onefile
switch:
pyinstaller your_program.py --noconsole --noconfirm --onefile
You'll get a larger executable in dist
folder. That executable is a self-running archive with your program and runtime resources which gets unpacked into temp directory when run - that's why it takes longer to start "onefile" package.
Note:
For macOS you can distribute eitherdist/your_program
ordist/your_program.app
which is an application bundle.
Customizing package icon
Default bundle app icon is diskette which might be confusing for younger developers missed those ancient times when floppy disks were used to store computer data.
You can replace the icon with your own by adding --icon
argument:
pyinstaller your_program.py --noconsole --noconfirm --onefile --icon <your-image.png>
PyInstaller will convert provided PNG to a platform specific format (.ico
for Windows and .icns
for macOS), but you need to install Pillow module for that:
pip install pillow
Packaging assets
Your Flet app can include assets. Provided app assets are in assets
folder next to your_program.py
they can be added to an application package with --add-data
argument, on macOS/Linux:
pyinstaller your_program.py --noconsole --noconfirm --onefile --add-data "assets:assets"
On Windows assets;assets
must be delimited with ;
:
pyinstaller your_program.py --noconsole --noconfirm --onefile --add-data "assets;assets"
Note:
You can find here all PyInstaller command-line options
Using CI for multi-platform packaging
To create an app package with PyInstaller for specific OS it must be run on that OS.
If you don't have an access to Mac or PC you can bundle your app for all three platforms with AppVeyor - Continuous Integration service for Windows, Linux and macOS. In short, Continuous Integration (CI) is an automated process of building, testing and deploying (Continuous Delivery - CD) application on every push to a repository.
AppVeyor is free for open source projects hosted on GitHub, GitLab and Bitbucket. To use AppVeyor, push your app to a repository within one of those source-control providers.
To get started with AppVeyor sign up for a free account.
Click "New project" button, authorize AppVeyor to access your GitHub, GitLab or Bitbucket account, choose a repository with your program and create a new project.
Now, to configure packaging of your app for Windows, Linux and macOS, add file with the following contents into the root of your repository appveyor.yml
. appveyor.yml
is a build configuration file, or CI workflow, describing build, test, packaging and deploy commands that must be run on every commit.
Note:
You can just fork flet-dev/python-ci-example repository and customize it to your needs.
When you push any changes to GitHub repository, AppVeyor will automatically start a new build:
What that CI workflow does on every push to the repository:
- Clones the repository to a clean virtual machine.
- Installs app dependencies using
pip
. - Runs
pyinstaller
to package Python app into "onefile" bundle for Windows, macOS and Ubuntu. - Zip/Tar packages and uploads them to "Artifacts".
- Uploads packages to GitHub releases when a new tag is pushed. Just push a new tag to make a release!
GITHUB_TOKEN
GITHUB_TOKEN
inappveyor.yml
is a GitHub Personal Access Token (PAT) used by AppVeyor to publish created packages to repository "Releases". You need to generate your own token and replace it inappveyor.yml
. Login to your GitHub account and navigate to Personal access token page. Click "Generate new token" and select "public_repo" or "repo" scope for public or private repository respectively. Copy generated token to a clipboard and return to AppVeyor Portal. Navigate to Encrypt configuration data page and paste token to "Value to encrypt" field, click "Encrypt" button. Put encrypted value underGITHUB_TOKEN
in yourappveyor.yml
.
Configure AppVeyor for your Python project, push a new tag to a repository and "automagically" get desktop bundle for all three platforms in GitHub releases! 🎉
In addition to GitHub Releases, you can also configure releasing of artifacts to Amazon S3 bucket or Azure Blob storage.
Step 6: Deploying a web app
Flet app can be deployed as a "standalone" web app which means both your Python app and Flet web server are deployed together as a bundle.
Flet apps use WebSockets for real-time partial updates of their UI and sending events back to your program.
When choosing a hosting provider for your Flet app you should pay attention to their support of WebSockets. Sometimes WebSockets are not allowed or come as a part of more expensive offering, sometimes there is a proxy that periodically breaks WebSocket connection by a timeout (Flet implements re-connection logic, but it could be unpleasant behavior for users of your app anyway).
Another important factor while choosing a hosting provider for Flet app is latency. Every user action on UI sends a message to Flet app and the app sends updated UI back to user. Make sure your hosting provider has multiple data centers, so you can run your app closer to the majority of your users.
Note:
We are not affiliated with hosting providers in this section - we just use their service and love it.
Fly.io
Fly.io has robust WebSocket support and can deploy your app to a data center that is close to your users. They have very attractive pricing with a generous free tier which allows you to host up to 3 applications for free.
To get started with Fly install flyctl and then authenticate:
flyctl auth login
To deploy the app with flyctl
you have to add the following 3 files into the folder with your Python app.
Create requirements.txt
with a list of application dependencies. At minimum it should contain flet
module:
flet>=0.1.33
Create fly.toml
describing Fly application:
app = "<your-app-name>"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []
[env]
FLET_SERVER_PORT = "8080"
[experimental]
allowed_public_ports = []
auto_rollback = true
[[services]]
http_checks = []
internal_port = 8080
processes = ["app"]
protocol = "tcp"
script_checks = []
[services.concurrency]
hard_limit = 25
soft_limit = 20
type = "connections"
[[services.ports]]
force_https = true
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
restart_limit = 0
timeout = "2s"
Replace <your-app-name>
with desired application name which will be also used in application URL, such as https://<your-app-name>.fly.dev
.
Note we are setting the value of FLET_SERVER_PORT
environment variable to 8080
which is an internal TCP port Flet web app is going to run on.
Create Dockerfile
containing the commands to build your application container:
FROM python:3-alpine
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["python", "./main.py"]
main.py
is a file with your Python program.
Note:
Fly.io deploys every app as a Docker container, but a great thing about Fly is that it provides a free remote Docker builder, so you don't need Docker installed on your machine.
Next, switch command line to a folder with your app and run the following command to create and initialize a new Fly app:
flyctl apps create --name <your-app-name>
Deploy the app by running:
flyctl deploy
That's it! Open your app in the browser by running:
flyctl apps open
Replit
Replit is an online IDE and hosting platform for web apps written in any language. Their free tier allows running any number of apps with some performance limitations.
To run your app on Replit:
- Sign up on Replit.
- Click "New repl" button.
- Select "Python" language from a list and provide repl name, e.g.
my-app
. - Click "Packages" tab and search for
flet
package; select its latest version. - Click "Secrets" tab and add
FLET_SERVER_PORT
variable with value5000
. - Switch back to "Files" tab and copy-paste your app into
main.py
. - Run the app. Enjoy.
Summary
In this tutorial you have learned how to:
- Create a simple Flet app;
- Work with Reusable UI components;
- Design UI layout using
Column
,Row
andContainer
controls; - Handle events;
- Package your Flet app into an executable;
- Deploy your Flet app to the web;
For further reading you can explore controls and examples repository.
We would love to hear your feedback! Please drop us an email, join the discussion on Discord, follow on Twitter.
Posted on June 28, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.