Python Snap Packaging — Deep Dive

Snap filesystem layout

When a snap is installed, its contents are mounted at /snap/<name>/current/. The internal layout follows a convention:

/snap/my-app/current/
├── bin/              # Entry point scripts
│   └── my-app
├── lib/              # Python and shared libraries
│   └── python3.10/
│       └── site-packages/
├── usr/              # System libraries bundled from the base
├── meta/             # Snap metadata
│   ├── snap.yaml     # Generated from snapcraft.yaml
│   └── gui/
│       └── my-app.desktop
└── snap/
    └── command-chain/ # Launch wrappers

The snap runs with several environment variables set:

SNAP=/snap/my-app/current           # Snap mount point
SNAP_DATA=/var/snap/my-app/current  # Writable data (persists across updates)
SNAP_USER_DATA=~/snap/my-app/current # Per-user writable data
SNAP_COMMON=/var/snap/my-app/common  # Data shared across versions

Your Python code should use these paths for file storage:

import os

def get_data_dir() -> str:
    """Get writable data directory, works both snapped and unsnapped."""
    snap_data = os.environ.get("SNAP_USER_DATA")
    if snap_data:
        return snap_data
    return os.path.expanduser("~/.my-app")

Multi-part builds

Complex Python applications often need system libraries compiled from source. Snapcraft’s multi-part system handles this:

parts:
  # Part 1: Build system dependencies
  libgeos:
    plugin: cmake
    source: https://github.com/libgeos/geos/archive/refs/tags/3.12.0.tar.gz
    cmake-parameters:
      - -DCMAKE_INSTALL_PREFIX=/usr
    build-packages:
      - g++
      - make

  # Part 2: Python application (depends on libgeos)
  my-gis-app:
    after: [libgeos]  # Build order dependency
    plugin: python
    source: .
    python-packages:
      - shapely       # Links against libgeos
      - geopandas
      - click
    build-packages:
      - libgeos-dev   # Needed at build time
    stage-packages:
      - libgeos-c1v5  # Needed at runtime

The after keyword ensures libgeos is built and staged before the Python part tries to compile Shapely.

Handling C extensions

Python packages with C extensions (NumPy, pandas, cryptography, etc.) need build tools and system libraries:

parts:
  my-data-app:
    plugin: python
    source: .
    python-packages:
      - numpy
      - pandas
      - scipy
      - scikit-learn
    build-packages:
      - gcc
      - g++
      - gfortran
      - libopenblas-dev
      - liblapack-dev
      - pkg-config
    stage-packages:
      - libopenblas0
      - liblapack3
      - libgfortran5

The distinction matters:

  • build-packages — installed during build, not included in the final snap.
  • stage-packages — included in the final snap, available at runtime.

For packages that provide pre-built wheels, you can skip build dependencies:

parts:
  my-app:
    plugin: python
    source: .
    python-packages:
      - numpy
    # Use manylinux wheels — no build tools needed
    python-constraints:
      - constraints.txt  # Pin to known-good wheel versions

Daemon services

Snaps can run Python applications as system services:

apps:
  my-service:
    command: bin/my-service
    daemon: simple
    restart-condition: always
    restart-delay: 5s
    plugs:
      - network
      - network-bind
    environment:
      PYTHONUNBUFFERED: "1"
      LOG_LEVEL: "INFO"

  my-worker:
    command: bin/celery-worker
    daemon: simple
    restart-condition: on-failure
    after:
      - my-service  # Start after the main service
    plugs:
      - network

Managing the daemon:

# The service starts automatically on install
sudo snap install my-app

# Service management
sudo snap start my-app.my-service
sudo snap stop my-app.my-service
sudo snap restart my-app.my-service
sudo snap logs my-app.my-service
sudo snap logs my-app.my-service -n 100  # Last 100 lines

Desktop application integration

For Python GUI applications (Tkinter, PyQt, GTK):

apps:
  my-gui-app:
    command: bin/my-gui-app
    extensions:
      - gnome          # GNOME desktop integration
    plugs:
      - desktop
      - desktop-legacy
      - wayland
      - x11
      - opengl

parts:
  my-gui-app:
    plugin: python
    source: .
    python-packages:
      - PyQt5
    stage-packages:
      - libqt5gui5
      - libqt5widgets5

Desktop file for app launcher integration:

# snap/gui/my-gui-app.desktop
[Desktop Entry]
Type=Application
Name=My GUI App
Icon=${SNAP}/meta/gui/icon.png
Exec=my-gui-app
Categories=Utility;

