How to Package a Python App Using Nuitka


Learn how to package a Python app in this tutorial.

For the most part, once you’ve written your Python code, you simply deploy it to a server, install the environment, grab the dependencies and you’re done.

However, there are times when you may want to provide your app to someone else and don’t want the hassle of getting them setup with all the training around making sure they have Python on their machine and can run your app.

Perhaps it’s even because you don’t want the other party to have your precious source code. Python is an interpreted language, making this mostly unavoidable.

What if there were another way? … enter Nuitka!

What is Nuitka?

Nuitka can be understood as being a compiler for your python code. No, it technically isn’t a compiler. What it really does is convert your code to C and then compile that down to a binary for distribution.

Show me an example!

If you’re saying “This all sounds too good, don’t tell me.. Show Me!”, then get ready, because I plan to do just that!

Installing Nuitka to Package a Python App

As with most things Python, it’s quick to get straight to the point.

Head over to PyPi and search for Nuitka to make sure we have the latest version.

<a href="https://pypi.org/project/Nuitka/" target="_blank" rel="noreferrer noopener nofollow">https://pypi.org/project/Nuitka/</a>

N.B. Before performing the next step, make sure to setup a Python Virtual Environment so that all packages will be installed locally to this tutorial.

Continuing; This gives us an easy way to get going, pip install Nuitka.

mkdir -p ~/src/tutorials/nuitka_testing
cd $_
virtualenv -p python3 venv
. venv/bin/activate

Now run the pip install Nuitka:

$ pip install nuitka

Collecting nuitka
  Downloading Nuitka-0.6.7.tar.gz (2.3 MB)
     |████████████████████████████████| 2.3 MB 1.6 MB/s
Building wheels for collected packages: nuitka
  Building wheel for nuitka (setup.py) ... done
  Created wheel for nuitka: filename=Nuitka-0.6.7-py3-none-any.whl size=2117847 sha256=5ce6d2ef97e7fd72aa8980c8ba7d6cfdecaf6f7b8971fd397241070d8a0f6e2e
  Stored in directory: /Users/ao/Library/Caches/pip/wheels/60/7f/ef/8c1ef8cf2b509e25ead8f221725a8f95db6d7af0fc67565fde
Successfully built nuitka
Installing collected packages: nuitka
Successfully installed nuitka-0.6.7

If you got stuff for some reason, read more about downloading Nuitka from the project’s website directly.

Testing out Nuitka

Nuitka is a Python module that we run against a project or python script.

This means we need a nice little test script to try it out.

Create a file called test1.py and enter the following code in it:

import string
from random import *
characters = string.ascii_letters + string.punctuation  + string.digits
password =  "".join(choice(characters) for x in range(randint(12, 16)))
print(password)

This will generate a unique strong password for us, between 12 and 16 characters.

If we run the script using python, we get output similar to this:

$ python test1.py

