I'm finishing an application that has a custom context menu with "Open" and "Open Containing Folder" working. I can't seem to get an "Open With" option like you see in file explorers. Depending on the file type there would be different options — so I don't think we're talking about predefined lists, but rather something extracted from the host OS. Any help with "Open With" would be greatly appreciated.
When you right-click a file in a file manager like Nemo or Nautilus, you see a list of applications that can handle that file type. That list comes from the operating system's knowledge of installed applications and their supported MIME types. To recreate this in your own PyQt6 application, you need to query that same information.
On Linux desktops (GNOME, Cinnamon, XFCE, etc.), this information is managed through the freedesktop.org standards. The gio module from PyGObject (the Python bindings for GLib/GIO) gives you clean access to all of it — which applications support a given MIME type, what their names and icons are, and how to launch them.
Let's walk through building this step by step.
Understanding the moving parts
There are three things you need to do:
- Determine the MIME type of a file (e.g.,
image/png,text/plain). - Find the applications that can open that MIME type.
- Launch the chosen application with the file as an argument.
The GIO library handles all three. If you're on a Linux desktop with GTK-based tools installed, you almost certainly have gi (PyGObject) available already. If not, you can install it:
sudo apt install python3-gi gir1.2-gio-2.0
Getting the MIME type for a file
GIO can detect the MIME type of a file by inspecting its contents and name:
from gi.repository import Gio
def get_mime_type(filepath):
"""Return the MIME type string for a given file path."""
gfile = Gio.File.new_for_path(filepath)
info = gfile.query_info(
"standard::content-type", Gio.FileQueryInfoFlags.NONE, None
)
return info.get_content_type()
Calling get_mime_type("/home/user/photo.png") would return something like "image/png".
Finding applications for a MIME type
Once you have the MIME type, you can ask GIO for a list of applications that support it:
from gi.repository import Gio
def get_apps_for_mime(mime_type):
"""Return a list of Gio.AppInfo objects that can open the given MIME type."""
return Gio.AppInfo.get_all_for_type(mime_type)
Each Gio.AppInfo object gives you the application's display name (e.g., "GIMP Image Editor"), its icon, and the ability to launch it with a file.
Launching an application with a file
To open a file with a specific application:
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PyQt6 Edition) The hands-on guide to making apps with Python — Save time and build better with this book. Over 15K copies sold.
from gi.repository import Gio
def open_file_with_app(app_info, filepath):
"""Launch the given app with the specified file."""
gfile = Gio.File.new_for_path(filepath)
app_info.launch([gfile], None)
That's the complete backend. Now let's wire it into a PyQt6 context menu.
Building the context menu in PyQt6
The idea is straightforward: when the user right-clicks, you build a QMenu that includes an "Open With" submenu. That submenu is populated dynamically based on the file they clicked on. If you're new to building menus and actions in PyQt6, the Actions, Toolbars & Menus tutorial covers the fundamentals.
Here's a minimal example using a QListWidget to display some file paths. Right-clicking a file shows the context menu with an "Open With" submenu:
import sys
import os
import subprocess
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QApplication,
QListWidget,
QMainWindow,
QMenu,
)
from gi.repository import Gio
def get_mime_type(filepath):
"""Return the MIME type string for a given file path."""
gfile = Gio.File.new_for_path(filepath)
info = gfile.query_info(
"standard::content-type", Gio.FileQueryInfoFlags.NONE, None
)
return info.get_content_type()
def get_apps_for_mime(mime_type):
"""Return a list of Gio.AppInfo objects for the given MIME type."""
return Gio.AppInfo.get_all_for_type(mime_type)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Open With Example")
self.resize(500, 400)
self.list_widget = QListWidget()
self.setCentralWidget(self.list_widget)
# Add some example files — replace these with real paths on your system.
example_files = [
os.path.expanduser("~/Documents"),
os.path.expanduser("~/Pictures"),
]
# Walk through directories and add files.
for folder in example_files:
if os.path.isdir(folder):
for entry in os.listdir(folder):
full_path = os.path.join(folder, entry)
if os.path.isfile(full_path):
self.list_widget.addItem(full_path)
self.list_widget.setContextMenuPolicy(
Qt.ContextMenuPolicy.CustomContextMenu
)
self.list_widget.customContextMenuRequested.connect(
self.show_context_menu
)
def show_context_menu(self, position):
item = self.list_widget.itemAt(position)
if item is None:
return
filepath = item.text()
menu = QMenu(self)
# "Open" action — uses the system default application.
open_action = menu.addAction("Open")
open_action.triggered.connect(
lambda: self.open_default(filepath)
)
# "Open Containing Folder" action.
open_folder_action = menu.addAction("Open Containing Folder")
open_folder_action.triggered.connect(
lambda: self.open_containing_folder(filepath)
)
# "Open With" submenu.
open_with_menu = menu.addMenu("Open With...")
self.populate_open_with_menu(open_with_menu, filepath)
menu.exec(self.list_widget.mapToGlobal(position))
def populate_open_with_menu(self, submenu, filepath):
"""Fill the 'Open With' submenu with available applications."""
try:
mime_type = get_mime_type(filepath)
except Exception:
no_apps = submenu.addAction("(unable to detect file type)")
no_apps.setEnabled(False)
return
apps = get_apps_for_mime(mime_type)
if not apps:
no_apps = submenu.addAction("(no applications found)")
no_apps.setEnabled(False)
return
for app_info in apps:
app_name = app_info.get_display_name()
action = submenu.addAction(app_name)
# Use a default argument in the lambda to capture the current
# app_info, avoiding the common closure-in-a-loop issue.
action.triggered.connect(
lambda checked, ai=app_info: self.open_with_app(
ai, filepath
)
)
def open_default(self, filepath):
"""Open the file with the system default application."""
subprocess.Popen(["xdg-open", filepath])
def open_containing_folder(self, filepath):
"""Open the folder containing the file."""
folder = os.path.dirname(filepath)
subprocess.Popen(["xdg-open", folder])
def open_with_app(self, app_info, filepath):
"""Launch a specific application with the given file."""
gfile = Gio.File.new_for_path(filepath)
try:
app_info.launch([gfile], None)
except Exception as e:
print(f"Failed to launch {app_info.get_display_name()}: {e}")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
You can copy this, update the example_files list with folders that exist on your system, and run it. Right-clicking any file in the list will show a context menu with the full "Open With..." submenu populated from your installed applications.
How the lambda capture works
You might have noticed this pattern in the loop:
action.triggered.connect(
lambda checked, ai=app_info: self.open_with_app(ai, filepath)
)
The ai=app_info part is a default argument that captures the current value of app_info at the time the lambda is created. Without it, every menu action would end up using the last value of app_info from the loop — a common and frustrating Python gotcha when connecting signals inside loops. For more on how signals and slots work in PyQt6, including lambda connections, see Signals, Slots & Events.
Adding application icons
You can make the submenu look more polished by including application icons. GIO provides icon information through app_info.get_icon(), and you can resolve this to a file path using the system icon theme. Here's an updated version of the populate_open_with_menu method:
from PyQt6.QtGui import QIcon
def populate_open_with_menu(self, submenu, filepath):
"""Fill the 'Open With' submenu with available applications."""
try:
mime_type = get_mime_type(filepath)
except Exception:
action = submenu.addAction("(unable to detect file type)")
action.setEnabled(False)
return
apps = get_apps_for_mime(mime_type)
if not apps:
action = submenu.addAction("(no applications found)")
action.setEnabled(False)
return
for app_info in apps:
app_name = app_info.get_display_name()
icon = app_info.get_icon()
action = submenu.addAction(app_name)
# Try to load the icon from the system icon theme.
if icon is not None and hasattr(icon, "get_names"):
for icon_name in icon.get_names():
qt_icon = QIcon.fromTheme(icon_name)
if not qt_icon.isNull():
action.setIcon(qt_icon)
break
action.triggered.connect(
lambda checked, ai=app_info: self.open_with_app(
ai, filepath
)
)
QIcon.fromTheme() looks up icons from the system's icon theme (the same one your desktop environment uses), so the icons in your submenu will match what users see in their native file manager.
A note on cross-platform compatibility
This approach relies on GIO, which is a GLib/GNOME technology. It works well on Linux desktops that follow freedesktop.org standards, which includes Linux Mint, Ubuntu, Fedora, and most others.
If you need cross-platform support:
- On macOS, you can use
subprocess.Popen(["open", "-a", app_name, filepath])and query available apps usingNSWorkspacevia PyObjC. - On Windows, you can use the
SHAssocEnumHandlersCOM API or query the registry, though this is more involved.
For a Linux-focused application, GIO is the natural and reliable choice. When you're ready to distribute your finished application, take a look at packaging PyQt6 apps for Linux with PyInstaller to create standalone executables.
Complete working example
Here is the full application with icons included. If you need help setting up your PyQt6 development environment first, see the PyQt6 installation guide for Linux.
import sys
import os
import subprocess
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import (
QApplication,
QListWidget,
QMainWindow,
QMenu,
)
from gi.repository import Gio
def get_mime_type(filepath):
"""Return the MIME type string for a given file path."""
gfile = Gio.File.new_for_path(filepath)
info = gfile.query_info(
"standard::content-type", Gio.FileQueryInfoFlags.NONE, None
)
return info.get_content_type()
def get_apps_for_mime(mime_type):
"""Return a list of Gio.AppInfo objects for the given MIME type."""
return Gio.AppInfo.get_all_for_type(mime_type)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Open With Example")
self.resize(500, 400)
self.list_widget = QListWidget()
self.setCentralWidget(self.list_widget)
# Add files from common directories. Update these paths to
# match your system.
search_dirs = [
os.path.expanduser("~/Documents"),
os.path.expanduser("~/Pictures"),
os.path.expanduser("~/Downloads"),
]
for folder in search_dirs:
if os.path.isdir(folder):
for entry in sorted(os.listdir(folder)):
full_path = os.path.join(folder, entry)
if os.path.isfile(full_path):
self.list_widget.addItem(full_path)
self.list_widget.setContextMenuPolicy(
Qt.ContextMenuPolicy.CustomContextMenu
)
self.list_widget.customContextMenuRequested.connect(
self.show_context_menu
)
def show_context_menu(self, position):
item = self.list_widget.itemAt(position)
if item is None:
return
filepath = item.text()
menu = QMenu(self)
# Open with default application.
open_action = menu.addAction("Open")
open_action.triggered.connect(
lambda: self.open_default(filepath)
)
# Open containing folder.
open_folder_action = menu.addAction("Open Containing Folder")
open_folder_action.triggered.connect(
lambda: self.open_containing_folder(filepath)
)
menu.addSeparator()
# Open With submenu.
open_with_menu = menu.addMenu("Open With...")
self.populate_open_with_menu(open_with_menu, filepath)
menu.exec(self.list_widget.mapToGlobal(position))
def populate_open_with_menu(self, submenu, filepath):
"""Fill the 'Open With' submenu with available applications."""
try:
mime_type = get_mime_type(filepath)
except Exception:
action = submenu.addAction("(unable to detect file type)")
action.setEnabled(False)
return
apps = get_apps_for_mime(mime_type)
if not apps:
action = submenu.addAction("(no applications found)")
action.setEnabled(False)
return
for app_info in apps:
app_name = app_info.get_display_name()
icon = app_info.get_icon()
action = submenu.addAction(app_name)
# Try to set the application's icon from the system theme.
if icon is not None and hasattr(icon, "get_names"):
for icon_name in icon.get_names():
qt_icon = QIcon.fromTheme(icon_name)
if not qt_icon.isNull():
action.setIcon(qt_icon)
break
action.triggered.connect(
lambda checked, ai=app_info: self.open_with_app(
ai, filepath
)
)
def open_default(self, filepath):
"""Open the file using the system default application."""
subprocess.Popen(["xdg-open", filepath])
def open_containing_folder(self, filepath):
"""Open the directory that contains the file."""
folder = os.path.dirname(filepath)
subprocess.Popen(["xdg-open", folder])
def open_with_app(self, app_info, filepath):
"""Launch a specific application to open the given file."""
gfile = Gio.File.new_for_path(filepath)
try:
app_info.launch([gfile], None)
except Exception as e:
print(f"Failed to launch {app_info.get_display_name()}: {e}")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Run this application, and you'll see a list of files from your home directories. Right-click any file and you'll get a context menu with "Open", "Open Containing Folder", and an "Open With..." submenu listing every application your system knows can handle that file type — just like a native file manager.