Declare Plugin Dependencies for a QGIS Plugin
A plugin that fails on load with a traceback is a plugin people uninstall. Most such failures are dependency problems: a QGIS version older than the API the plugin uses, another plugin it relies on that is not installed, a Python package that is missing from the user's QGIS. QGIS's plugin metadata can declare the first two so the plugin manager handles them, and a few lines of load-time checking turn the third into a helpful message instead of a crash.
This recipe belongs to Plugin Boilerplate & Structure. It sets the QGIS version range, declares dependencies on other plugins, checks Python packages at load time, keeps optional features optional, and tests that the declared minimum really works.
Prerequisites
- A plugin with a
metadata.txt, as created in creating a QGIS plugin with Plugin Builder. - A list of what the plugin actually uses: QGIS APIs by version, other plugins, third-party Python packages.
Set the QGIS version range
qgisMinimumVersion is required in every plugin's metadata; qgisMaximumVersion is optional. The plugin manager hides plugins outside the range of the running QGIS.
[general]
name=Asset Sync
version=1.4.0
qgisMinimumVersion=3.34
qgisMaximumVersion=4.99
description=Synchronise asset layers with the corporate asset register
about=Pushes edits from QGIS layers to the asset register API and pulls updates back.
Breakdown: Set the minimum to the oldest release you test on and whose APIs you use — typically the current LTR, 3.34 or 3.40. Methods documented as "since QGIS 3.36" in the API reference mean a 3.34 minimum is wrong if you call them. qgisMaximumVersion defaults to the same major version as the minimum, so a plugin declaring only qgisMinimumVersion=3.34 is treated as QGIS 3 only and hidden on QGIS 4; declaring 4.99 states that it has been made compatible with QGIS 4, as described in porting a plugin to QGIS 4 and Qt6. Only raise the maximum once you have tested on that major version.
Declare dependencies on other plugins
A plugin that builds on another — a Processing provider that needs the "QuickOSM" plugin's algorithms, a tool that extends "Data Plotly" — declares it with plugin_dependencies.
plugin_dependencies=QuickOSM==2.2.3,Data Plotly
Breakdown: The value is a comma-separated list of plugin names as they appear in the repository, optionally with an exact version after ==. When a user installs your plugin, the plugin manager checks the list and offers to install missing dependencies. Pin a version only when you really need that exact one; an unpinned name accepts any installed version. Dependencies are a convenience, not a guarantee — users can decline or uninstall them — so code that calls another plugin should still check that it is loaded.
from qgis.utils import plugins, iface
def require_plugin(name):
if name not in plugins:
iface.messageBar().pushWarning("Asset Sync",
f"This feature needs the '{name}' plugin. Install it from Plugins → Manage and Install Plugins.")
return None
return plugins[name]
Breakdown: qgis.utils.plugins is a dictionary of loaded plugin instances keyed by their package name. Checking it before calling into another plugin turns a missing dependency into a clear instruction rather than an AttributeError.
Check Python packages at load time
The plugin manager does not install Python packages. A plugin that imports requests, pandas or shapely at the top of its module crashes on load where they are missing. Checking first and failing gracefully keeps the plugin loadable.
import importlib
REQUIRED = {"requests": "requests", "shapely": "shapely>=2.0"}
def missing_packages():
missing = []
for module, spec in REQUIRED.items():
try:
importlib.import_module(module)
except ImportError:
missing.append(spec)
return missing
class AssetSyncPlugin:
def __init__(self, iface):
self.iface = iface
self.actions = []
def initGui(self):
from qgis.PyQt.QtWidgets import QAction, QMessageBox
missing = missing_packages()
if missing:
act = QAction("Asset Sync: install required packages…", self.iface.mainWindow())
act.triggered.connect(lambda: QMessageBox.information(
self.iface.mainWindow(), "Asset Sync",
"Asset Sync needs these Python packages:\n\n " + "\n ".join(missing) +
"\n\nInstall them into QGIS's Python (see the plugin's README), then restart QGIS."))
self.iface.addPluginToMenu("Asset Sync", act)
self.actions.append(act)
return
from .sync_tools import build_actions # imports requests/shapely inside
self.actions = build_actions(self.iface)
def unload(self):
for act in self.actions:
self.iface.removePluginMenu("Asset Sync", act)
Breakdown: Imports of third-party packages move out of the module's top level and into code that runs only when they are present, so the plugin module always imports. With packages missing, the plugin adds one menu entry that explains exactly what to install, rather than its normal tools; QGIS loads without errors and the user has a path forward. Installation instructions differ by platform; link to them from the message or the README, as covered in installing Python packages into QGIS. Bundling pure-Python packages inside the plugin is the alternative described in bundling third-party dependencies.
Keep optional features optional
Not every dependency is essential. An export to Excel needs openpyxl; the rest of the plugin does not. Treating such features as optional keeps the plugin useful everywhere.
def has(module):
try:
importlib.import_module(module)
return True
except ImportError:
return False
FEATURES = {"excel_export": has("openpyxl"), "charts": has("matplotlib")}
def add_export_actions(iface, menu):
from qgis.PyQt.QtWidgets import QAction
xlsx = QAction("Export to Excel", iface.mainWindow())
xlsx.setEnabled(FEATURES["excel_export"])
if not FEATURES["excel_export"]:
xlsx.setToolTip("Install 'openpyxl' into QGIS's Python to enable Excel export")
menu.addAction(xlsx)
return [xlsx]
Breakdown: A feature table computed once at load records which optional packages are available. Actions for unavailable features are shown disabled, with a tooltip saying what would enable them — users discover the capability and learn how to unlock it, instead of never knowing it exists. Keep the list of optional packages in the README and the plugin's "about" text.
Guard version-specific code paths
Supporting a range of QGIS versions sometimes means using a newer API where it exists and falling back elsewhere. Checking the running version — or better, the presence of the feature — keeps one codebase working across the range.
from qgis.core import Qgis
QGIS_INT = Qgis.QGIS_VERSION_INT # e.g. 34010 for 3.40.10
def set_label_format(item, fmt):
if hasattr(item, "setTextFormat"): # 3.24+
item.setTextFormat(fmt)
else:
item.setFont(fmt.font())
item.setFontColor(fmt.color())
NEW_ENUMS = QGIS_INT >= 33000 # scoped enums on Qgis.* from 3.30
Breakdown: Qgis.QGIS_VERSION_INT encodes major, minor and patch as one integer, convenient for comparisons such as "3.36 or newer". Feature detection with hasattr is often more robust than version numbers, because it tests exactly what the code needs and keeps working on releases you did not anticipate. Keep such branches in a small compatibility module rather than scattered through the plugin, and delete them when the minimum version moves past them.
Test the declared minimum
A minimum version that was never tested is a guess. Running the plugin's tests against the oldest QGIS it claims to support catches APIs added later.
# .github/workflows/test.yml (excerpt)
strategy:
matrix:
qgis: ["release-3_34", "release-3_40", "latest"]
container: qgis/qgis:${{ matrix.qgis }}
steps:
- uses: actions/checkout@v4
- run: xvfb-run -a python3 -m pytest tests/
Breakdown: The official QGIS container images are tagged per release line, so a test matrix over the declared minimum, the LTR and the latest release is a few lines of CI configuration. A failure only on the minimum points straight at a newer API; either avoid it or raise qgisMinimumVersion. The full CI setup is covered in running QGIS plugin tests in GitHub Actions.
QGIS version compatibility
qgisMinimumVersion, qgisMaximumVersion and plugin_dependencies are read by the plugin manager in QGIS 3.34 LTR, 3.40 LTR and QGIS 4. The default maximum version rule means plugins must explicitly declare QGIS 4 support to appear there. qgis.utils.plugins is available on all current releases.
Troubleshooting
- The plugin is invisible in the plugin manager. The running QGIS is outside the declared version range; check the maximum version.
- Users report an ImportError on load. A third-party package is imported at module top level; move the import behind a check.
- The dependency plugin was not installed. Users can decline; check
qgis.utils.pluginsbefore calling it. - A method does not exist on some machines. The minimum version is lower than the API requires; test on it.
Conclusion
Set qgisMinimumVersion to the oldest release you test and declare qgisMaximumVersion only when you support the next major version, list other plugins in plugin_dependencies and still check they are loaded, check Python packages at load time and explain what to install instead of crashing, keep optional features optional with disabled actions and tooltips, and test the declared minimum in CI.
Frequently Asked Questions
Can a plugin depend on a specific Python version?
Not in metadata; the QGIS version implies the Python version. Check sys.version_info if needed.
Should I pin dependency plugin versions? Only when an API you use changed between versions; pins block users from updates.
Does the plugin manager install pip packages? No. Some QGIS versions show a hint for missing packages, but installation remains the user's step.
What happens if two plugins need conflicting package versions? They share one Python environment, so only one version can be loaded — another reason to keep dependencies minimal.