Run Python at QGIS Startup in PyQGIS

Some setup should happen every time without anybody remembering to do it: register a custom expression function the whole team uses, add a proxy exception, set a default project CRS, connect a signal that stamps projects on save, open the company basemap. QGIS provides three hooks for running Python automatically — a startup.py file that runs when the application starts, an environment variable that runs a script even earlier, and project macros that run when a particular project opens, saves or closes.

This recipe belongs to QGIS Python Console Basics. It sets up each hook, explains when during start-up each one runs and what is available at that moment, and shows when to stop using hooks and write a small plugin instead.

When each hook runsA horizontal timeline of a QGIS session. First, the PYQGIS_STARTUP script runs before QGIS initialisation completes, so settings can be changed but no interface exists. Next, Python is initialised and startup.py in the QGIS settings folder runs, with iface available but plugins and the main window not fully ready. Then plugins load and the initializationCompleted signal fires. Later, when a project with macros is opened, openProject runs; on save, saveProject runs; on close, closeProject runs.Earlier hooks see less of QGISPYQGIS_STARTUPbefore initno interface yetstartup.pyPython readyUI still loadinginitializationCompletedplugins loadedUI fully readyproject macrosopenProject · saveProjectcloseProject

Prerequisites

  • QGIS 3.40 LTR or newer, or the QGIS 4 series.
  • Access to the QGIS settings folder on the machines concerned, or the ability to set environment variables for how QGIS is launched.
  • Code that you would otherwise paste into the Python console at the start of every session.

startup.py: run code whenever QGIS starts

When Python initialises, QGIS looks for a file named startup.py in the QGIS settings folder — the parent of the profiles directory — and runs it. Typical locations are ~/.local/share/QGIS/QGIS3/startup.py on Linux, %APPDATA%\QGIS\QGIS3\startup.py on Windows and ~/Library/Application Support/QGIS/QGIS3/startup.py on macOS. The QGIS 4 series uses its own settings folder name, so check the path it reports rather than assuming.

# startup.py
from qgis.core import QgsApplication, QgsExpression, QgsSettings, QgsMessageLog, Qgis
from qgis.utils import iface, qgsfunction


@qgsfunction(group="Company", referenced_columns=[])
def asset_url(asset_id, feature, parent):
    """Returns the asset system URL for an asset id."""
    return f"https://assets.example.gov/assets/{asset_id}"


settings = QgsSettings()
settings.setValue("Projections/defaultProjectCrs", "EPSG:27700")
settings.setValue("proxy/proxyExcludedUrls", "https://assets.example.gov|https://gis.example.gov")


def after_ui_ready():
    QgsMessageLog.logMessage("company startup.py applied", "Startup", Qgis.MessageLevel.Info)
    iface.messageBar().pushInfo("Startup", "Company settings loaded")


iface.initializationCompleted.connect(after_ui_ready)

Breakdown: Registering an expression function with @qgsfunction at start-up makes it available in every project and every expression dialog for the session, exactly as described in registering a custom expression function. Settings written through QgsSettings take effect for the current profile and persist, so this is also a way to enforce defaults across a team. startup.py runs before the interface is fully built and before plugins load, so anything that needs the main window, the canvas or a plugin should be deferred to initializationCompleted, which fires once when QGIS is ready. Logging to the message log leaves a trace that the script ran, which saves time when someone asks why a setting keeps coming back.

One startup.py, many profilesA folder tree. The QGIS3 settings folder contains startup.py and a profiles directory. profiles contains default and fieldwork, each with its own python/plugins folder and QGIS3.ini. startup.py runs for whichever profile is launched, so code in it should check the active profile name if behaviour must differ between profiles.startup.py sits above the profilesQGIS3/startup.pyprofiles/default/python/plugins/ · QGIS/QGIS3.inifieldwork/python/plugins/ · QGIS/QGIS3.iniruns for every profilecheck the active oneif behaviour differs:QgsApplication.instance().qgisSettingsDirPath()

Because the file sits above the profiles, it runs for every profile on the machine. When behaviour should differ — a field profile that skips the web basemap, say — check which profile is active: QgsApplication.qgisSettingsDirPath() returns the active profile's folder, whose last path component is the profile name. Keep the file short and defensive: wrap each block in its own try/except that logs the exception, so one broken line — a renamed setting, a missing module on a new laptop — does not stop everything after it from running.

PYQGIS_STARTUP: run code before QGIS initialises

The PYQGIS_STARTUP environment variable names a Python file that runs even earlier, before QGIS finishes initialising. It is the tool for adjusting the Python environment itself — extending sys.path to a shared library folder, setting environment variables that libraries read at import time — not for anything that touches QGIS objects.

# /srv/gis/pyqgis_startup.py, launched with PYQGIS_STARTUP=/srv/gis/pyqgis_startup.py
import os
import sys

SHARED = "/srv/gis/python-lib"
if os.path.isdir(SHARED) and SHARED not in sys.path:
    sys.path.insert(0, SHARED)

os.environ.setdefault("PROJ_NETWORK", "ON")
os.environ.setdefault("GDAL_HTTP_TIMEOUT", "30")

