py_engine_desktop
A Flutter plugin for managing embedded Python runtimes on desktop platforms (Windows, macOS, Linux). This plugin allows you to run Python scripts and interactive REPLs directly from your Flutter desktop applications.
π― Production Ready - Tested on Windows & macOS, Linux testing in progress
Features
- π Embedded Python Runtime: Automatically downloads and extracts portable Python distributions
- π₯οΈ Desktop Support: Works on Windows, macOS, and Linux
- π Script Execution: Run Python scripts with real-time stdout/stderr output
- π Interactive REPL: Start Python REPLs and send commands interactively
- π¦ Package Management: Install Python packages using pip
- π Easy Setup: One-time initialization handles everything automatically
Supported Platforms
Platform | Support | Architecture | Tested |
---|---|---|---|
Windows | β | x64 | β |
macOS | β | x64 | β |
Linux | β | x64 | π In Progress |
Android | β | - | - |
iOS | β | - | - |
Web | β | - | - |
Installation
Add this to your package's pubspec.yaml
file:
dependencies:
py_engine_desktop: ^0.0.1
Then run:
flutter pub get
π Important: macOS Configuration
For macOS apps, you MUST disable sandbox mode to allow Python executable execution.
In your Flutter macOS project, update the following files:
macos/Runner/DebugProfile.entitlements
and macos/Runner/Release.entitlements
Change:
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
To:
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
</dict>
β οΈ Note: Disabling sandbox mode is required for executing Python processes. This is a limitation of macOS security model when running external executables. Without this change, you'll get
ProcessException: Operation not permitted
errors.
Quick Start
1. Add dependency
Add this to your pubspec.yaml
:
dependencies:
py_engine_desktop: ^0.0.1
2. Import and Initialize
import 'package:py_engine_desktop/py_engine_desktop.dart';
class MyPythonApp extends StatefulWidget {
@override
_MyPythonAppState createState() => _MyPythonAppState();
}
class _MyPythonAppState extends State<MyPythonApp> {
bool _initialized = false;
@override
void initState() {
super.initState();
_initializePython();
}
Future<void> _initializePython() async {
try {
await PyEngineDesktop.init();
setState(() => _initialized = true);
print('Python engine ready!');
} catch (e) {
print('Failed to initialize: $e');
}
}
}
3. Run Python Scripts
Future<void> runPythonScript() async {
if (!_initialized) return;
// Create a simple Python script
final script = '''
import math
print("Hello from Python!")
print(f"Pi = {math.pi}")
for i in range(5):
print(f"Count: {i}")
''';
// Write script to temp file
final tempDir = await getTemporaryDirectory();
final scriptFile = File(path.join(tempDir.path, 'my_script.py'));
await scriptFile.writeAsString(script);
// Execute the script
final pythonScript = await PyEngineDesktop.startScript(scriptFile.path);
// Listen to output
pythonScript.stdout.listen((line) {
print('Python Output: $line');
});
pythonScript.stderr.listen((line) {
print('Python Error: $line');
});
// Wait for completion
await pythonScript.exitCode;
print('Script completed!');
}
4. Interactive Python REPL
PythonRepl? _repl;
Future<void> startInteractivePython() async {
if (!_initialized) return;
_repl = await PyEngineDesktop.startRepl();
// Listen to all output
_repl!.output.listen((output) {
print('REPL: $output');
});
// Send some commands
_repl!.send('import numpy as np');
_repl!.send('arr = np.array([1, 2, 3, 4, 5])');
_repl!.send('print("Array:", arr)');
_repl!.send('print("Mean:", np.mean(arr))');
}
void sendCommand(String command) {
if (_repl != null) {
_repl!.send(command);
}
}
5. Package Management
Future<void> setupPythonPackages() async {
if (!_initialized) return;
// Install essential packages
await PyEngineDesktop.pipInstall('numpy');
await PyEngineDesktop.pipInstall('pandas');
await PyEngineDesktop.pipInstall('requests');
print('Packages installed successfully!');
}
// Test if packages work
Future<void> testPackages() async {
final repl = await PyEngineDesktop.startRepl();
repl.output.listen((output) => print(output));
// Test numpy
repl.send('import numpy as np');
repl.send('print("NumPy version:", np.__version__)');
// Test pandas
repl.send('import pandas as pd');
repl.send('df = pd.DataFrame({"A": [1,2,3], "B": [4,5,6]})');
repl.send('print(df)');
repl.stop();
}
6. Complete Widget Example
class PythonConsole extends StatefulWidget {
@override
_PythonConsoleState createState() => _PythonConsoleState();
}
class _PythonConsoleState extends State<PythonConsole> {
final TextEditingController _controller = TextEditingController();
final List<String> _output = [];
PythonRepl? _repl;
bool _initialized = false;
@override
void initState() {
super.initState();
_initPython();
}
Future<void> _initPython() async {
await PyEngineDesktop.init();
_repl = await PyEngineDesktop.startRepl();
_repl!.output.listen((line) {
setState(() => _output.add(line));
});
setState(() => _initialized = true);
}
void _sendCommand() {
final command = _controller.text.trim();
if (command.isNotEmpty && _repl != null) {
setState(() => _output.add('>>> $command'));
_repl!.send(command);
_controller.clear();
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: Container(
padding: EdgeInsets.all(8),
color: Colors.black,
child: ListView.builder(
itemCount: _output.length,
itemBuilder: (context, index) => Text(
_output[index],
style: TextStyle(color: Colors.green, fontFamily: 'monospace'),
),
),
),
),
if (_initialized)
Padding(
padding: EdgeInsets.all(8),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(hintText: 'Enter Python command...'),
onSubmitted: (_) => _sendCommand(),
),
),
IconButton(
icon: Icon(Icons.send),
onPressed: _sendCommand,
),
],
),
),
],
);
}
@override
void dispose() {
_repl?.stop();
super.dispose();
}
}
API Reference
PyEngineDesktop
Main class providing static methods for Python engine management.
PyEngineDesktop.init()
Purpose: Initializes the Python engine by extracting and setting up the embedded Python runtime.
What it does:
- Extracts Python runtime from bundled assets (first run only)
- Sets up Python executable with proper permissions
- Configures site-packages directory for pip installations
- Caches runtime for faster subsequent launches
// Basic initialization
await PyEngineDesktop.init();
// With error handling
try {
await PyEngineDesktop.init();
print('Python engine ready!');
} catch (e) {
if (e is UnsupportedError) {
print('Platform not supported');
} else {
print('Initialization failed: $e');
}
}
PyEngineDesktop.startScript(String scriptPath)
Purpose: Executes a Python script file and returns a PythonScript
object for monitoring.
What it does:
- Validates script file exists before execution
- Starts Python process with the script
- Automatically includes site-packages in Python path
- Returns
PythonScript
object for output monitoring and control
// Basic script execution
final script = await PyEngineDesktop.startScript('/path/to/script.py');
// Listen to output streams
script.stdout.listen((line) => print('Output: $line'));
script.stderr.listen((line) => print('Error: $line'));
// Wait for completion
final exitCode = await script.exitCode;
print('Script finished with code: $exitCode');
// Or stop manually if needed
script.stop();
PyEngineDesktop.startRepl()
Purpose: Starts an interactive Python REPL (Read-Eval-Print Loop) session.
What it does:
- Launches Python in interactive mode using
code.interact()
- Automatically includes site-packages for installed packages
- Combines stdout/stderr into single output stream
- Allows sending commands via
send()
method
// Start REPL and send commands
final repl = await PyEngineDesktop.startRepl();
// Listen to all output (both results and prompts)
repl.output.listen((output) => print(output));
// Send Python commands
repl.send('print("Hello Python!")');
repl.send('x = 5 + 3');
repl.send('print(f"Result: {x}")');
// Send multi-line code
repl.send('for i in range(3):');
repl.send(' print(f"Count: {i}")');
// Stop when done
repl.stop();
PyEngineDesktop.pipInstall(String package)
/ PyEngineDesktop.pipUninstall(String package)
Purpose: Manages Python packages using pip package manager.
What it does:
pipInstall
: Downloads and installs Python packages from PyPIpipUninstall
: Removes installed Python packages- Automatically downloads and sets up pip if not available
- Installs packages to embedded Python's site-packages directory
// Install packages
try {
await PyEngineDesktop.pipInstall('numpy');
await PyEngineDesktop.pipInstall('requests==2.28.1'); // Specific version
print('Packages installed successfully');
} catch (e) {
print('Installation failed: $e');
}
// Uninstall packages
try {
await PyEngineDesktop.pipUninstall('numpy');
print('Package uninstalled successfully');
} catch (e) {
print('Uninstallation failed: $e');
}
PyEngineDesktop.pythonPath
Purpose: Gets the absolute path to the embedded Python executable.
What it does:
- Returns full path to Python executable after initialization
- Throws
StateError
if called beforeinit()
- Path points to embedded Python runtime, not system Python
// Get Python executable path
await PyEngineDesktop.init();
final pythonPath = PyEngineDesktop.pythonPath;
print('Python executable: $pythonPath');
// Output example: C:\Users\...\AppData\Roaming\py_engine_desktop\python\python.exe
PyEngineDesktop.stopScript(PythonScript script)
Purpose: Stops a running Python script process.
What it does:
- Terminates the script process immediately
- Closes output streams
- Safe to call multiple times
final script = await PyEngineDesktop.startScript('script.py');
// ... later
await PyEngineDesktop.stopScript(script);
// Or use script.stop() directly
PyEngineDesktop.stopRepl(PythonRepl repl)
Purpose: Stops a running Python REPL session.
What it does:
- Terminates the REPL process
- Closes output streams
- Safe to call multiple times
final repl = await PyEngineDesktop.startRepl();
// ... later
await PyEngineDesktop.stopRepl(repl);
// Or use repl.stop() directly
PythonScript
Object returned by startScript()
representing a running Python script.
Properties:
Stream<String> stdout
- Script's standard output (line by line)Stream<String> stderr
- Script's error output (line by line)Process process
- Underlying Dart process objectFuture<int> exitCode
- Completes when script finishes with exit code
Methods:
void stop()
- Terminates the script immediately
PythonRepl
Object returned by startRepl()
representing an interactive Python session.
Properties:
Stream<String> output
- Combined stdout/stderr output streamProcess process
- Underlying Dart process objectFuture<int> exitCode
- Completes when REPL session ends
Methods:
void send(String code)
- Sends Python code to executevoid stop()
- Terminates the REPL session
Example Usage
Check out the example directory for a complete demo application that shows:
- Python engine initialization
- Running Python scripts with output display
- Interactive REPL with command input
- Installing and testing NumPy package
How It Works
Python Runtime Distribution
The plugin automatically downloads portable Python distributions:
- Windows: Embeddable Python distribution (python.org)
- macOS/Linux: Standalone Python builds (python-build-standalone)
File Locations
Python runtimes are extracted to platform-specific application support directories:
- Windows:
%AppData%/py_engine_desktop/python
- macOS:
~/Library/Application Support/py_engine_desktop/python
- Linux:
~/.local/share/py_engine_desktop/python
First Run Setup
On first initialization:
- Detects the current platform
- Extracts the appropriate Python runtime from assets
- Sets up the Python environment
- Subsequent runs use the cached Python installation
Error Handling
The plugin provides comprehensive error handling:
try {
await PyEngineDesktop.init();
} catch (e) {
if (e is UnsupportedError) {
print('Platform not supported: $e');
} else {
print('Initialization failed: $e');
}
}
Limitations
- Desktop Only: Only works on desktop platforms (Windows, macOS, Linux)
- Single Architecture: Currently supports x64 architectures only
- Python Version: Uses Python 3.11.x
- Size: Python runtimes add ~10-30MB to your app's size
- First Run: Initial setup requires extracting Python runtime (one-time delay)
Performance & Size
Platform | Runtime Size | First Init Time |
---|---|---|
Windows | ~15MB | 2-5 seconds |
macOS | ~25MB | 3-7 seconds |
Linux | ~30MB | 3-8 seconds |
Note: Python runtime is cached after first initialization. Subsequent app launches are instant.
Development
Building from Source
git clone https://github.com/NagarChinmay/py_engine_desktop.git
cd py_engine_desktop
flutter pub get
cd example
flutter run
Running Tests
flutter test
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
Issues & Support
- π Bug Reports: GitHub Issues
- π‘ Feature Requests: GitHub Issues
- π Documentation: Check the example for complete usage
License
This project is licensed under the MIT License - see the LICENSE file for details.
Changelog
See CHANGELOG.md for a list of changes.