Skip to content

Using pytauri

Note

The dependency versions specified in the following tutorial are the versions at the time of writing. There may be newer versions available when you use it.

Create venv

Create a virtual environment using uv:

uv venv --python-preference only-system

Warning

--python-preference only-system is necessary. Using uv's managed Python may result in not finding dynamic libraries.

activate the virtual environment:

source .venv/bin/activate
.venv\Scripts\Activate.ps1

Init pyproject

Create the src-tauri/python/tauri_app folder to store Python code, and add the following file:

ref: https://packaging.python.org/en/latest/guides/writing-pyproject-toml/

src-tauri/pyproject.toml
[project]
name = "tauri-app"  # (1)!
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.9"
dependencies = ["pytauri == 0.4.*"]  # (2)!

[project.entry-points.pytauri]
ext_mod = "tauri_app.ext_mod"

[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[tool.setuptools.packages]
find = { where = ["python"] }  # (3)!
  1. your python package name.
  2. This is the version at the time of writing this tutorial. There may be a newer version of pytauri available when you use it.
  3. the folder where your python code is stored, i.e., src-tauri/python.

Tip

Note the highlighted project.entry-points. We will explain its specific meaning when building the Wheel. For now, let's continue with the tutorial.

Install your project

Use uv to install your Python package in editable mode:

uv pip install -e src-tauri

Add following code:

src-tauri/python/tauri_app/__init__.py
"""The tauri-app."""

from pytauri import (
    BuilderArgs,
    builder_factory,
    context_factory,
)


def main() -> None:
    """Run the tauri-app."""
    app = builder_factory().build(
        BuilderArgs(
            context=context_factory(),
        )
    )
    app.run()
src-tauri/python/tauri_app/__main__.py
"""The main entry point for the Tauri app."""

from multiprocessing import freeze_support

from tauri_app import main

# - If you don't use `multiprocessing`, you can remove this line.
# - If you do use `multiprocessing` but without this line,
#   you will get endless spawn loop of your application process.
#   See: <https://pyinstaller.org/en/v6.11.1/common-issues-and-pitfalls.html#multi-processing>.
freeze_support()

main()

Run pytauri from rust

Add following dependencies to Cargo.toml:

ref: https://doc.rust-lang.org/cargo/reference/cargo-targets.html#binaries

src-tauri/Cargo.toml
# ...

[[bin]]
# the same as the package name
name = "tauri-app"
path = "src/main.rs"
required-features = ["pytauri/standalone"]

[dependencies]
# ...
pyo3 = { version = "0.23" }
pytauri = { version = "0.4" }  # (1)!
  1. This is the version at the time of writing this tutorial. There may be a newer version of pytauri available when you use it.

Also, enable the pytauri/standalone feature:

src-tauri/tauri.conf.json
{
    "build": {
        "features": ["pytauri/standalone"]
    }
}

Warning

If you do not enable required-features in tauri-cli, cargo will silently skip building your main.rs executable file.

Change following rust code:

src-tauri/src/lib.rs
use pyo3::prelude::*;

// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

pub fn tauri_generate_context() -> tauri::Context {
    tauri::generate_context!()
}

#[pymodule(gil_used = false)]
#[pyo3(name = "ext_mod")]
pub mod ext_mod {
    use super::*;

    #[pymodule_init]
    fn init(module: &Bound<'_, PyModule>) -> PyResult<()> {
        pytauri::pymodule_export(
            module,
            // i.e., `context_factory` function of python binding
            |_args, _kwargs| Ok(tauri_generate_context()),
            // i.e., `builder_factory` function of python binding
            |_args, _kwargs| {
                let builder = tauri::Builder::default()
                    .plugin(tauri_plugin_opener::init())
                    .invoke_handler(tauri::generate_handler![greet]);
                Ok(builder)
            },
        )
    }
}
src-tauri/src/main.rs
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use std::{convert::Infallible, env::var, error::Error, path::PathBuf};

use pyo3::wrap_pymodule;
use pytauri::standalone::{
    dunce::simplified, PythonInterpreterBuilder, PythonInterpreterEnv, PythonScript,
};
use tauri::{Builder, Manager as _};

use tauri_app_lib::{ext_mod, tauri_generate_context};

fn main() -> Result<Infallible, Box<dyn Error>> {
    let py_env = if cfg!(dev) {
        // `cfg(dev)` is set by `tauri-build` in `build.rs`, which means running with `tauri dev`,
        // see: <https://github.com/tauri-apps/tauri/pull/8937>.

        let venv_dir = var("VIRTUAL_ENV").map_err(|err| {
            format!(
                "The app is running in tauri dev mode, \
                please activate the python virtual environment first \
                or set the `VIRTUAL_ENV` environment variable: {err}",
            )
        })?;
        PythonInterpreterEnv::Venv(PathBuf::from(venv_dir).into())
    } else {
        // embedded Python, i.e., bundle mode with `tauri build`.

        // Actually, we don't use this app, we just use it to get the resource directory
        let sample_app = Builder::default()
            .build(tauri_generate_context())
            .map_err(|err| format!("failed to build sample app: {err}"))?;
        let resource_dir = sample_app
            .path()
            .resource_dir()
            .map_err(|err| format!("failed to get resource dir: {err}"))?;

        // 👉 Remove the UNC prefix `\\?\`, Python ecosystems don't like it.
        let resource_dir = simplified(&resource_dir).to_owned();

        // 👉 When bundled as a standalone App, we will put python in the resource directory
        PythonInterpreterEnv::Standalone(resource_dir.into())
    };

    // 👉 Equivalent to `python -m tauri_app`,
    // i.e, run the `src-tauri/python/tauri_app/__main__.py`
    let py_script = PythonScript::Module("tauri_app".into());

    // 👉 `ext_mod` is your extension module, we export it from memory,
    // so you don't need to compile it into a binary file (.pyd/.so).
    let builder =
        PythonInterpreterBuilder::new(py_env, py_script, |py| wrap_pymodule!(ext_mod)(py));
    let interpreter = builder.build()?;

    let exit_code = interpreter.run();
    std::process::exit(exit_code);
}

Launch the app in dev mode

The tauri-cli has the ability to watch code changes and hot reload. Before starting, we need to add the following file to tell tauri-cli to ignore the python bytecode:

ref: https://tauri.app/develop/#reacting-to-source-code-changes

src-tauri/.taurignore
__pycache__

Also, we need tell vite to ignore .venv:

vite.config.ts
// https://vitejs.dev/config/
export default defineConfig(async () => ({
  server: {
    watch: {
      // 3. tell vite to ignore watching `src-tauri`
      ignored: ["**/src-tauri/**", "**/.venv/**"],
    },
  },
}));

Run pnpm tauri dev, and after recompiling, you will see a window similar to the previous step.

Try modifying the Python code, and you will notice that the Python code is quickly reloaded without needing to recompile the Rust code.

Next Steps

Next, we will demonstrate how to package your application.