Breakdown: Putting a shared folder on sys.path this early means every plugin and every console session can import the organisation's helper modules without installing them into QGIS's own Python, which pairs well with the approaches in installing Python packages into the QGIS environment. GDAL and PROJ read environment variables when they are first used, so setting them here, before any layer loads, reliably takes effect. Set the variable itself in the shortcut or launcher script that starts QGIS, so it applies to QGIS and nothing else on the machine.

Project macros: run code when a project opens

Project macros belong to one project file rather than the application. In Project → Properties → Macros you define up to three functions — openProject, saveProject and closeProject — and QGIS calls them at those moments. They can also be written from Python.

Macros only run when the user allows themA project containing macros is opened. The Enable macros setting decides what happens: Never blocks them, Ask shows a message bar with an option to enable them for this project, For this session only enables them until QGIS closes, and Always runs them without asking. Because many organisations set Never or Ask, macros should only add convenience, not enforce rules.Whether macros run is the user's decisionNevermacros ignoredAskmessage bar promptper projectThis sessionallowed untilQGIS closesAlwaysruns silentlydesign macros as convenience, never as enforcement

from qgis.core import QgsProject

MACROS = '''
from qgis.core import QgsProject, QgsExpressionContextUtils
from qgis.utils import iface
from datetime import datetime, timezone

def openProject():
    iface.messageBar().pushInfo("Flood model", "Remember to refresh the gauge layer")

def saveProject():
    project = QgsProject.instance()
    QgsExpressionContextUtils.setProjectVariable(
        project, "last_saved_utc", datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M"))
    QgsExpressionContextUtils.setProjectVariable(
        project, "last_saved_by", QgsExpressionContextUtils.globalScope().variable("user_full_name"))

def closeProject():
    pass
'''

project = QgsProject.instance()
project.writeEntry("Macros", "/pythonCode", MACROS)
project.write()

Breakdown: Macros are stored in the project under the Macros/pythonCode entry, so writing that entry and saving the project is the scripted equivalent of the dialog. The save macro stamps two project variables, which layouts can show in a footer with @last_saved_utc — the idea behind using project variables and metadata. Whether macros run at all depends on each user's Enable macros setting: never, ask, for the session, or always. Many organisations set it to never or ask, because a macro is code that runs on the machine of whoever opens a project. That makes macros suitable for conveniences like a reminder or a stamp, and unsuitable for anything that must happen.

When to write a plugin instead

Hooks are the right tool for a handful of lines. They become the wrong tool when the code grows, when several people depend on it, or when it needs a user interface. A startup script is invisible — nobody knows it exists until it breaks — and it cannot be updated centrally, disabled from the Plugin Manager, or tested.

The step up is small. A plugin with an empty initGui that registers the same expression functions and connects the same signals is about thirty lines of boilerplate, can be distributed from a private plugin repository, updated for everyone at once, and switched off by a user who needs to rule it out while troubleshooting. Creating a QGIS plugin with Plugin Builder produces that skeleton in a few minutes. A reasonable rule: if a startup script needs a second file, a configuration setting, or a colleague to install it, it has become a plugin.

QGIS version compatibility

startup.py and PYQGIS_STARTUP have been supported throughout QGIS 3.x and continue in the QGIS 4 series, though the settings folder name differs there. iface.initializationCompleted has existed since 3.0. The macro security setting gained finer options in the 3.x releases; recent versions also apply project trust rules to other embedded code such as form init functions. Qgis.MessageLevel.Info is the scoped spelling required on QGIS 4.

Troubleshooting

  • startup.py does not run. It is inside a profile folder instead of the settings folder above profiles, or it raised an exception — check the Python tab of the message log.
  • iface is None or the main window is missing. The code ran too early; move it into initializationCompleted.
  • Settings keep reverting after users change them. startup.py writes them on every start; write only when unset if users may override.
  • Macros never run. The user's macro setting is never or ask; check Settings → Options → General.
  • PYQGIS_STARTUP has no effect. The variable is set for a different process than the one that launches QGIS.

Conclusion

Use PYQGIS_STARTUP to adjust the Python environment before QGIS loads, startup.py for application-wide setup with interface work deferred to initializationCompleted, and project macros for small conveniences tied to one project. Log what each hook does, remember that macros depend on the user's security settings, and turn a hook into a plugin as soon as it grows beyond a few lines.

Frequently Asked Questions

Can startup.py load a plugin? It can enable one through qgis.utils.loadPlugin and startPlugin, but it is cleaner to enable plugins through settings or the Plugin Manager.

Does startup.py run in qgis_process or standalone scripts? No. It is part of QGIS Desktop start-up; standalone scripts run their own initialisation code.

Can macros use code from a plugin? Yes, if the plugin is installed and enabled — import its module in the macro. Guard the import, since the plugin may be missing on another machine.

How do I distribute startup.py to a whole team? Through the same deployment mechanism that installs QGIS, or by setting PYQGIS_STARTUP to a shared file. For anything substantial, a plugin in a private repository is easier to manage.