The extensions field is powerful — gnome automatically sets up GTK themes, fonts, cursor themes, and desktop portal integration.

CI/CD automated publishing

GitHub Actions

name: Build and Publish Snap
on:
  push:
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: snapcore/action-build@v1
        id: build
      
      - uses: snapcore/action-publish@v1
        env:
          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAP_TOKEN }}
        with:
          snap: ${{ steps.build.outputs.snap }}
          release: stable

  # Test before publishing
  test:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Install snap
        run: |
          sudo snap install --dangerous ${{ needs.build.outputs.snap }}
          my-python-app --self-test

Generate the store credentials:

snapcraft export-login --snaps my-python-app --channels stable,candidate credentials.txt
# Store the contents as a GitHub secret

Launchpad builds

Canonical’s Launchpad can build snaps automatically from GitHub:

  1. Connect your GitHub repo to Launchpad.
  2. Launchpad detects snapcraft.yaml changes.
  3. Builds the snap on Canonical’s infrastructure (supports ARM, RISC-V, etc.).
  4. Publishes directly to the Snap Store.

This is the easiest path for multi-architecture builds — Launchpad has native ARM builders, no cross-compilation needed.

Advanced snapcraft.yaml patterns

Python version override

parts:
  my-app:
    plugin: python
    source: .
    build-environment:
      - PARTS_PYTHON_INTERPRETER: python3.11
    build-packages:
      - python3.11-dev
      - python3.11-venv
    stage-packages:
      - python3.11-minimal
      - libpython3.11-stdlib

Startup optimization

apps:
  my-app:
    command: bin/my-app
    command-chain:
      - snap/command-chain/snapcraft-runner  # Standard wrapper
    environment:
      # Disable Python's site module for faster startup
      PYTHONNOUSERSITE: "1"
      # Pre-compiled bytecode
      PYTHONDONTWRITEBYTECODE: "1"

Build caching with snapcraft

# Enable build caching
snapcraft --use-lxd  # LXD provides better caching than Multipass

# Clean and rebuild specific parts
snapcraft clean my-app
snapcraft

Size optimization

Snaps can be large because they bundle system libraries. Strategies to reduce size:

parts:
  my-app:
    plugin: python
    source: .
    # Only include what you need from the base
    stage:
      - -usr/share/doc/*     # Remove documentation
      - -usr/share/man/*     # Remove man pages
      - -usr/share/locale/*  # Remove unused locales
    organize:
      # Restructure files if needed
      usr/lib/python3/dist-packages/*: lib/python3.10/site-packages/

Typical snap sizes for Python applications:

ContentApproximate size
Minimal CLI (click + requests)15-25 MB
Web app (Flask + SQLAlchemy)30-50 MB
Data science (numpy + pandas)80-120 MB
GUI app (PyQt5 + GNOME)100-200 MB

Debugging snaps

# Run the snap in devmode for debugging
sudo snap install my-app --devmode

# Check confinement violations
sudo journalctl -t audit | grep my-app

# Enter the snap's environment
snap run --shell my-app
# Now you're inside the snap's filesystem namespace

# Check snap connections (interfaces)
snap connections my-app

# Manually connect a plug
sudo snap connect my-app:removable-media

For development iteration:

# Install from local file without store
sudo snap install my-python-app_1.0.0_amd64.snap --dangerous --devmode

# Watch logs
sudo snap logs my-app -f

One thing to remember: Snaps give Python applications a complete Linux distribution pipeline — from sandboxed packaging with the Python plugin, through multi-architecture CI builds on Launchpad, to automatic updates from the Snap Store — making it the most integrated way to ship Python software to Linux desktops and servers.

pythonsnappackaginglinuxdistribution

See Also

  • Python Appimage Distribution An AppImage is like a portable app on a USB stick — download one file, double-click it, and your Python program runs on any Linux computer without installing anything.
  • Python Briefcase Native Apps Imagine a travel agent who repacks your suitcase for each country's customs — Briefcase converts your Python app into proper native packages for every platform.
  • Python Flatpak Packaging Flatpak wraps your Python app in a safe bubble that works on every Linux system — like a snow globe that keeps your program perfect inside.
  • Python Mypyc Compilation Your type hints are not just for documentation — mypyc turns them into speed boosts by compiling typed Python into fast C extensions.
  • Python Nuitka Compilation What if your Python code could run as fast as a race car instead of a bicycle? Nuitka translates Python into C to make that happen.