Tutorial: Build and package a multi-platform desktop app in Python

flet

Flet

Posted on June 28, 2022

Tutorial: Build and package a multi-platform desktop app in Python

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:

Image description

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

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Run this app and you will see a new window with a greeting:

Image description

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)
Enter fullscreen mode Exit fullscreen mode

Run the app and you should see a page like this:

Image description

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)
Enter fullscreen mode Exit fullscreen mode

Run the app and you should see a page like this:

Image description

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:

Image description

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)
Enter fullscreen mode Exit fullscreen mode

Run the app and you should see a page like this:

Image description

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)
Enter fullscreen mode Exit fullscreen mode

Read more about creating user controls.

Try something
Try adding two CalculatorApp components to the page:

# create application instance
calc1 = CalculatorApp()
calc2 = CalculatorApp()

# add application's root control to the page
page.add(calc1, calc2)
Enter fullscreen mode Exit fullscreen mode

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",
)
Enter fullscreen mode Exit fullscreen mode

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"

Enter fullscreen mode Exit fullscreen mode

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:

Image description

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
Enter fullscreen mode Exit fullscreen mode

Navigate to the directory where your .py file is located and build your app with the following command:

pyinstaller your_program.py
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

on Windows:

dist\your_program\your_program.exe
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 either dist/your_program or dist/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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

On Windows assets;assets must be delimited with ;:

pyinstaller your_program.py --noconsole --noconfirm --onefile --add-data "assets;assets"
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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 in appveyor.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 in appveyor.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 under GITHUB_TOKEN in your appveyor.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! 🎉

Image description

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Deploy the app by running:

flyctl deploy
Enter fullscreen mode Exit fullscreen mode

That's it! Open your app in the browser by running:

flyctl apps open
Enter fullscreen mode Exit fullscreen mode

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 value 5000.
  • 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 and Container 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.

💖 💪 🙅 🚩
flet
Flet

Posted on June 28, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related