KdcM[btk8JvW

Excellent!

So now let’s add Nuitka into the mix. Run the following:

python -m nuitka test1.py

This will take a moment and will not render any output to the screen.

If we execute a ls -lashp then we will see what has been created:

$ ls -lashp

total 496
  0 drwxr-xr-x   6 ao  staff   192B  ... ./
  0 drwxr-xr-x   4 ao  staff   128B  ... ../
488 -rwxr-xr-x   1 ao  staff   243K  ... test1.bin
  0 drwxr-xr-x  18 ao  staff   576B  ... test1.build/
  8 -rw-r--r--   1 ao  staff   195B  ... test1.py
  0 drwxr-xr-x   6 ao  staff   192B  ... venv/

We can now execute ./test1.bin directly and see the application run.

$ ./test1.bin

7'4^5`YNux5Z

Additional CLI arguments

While the default arguments work pretty well, if we want to add debug symbols, or package our application as a standalone app, there are a ton of additional arguments we can pass in.

Issue a python -m nuitka --help to see all the options.

$ python -m nuitka --help

Usage: __main__.py [--module] [--run] [options] main_module.py
Options:
  --version
  -h, --help
  --module
  --standalone
  --python-debug
  --python-flag=PYTHON_FLAGS
  --python-for-scons=PYTHON_SCONS
  --warn-implicit-exceptions
  --warn-unusual-code
  --assume-yes-for-downloads
  Control the inclusion of modules and packages:
    --include-package=PACKAGE
    --include-module=MODULE
    --include-plugin-directory=MODULE/PACKAGE
    --include-plugin-files=PATTERN
  Control the recursion into imported modules:
    --follow-stdlib, --recurse-stdlib
    --nofollow-imports, --recurse-none
    --follow-imports, --recurse-all
    --follow-import-to=MODULE/PACKAGE, --recurse-to=MODULE/PACKAGE
    --nofollow-import-to=MODULE/PACKAGE, --recurse-not-to=MODULE/PACKAGE
  Immediate execution after compilation:
    --run
    --debugger, --gdb
    --execute-with-pythonpath
  Dump options for internal tree:
    --xml
  Code generation choices:
    --full-compat
    --file-reference-choice=FILE_REFERENCE_MODE
  Output choices:
    -o FILENAME
    --output-dir=DIRECTORY
    --remove-output
    --no-pyi-file
  Debug features:
    --debug
    --unstripped
    --profile
    --graph
    --trace-execution
    --recompile-c-only
    --generate-c-only
    --experimental=EXPERIMENTAL
  Backend C compiler choice:
    --clang
    --mingw64
    --msvc=MSVC
    -j N, --jobs=N
    --lto
  Tracing features:
    --show-scons
    --show-progress
    --show-memory
    --show-modules
    --verbose
  Windows specific controls:
    --windows-dependency-tool=DEPENDENCY_TOOL
    --windows-disable-console
    --windows-icon=ICON_PATH
  Plugin control:
    --plugin-enable=PLUGINS_ENABLED, --enable-plugin=PLUGINS_ENABLED
    --plugin-disable=PLUGINS_DISABLED, --disable-plugin=PLUGINS_DISABLED
    --plugin-no-detection
    --plugin-list
    --user-plugin=USER_PLUGINS

First let’s remove all the old stuff so that we can see what happens when a standalone build occurs.

$ rm -rf test1.bin test1.build
$ ls -lashp

total 8
0 drwxr-xr-x  4 ao  staff   128B  ... ./
0 drwxr-xr-x  4 ao  staff   128B  ... ../
8 -rw-r--r--  1 ao  staff   195B  ... test1.py
0 drwxr-xr-x  6 ao  staff   192B  ... venv/

How to Build a standalone Python App

python -m nuitka --standalone test1.py

This takes a moment or two, but when it’s done we see our distribution created.

$ ls -lashp

total 8
0 drwxr-xr-x   6 ao  staff   192B  ... ./
0 drwxr-xr-x   4 ao  staff   128B  ... ../
0 drwxr-xr-x  20 ao  staff   640B  ... test1.build/
0 drwxr-xr-x  65 ao  staff   2.0K  ... test1.dist/
8 -rw-r--r--   1 ao  staff   195B  ... test1.py
0 drwxr-xr-x   6 ao  staff   192B  ... venv/

Let’s examine the build in more depth:

$ tree -L 2

.
├── test1.build
│&nbsp;&nbsp; ├── @sources.tmp
│&nbsp;&nbsp; ├── __constants.bin
│&nbsp;&nbsp; ├── __constants.c
│&nbsp;&nbsp; ├── __constants.o
│&nbsp;&nbsp; ├── __constants_data.c
│&nbsp;&nbsp; ├── __constants_data.o
│&nbsp;&nbsp; ├── __frozen.c
│&nbsp;&nbsp; ├── __frozen.o
│&nbsp;&nbsp; ├── __helpers.c
│&nbsp;&nbsp; ├── __helpers.h
│&nbsp;&nbsp; ├── __helpers.o
│&nbsp;&nbsp; ├── build_definitions.h
│&nbsp;&nbsp; ├── module.__main__.c
│&nbsp;&nbsp; ├── module.__main__.o
│&nbsp;&nbsp; ├── scons-report.txt
│&nbsp;&nbsp; └── static_src
├── test1.dist
│&nbsp;&nbsp; ├── Python
│&nbsp;&nbsp; ├── _asyncio.so
│&nbsp;&nbsp; ├── _bisect.so
│&nbsp;&nbsp; ├── _blake2.so
│&nbsp;&nbsp; ├── _bz2.so
│&nbsp;&nbsp; ├── _codecs_cn.so
│&nbsp;&nbsp; ├── _codecs_hk.so
│&nbsp;&nbsp; ├── _codecs_iso2022.so
│&nbsp;&nbsp; ├── _codecs_jp.so
│&nbsp;&nbsp; ├── _codecs_kr.so
│&nbsp;&nbsp; ├── _codecs_tw.so
│&nbsp;&nbsp; ├── _contextvars.so
│&nbsp;&nbsp; ├── _crypt.so
│&nbsp;&nbsp; ├── _csv.so
│&nbsp;&nbsp; ├── _ctypes.so
│&nbsp;&nbsp; ├── _curses.so
│&nbsp;&nbsp; ├── _curses_panel.so
│&nbsp;&nbsp; ├── _datetime.so
│&nbsp;&nbsp; ├── _dbm.so
│&nbsp;&nbsp; ├── _decimal.so
│&nbsp;&nbsp; ├── _elementtree.so
│&nbsp;&nbsp; ├── _gdbm.so
│&nbsp;&nbsp; ├── _hashlib.so
│&nbsp;&nbsp; ├── _heapq.so
│&nbsp;&nbsp; ├── _json.so
│&nbsp;&nbsp; ├── _lsprof.so
│&nbsp;&nbsp; ├── _lzma.so
│&nbsp;&nbsp; ├── _multibytecodec.so
│&nbsp;&nbsp; ├── _multiprocessing.so
│&nbsp;&nbsp; ├── _opcode.so
│&nbsp;&nbsp; ├── _pickle.so
│&nbsp;&nbsp; ├── _posixsubprocess.so
│&nbsp;&nbsp; ├── _queue.so
│&nbsp;&nbsp; ├── _random.so
│&nbsp;&nbsp; ├── _scproxy.so
│&nbsp;&nbsp; ├── _sha3.so
│&nbsp;&nbsp; ├── _socket.so
│&nbsp;&nbsp; ├── _sqlite3.so
│&nbsp;&nbsp; ├── _ssl.so
│&nbsp;&nbsp; ├── _struct.so
│&nbsp;&nbsp; ├── _tkinter.so
│&nbsp;&nbsp; ├── _uuid.so
│&nbsp;&nbsp; ├── array.so
│&nbsp;&nbsp; ├── audioop.so
│&nbsp;&nbsp; ├── binascii.so
│&nbsp;&nbsp; ├── fcntl.so
│&nbsp;&nbsp; ├── grp.so
│&nbsp;&nbsp; ├── libcrypto.1.1.dylib
│&nbsp;&nbsp; ├── libgdbm.6.dylib
│&nbsp;&nbsp; ├── liblzma.5.dylib
│&nbsp;&nbsp; ├── libreadline.8.dylib
│&nbsp;&nbsp; ├── libsqlite3.0.dylib
│&nbsp;&nbsp; ├── libssl.1.1.dylib
│&nbsp;&nbsp; ├── math.so
│&nbsp;&nbsp; ├── mmap.so
│&nbsp;&nbsp; ├── pyexpat.so
│&nbsp;&nbsp; ├── readline.so
│&nbsp;&nbsp; ├── select.so
│&nbsp;&nbsp; ├── site
│&nbsp;&nbsp; ├── termios.so
│&nbsp;&nbsp; ├── test1
│&nbsp;&nbsp; ├── unicodedata.so
│&nbsp;&nbsp; └── zlib.so
├── test1.py
└── venv
    ├── bin
    ├── include
    └── lib
8 directories, 78 files

From the above output, we see the build directory contains C language code, while the dist directory contains a self executable test1 application.

Closing remarks

I really like the idea of Nuitka and the potential it brings to the table.

Being able to compile Python code would be a fantastic edge to the Python community. Albeit if only ever used to package a Python app and to distribute it.

Tell me what you think.