Building Desktop Applications with PyTauri
Table of Contents
Overview
PyTauri is a framework for building desktop applications with Python. The original Rust-based Tauri framework enables cross-platform desktop app development using Rust for the backend and TypeScript with web frameworks for the frontend. PyTauri replaces the Rust backend with Python, allowing you to build apps with Python instead. I found this interesting, so I’m writing this post in real-time while reading the documentation with minimal prior knowledge. The writing may not be polished, but I’ll document my journey step by step as I work through the docs.
Why I Decided to Learn PyTauri
Recently, I’ve been working on desktop applications using Tauri at work. We had originally built an app with machine learning and generative AI features in Python, but anticipated distribution challenges, so we started converting most of it to Rust. During this process, we needed to implement machine learning components that couldn’t be easily converted to Rust using PyO3. While researching PyO3, I stumbled upon PyTauri. Since converting to Rust wasn’t always straightforward and we were already using PyO3 for hard-to-convert parts, the name alone piqued my interest. At the time of writing, I don’t fully understand what PyTauri is, but I’m going to start building an app with it right now.
How to Use PyTauri
Environment Setup
I’ll proceed by following the PyTauri documentation. According to the docs, Windows 10 is the top priority platform, followed by Linux (including WSL), Mac OS, and Windows 7 which are less tested. Since I don’t have Windows 10, I’ll proceed with Windows 11. This post assumes PyTauri 0.8. You’ll need Python 3.9 or higher and the latest stable version of Rust installed beforehand.
The System Dependencies section links to Tauri’s Prerequisites, so I’ll check that. For Windows, you need to download Microsoft C++ Build Tools and select “Desktop development with C++” in the installer. There are mentions of WebView2 and VBSCRIPT, but I’ll skip those for now unless needed. I’ll also skip Rust since it should already be installed. For Node.js, it’s recommended if you’re comfortable with web development (Note: the tutorial uses web technologies, so you should probably install it). While you can build desktop apps with Python frameworks like FastAPI, Gradio, or NiceGUI, Node.js seems beneficial from a UI flexibility and performance perspective if you’re interested in web technologies. On the other hand, there’s an advantage to building with Python only, so choose based on your skills and preferences. I’ll skip “Configure for Mobile Targets” as it’s for Tauri’s mobile support and not directly relevant to PyTauri.
Since I want to learn PyTauri rather than Tauri, I’ll return to the PyTauri tutorial and navigate to Getting Started. The Create PyTauri App page appears. Here, uv is suddenly introduced, so let’s install it using PowerShell or Command Prompt.
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"We also need tauri-apps/cli and create-tauri-app, so let’s install them.
npm add -D @tauri-apps/cli
cargo install create-tauri-app --lockedProject Setup
Next, we’ll create a Tauri project, so let’s run the command.
> npm create tauri-app
Need to install the following packages:
create-tauri-app@4.6.2
Ok to proceed? (y) y
> npx
> create-tauri-app
✔ Project name · hello_pytauri
✔ Identifier · com.rmc-8.hello_pytauri
✔ Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, deno, bun)
✔ Choose your package manager · npm
✔ Choose your UI template · Svelte - (https://svelte.dev/)
✔ Choose your UI flavor · TypeScript
Template created! To get started run:
cd hello_pytauri
npm install
npm run tauri android init
For Desktop development, run:
npm run tauri dev
For Android development, run:
npm run tauri android devI chose npm, Svelte, and TypeScript for convenience and preference, but any choice works. The project structure looks roughly like this:
└─hello_pytauri
├─.vscode
├─src
│ └─routes
├─src-tauri
│ ├─capabilities
│ ├─icons
│ └─src
└─statichello_pytauri is the entire app, src is the frontend, and src-tauri is the Rust side. So far, there’s no sign of Python as everything is filled with other languages, but let’s continue reading.
PyTauri Setup
Moving to Using PyTauri, Python finally appears. First, create a virtual environment with uv.
uv venv --python-preference only-systemThen activate the virtual environment. Since we’re on Windows, here’s the PowerShell command:
.venv\Scripts\Activate.ps1Next, create pyproject.toml inside src-tauri/. It seems we’ll be injecting Python mechanisms into the Rust backend.
[project]
name = "hello_pytauri"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.9"
dependencies = ["pytauri == 0.8.*"]
[project.entry-points.pytauri]
ext_mod = "tauri_app.ext_mod"
[build-system]
requires = ["setuptools >= 80"]
build-backend = "setuptools.build_meta"
[tool.setuptools.packages]
find = { where = ["python"] }Then install with uv’s pip:
uv pip install -e src-tauriAfter that, write code in src-tauri/python/tauri_app/__init__.py:
from pytauri import (
builder_factory,
context_factory,
)
def main() -> int:
app = builder_factory().build(
context=context_factory(),
invoke_handler=None, # TODO
)
exit_code = app.run_return()
return exit_codeAlso write code in src-tauri/python/tauri_app/__main__.py:
import sys
from multiprocessing import freeze_support
from tauri_app import main
freeze_support()
sys.exit(main())Add pytauri and pyo3 to [dependencies] in Cargo.toml:
[dependencies]
# ...
pytauri = { version = "0.8" }
pyo3 = { version = "0.25" }Add the following to the build configuration in src-tauri/tauri.conf.json:
{
"build": {
"features": ["pytauri/standalone"]
}
}Then copy-paste code into src-tauri/src/lib.rs. It appears suddenly without explanation, so let’s just treat it as boilerplate and copy it without hesitation:
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)
},
)
}
}Similarly, copy-paste src-tauri/src/main.rs. Note that I’ve adjusted the use hello_pytauri_lib part to match your app name:
// 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::utils::platform::resource_dir;
use hello_pytauri_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`.
let context = tauri_generate_context();
let resource_dir = resource_dir(context.package_info(), &tauri::Env::default())
.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);
}Create src-tauri/.taurignore with the following content:
__pycache__Add , "**/.venv/**" to the ignored list in vite.config.(js|ts):
import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig(async () => ({
// ...
server: {
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ["**/src-tauri/**", "**/.venv/**"],
},
},
}));We’ve completed the lengthy preparation. Good work! Now we’ll set up communication between the Python backend and TypeScript frontend, but there’s still a bit more preparation needed.
Preparing Communication Between Python and TypeScript
Let’s move to IPC between Python and JavaScript. Add more to [dependencies] in Cargo.toml:
[dependencies]
tauri-plugin-pytauri = { version = "0.8" }Add permissions for using PyTauri to src-tauri/capabilities/default.json:
{
// ...
"permissions": [
// ...
"pytauri:default"
]
}Add pydantic and anyio to [project] in src-tauri/pyproject.toml:
# ...
[project]
# ...
dependencies = [
# ...
"pydantic == 2.*",
"anyio == 4.*"
]Since manually adding them doesn’t install them in the virtual environment, run uv sync to add them. Since we’ve been using TypeScript, let’s move to Generate TypeScript Client for IPC and continue. Install additional packages with npm:
npm install json-schema-to-typescript --save-dev
npm instal json2ts --helpThen write src-tauri/python/tauri_app/__init__.py:
import sys
from os import getenv
from pathlib import Path
from anyio.from_thread import start_blocking_portal # noqa: F401
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
from pytauri import Commands, builder_factory, context_factory
# Enable TypeScript type definition generation (dev only)
PYTAURI_GEN_TS = getenv("PYTAURI_GEN_TS") != "0"
commands: Commands = Commands(experimental_gen_ts=PYTAURI_GEN_TS)
class _BaseModel(BaseModel):
model_config = ConfigDict(
# Automatically convert JavaScript camelCase to Python snake_case
alias_generator=to_camel,
# Forbid unknown fields (improves TypeScript type safety)
extra="forbid",
)
class Person(_BaseModel):
"""A person to greet.
@property name - The name of the person.
"""
name: str
@commands.command()
async def greet_to_person(body: Person) -> str:
"""A simple command that returns a greeting message from Python backend.
@param body - The person to greet.
"""
python_version = ".".join(sys.version.split(".")[:2]) # e.g., "3.12"
return f"Hello, {body.name}! 👋 This greeting comes from Python {python_version} backend!"
def main() -> int:
with start_blocking_portal(
"asyncio"
) as portal: # Added (trio can be used instead of asyncio)
if PYTAURI_GEN_TS:
# Generate TypeScript client code in src/lib directory
output_dir = Path(__file__).parent.parent.parent.parent / "src" / "lib"
json2ts_cmd = "npm json2ts --format=false"
# Auto-generate TypeScript type definitions in background
portal.start_task_soon(
lambda: commands.experimental_gen_ts_background(
output_dir, json2ts_cmd, cmd_alias=to_camel
)
)
app = builder_factory().build(
context=context_factory(),
invoke_handler=commands.generate_handler(portal), # Added
)
exit_code = app.run_return()
return exit_code
It’s important to define functions callable from TypeScript using the @commands.command() decorator, and to safely define types for arguments and return values using Pydantic. Pydantic is highly reliable as it’s used in FastAPI and LangChain, but this might not be intuitive for Pythonistas since Python is dynamically typed. For the communication client, asyncio and trio are available, but asyncio is fine unless you have a specific reason to use trio.
Preparing to Call Python Functions from TypeScript
Next, we’ll call Python functions from TypeScript. Calling functions requires an additional package, so add it with npm install tauri-plugin-pytauri-api. Then copy-paste according to the documentation. Since I’m building with SvelteKit, I’ll place it in src\lib\main.ts.
/* eslint-disable */
/**
* This file was automatically generated by pytauri-gen-ts.
* DO NOT MODIFY IT BY HAND. Instead, modify the source commands API,
* and run pytauri-gen-ts to regenerate this file.
*/
import { pyInvoke } from "tauri-plugin-pytauri-api";
import type { InvokeOptions } from "@tauri-apps/api/core";
import type { Commands } from "./_apiTypes.d.ts";
/**
* A simple command that returns a greeting message.
*
* @param body - The person to greet.
*/
export async function greetToPerson(
body: Commands["greet_to_person"]["input"],
options?: InvokeOptions
): Promise<Commands["greet_to_person"]["output"]> {
return await pyInvoke("greet_to_person", body, options);
}You can confirm the consistency between greet_to_person defined in Python and greetToPerson defined in TypeScript. At this point, the corresponding type declaration for ./_apiTypes.d.ts can’t be found, but that’s okay. Now let’s try calling the process from Svelte:
<script lang="ts">
import { greetToPerson } from "$lib/main"; // Import Python process from main.ts
let name = $state("");
let greetMsg = $state("");
async function greet(event: Event) { // Wrap greetToPerson in a function
event.preventDefault();
greetMsg = await greetToPerson({ name });
}
</script>
<main class="container">
<h1>Welcome to Tauri + Svelte</h1>
<div class="row">
<a href="https://vite.dev" target="_blank">
<img src="/vite.svg" class="logo vite" alt="Vite Logo" />
</a>
<a href="https://tauri.app" target="_blank">
<img src="/tauri.svg" class="logo tauri" alt="Tauri Logo" />
</a>
<a href="https://svelte.dev" target="_blank">
<img src="/svelte.svg" class="logo svelte-kit" alt="SvelteKit Logo" />
</a>
</div>
<p>Click on the Tauri, Vite, and SvelteKit logos to learn more.</p>
<form class="row" onsubmit={greet}> <!-- Call greet -->
<input id="greet-input" placeholder="Enter a name..." bind:value={name} />
<button type="submit">Greet</button>
</form>
<p>{greetMsg}</p>
</main>Run PyTauri with npm run tauri dev. If there are no errors, a window will open, so try entering a name.

If the greeting message appears as defined in the Python function, you’ve succeeded!
Thoughts So Far
There are more sections after this on state management with PyTauri and using Tauri plugins, but this post has gotten quite long, so I’ll stop here. The manual process of embedding Python modules into Tauri was quite complex and honestly tedious. Also, to connect Python and TypeScript (or JavaScript), you need to define types using Pydantic on the Python side and match function names while juggling snake_case and camelCase notation, which is somewhat tricky and difficult to write manually. If I were to actually build an app, I’d probably streamline this with AI-assisted coding. However, Python has incredibly powerful machine learning libraries, you can use the LangChain/LangGraph framework for generative AI, and there’s a vast array of other libraries. The advantage is easy integration of Python-powered features, making it a good choice for Pythonistas or those who want to leverage Python libraries while creating easily distributable desktop apps.
Conclusion
While there are potential projects I could build with PyTauri, I don’t have any specific ideas I’m eager to pursue right now. I hope the manual work in PyTauri gets reduced, and I’d like to revisit PyTauri after letting it simmer for a while. With Tauri, it was Rust and frontend, so if you can tolerate the tediousness, this seems like an interesting framework for Pythonistas as a Python-based Tauri alternative. If you’re curious enough, please give it a try!

![[Python] Summarizing and Translating Foreign Tech Articles Using Generative AI](https://pub-21c8df4785a6478092d6eb23a55a5c42.r2.dev/img/eyecatch/langchain_eyecatch.webp)

![[Python] Retrieving Article Information from Dev Community (dev.to) via API](https://pub-21c8df4785a6478092d6eb23a55a5c42.r2.dev/img/eyecatch/devto_eycatch.webp)
![[Python] Automatically Record Sleep Time to Toggl](https://pub-21c8df4785a6478092d6eb23a55a5c42.r2.dev/img/eyecatch/garmin_toggl.webp)
