[{"data":1,"prerenderedAt":1218},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002Frun-python-at-qgis-startup-pyqgis":3},{"id":4,"title":5,"body":6,"description":1207,"extension":1208,"meta":1209,"navigation":275,"path":1214,"seo":1215,"stem":1216,"__hash__":1217},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002Frun-python-at-qgis-startup-pyqgis\u002Findex.md","Run Python at QGIS Startup in PyQGIS",{"type":7,"value":8,"toc":1195},"minimark",[9,13,22,31,175,180,197,201,223,479,503,584,599,603,613,726,739,743,761,832,989,1011,1015,1018,1030,1034,1049,1053,1104,1108,1120,1124,1137,1143,1149,1158,1162,1191],[10,11,5],"h1",{"id":12},"run-python-at-qgis-startup-in-pyqgis",[14,15,16,17,21],"p",{},"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 ",[18,19,20],"code",{},"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.",[14,23,24,25,30],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002F","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.",[14,32,33],{},[34,35,40,44,48,55,72,81,89,95,99,103,107,115,120,125,129,134,137,141,145,150,153,156,159,163,168,172],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 300","img","The QGIS start-up timeline and where each hook runs: PYQGIS_STARTUP before QGIS finishes initialising, startup.py once Python is ready, initializationCompleted when the interface is fully loaded, and project macros each time a project with macros opens, saves or closes","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"When each hook runs",[45,46,47],"desc",{},"A 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.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","300","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"stTimeArrow","0 0 10 10","8","5","7","auto-start-reverse",[68,69],"path",{"d":70,"fill":71},"M0 0 L10 5 L0 10 z","#2f3b35",[73,74,80],"text",{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Earlier hooks see less of QGIS",[82,83],"line",{"x1":84,"y1":85,"x2":86,"y2":85,"stroke":87,"style":88},"30","150","730","#9aa39d","stroke-width:4;marker-end:url(#stTimeArrow)",[90,91],"circle",{"cx":92,"cy":85,"r":93,"fill":94},"90","10","#b45309",[90,96],{"cx":97,"cy":85,"r":93,"fill":98},"240","#0f766e",[90,100],{"cx":101,"cy":85,"r":93,"fill":102},"400","#2563eb",[90,104],{"cx":105,"cy":85,"r":93,"fill":106},"600","#15803d",[49,108],{"x":109,"y":110,"width":111,"height":112,"rx":93,"fill":113,"stroke":94,"style":114},"20","52","140","76","#fdf2e2","stroke-width:2",[73,116,119],{"x":92,"y":117,"style":118,"fill":94,"textAnchor":79},"78","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","PYQGIS_STARTUP",[73,121,124],{"x":92,"y":122,"style":123,"fill":71,"textAnchor":79},"100","text-anchor:middle;font-size:9.5px;font-family:sans-serif","before init",[73,126,128],{"x":92,"y":127,"style":123,"fill":71,"textAnchor":79},"116","no interface yet",[49,130],{"x":131,"y":132,"width":111,"height":112,"rx":93,"fill":133,"stroke":98,"style":114},"170","172","#eef7f4",[73,135,20],{"x":97,"y":136,"style":118,"fill":98,"textAnchor":79},"198",[73,138,140],{"x":97,"y":139,"style":123,"fill":71,"textAnchor":79},"220","Python ready",[73,142,144],{"x":97,"y":143,"style":123,"fill":71,"textAnchor":79},"236","UI still loading",[49,146],{"x":147,"y":110,"width":148,"height":112,"rx":93,"fill":149,"stroke":102,"style":114},"320","160","#eff3ff",[73,151,152],{"x":101,"y":117,"style":118,"fill":102,"textAnchor":79},"initializationCompleted",[73,154,155],{"x":101,"y":122,"style":123,"fill":71,"textAnchor":79},"plugins loaded",[73,157,158],{"x":101,"y":127,"style":123,"fill":71,"textAnchor":79},"UI fully ready",[49,160],{"x":161,"y":132,"width":139,"height":112,"rx":93,"fill":162,"stroke":106,"style":114},"510","#e8efe6",[73,164,167],{"x":165,"y":136,"style":118,"fill":166,"textAnchor":79},"620","#166534","project macros",[73,169,171],{"x":165,"y":139,"style":170,"fill":71,"textAnchor":79},"text-anchor:middle;font-size:9.5px;font-family:monospace","openProject · saveProject",[73,173,174],{"x":165,"y":143,"style":170,"fill":71,"textAnchor":79},"closeProject",[176,177,179],"h2",{"id":178},"prerequisites","Prerequisites",[181,182,183,191,194],"ul",{},[184,185,186,190],"li",{},[187,188,189],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series.",[184,192,193],{},"Access to the QGIS settings folder on the machines concerned, or the ability to set environment variables for how QGIS is launched.",[184,195,196],{},"Code that you would otherwise paste into the Python console at the start of every session.",[176,198,200],{"id":199},"startuppy-run-code-whenever-qgis-starts","startup.py: run code whenever QGIS starts",[14,202,203,204,206,207,210,211,214,215,218,219,222],{},"When Python initialises, QGIS looks for a file named ",[18,205,20],{}," in the QGIS settings folder — the parent of the ",[18,208,209],{},"profiles"," directory — and runs it. Typical locations are ",[18,212,213],{},"~\u002F.local\u002Fshare\u002FQGIS\u002FQGIS3\u002Fstartup.py"," on Linux, ",[18,216,217],{},"%APPDATA%\\QGIS\\QGIS3\\startup.py"," on Windows and ",[18,220,221],{},"~\u002FLibrary\u002FApplication Support\u002FQGIS\u002FQGIS3\u002Fstartup.py"," on macOS. The QGIS 4 series uses its own settings folder name, so check the path it reports rather than assuming.",[224,225,230],"pre",{"className":226,"code":227,"language":228,"meta":229,"style":229},"language-python shiki shiki-themes github-dark","# startup.py\nfrom qgis.core import QgsApplication, QgsExpression, QgsSettings, QgsMessageLog, Qgis\nfrom qgis.utils import iface, qgsfunction\n\n\n@qgsfunction(group=\"Company\", referenced_columns=[])\ndef asset_url(asset_id, feature, parent):\n    \"\"\"Returns the asset system URL for an asset id.\"\"\"\n    return f\"https:\u002F\u002Fassets.example.gov\u002Fassets\u002F{asset_id}\"\n\n\nsettings = QgsSettings()\nsettings.setValue(\"Projections\u002FdefaultProjectCrs\", \"EPSG:27700\")\nsettings.setValue(\"proxy\u002FproxyExcludedUrls\", \"https:\u002F\u002Fassets.example.gov|https:\u002F\u002Fgis.example.gov\")\n\n\ndef after_ui_ready():\n    QgsMessageLog.logMessage(\"company startup.py applied\", \"Startup\", Qgis.MessageLevel.Info)\n    iface.messageBar().pushInfo(\"Startup\", \"Company settings loaded\")\n\n\niface.initializationCompleted.connect(after_ui_ready)\n","python","",[18,231,232,240,257,270,277,282,314,326,332,357,362,367,378,395,410,415,420,431,448,463,468,473],{"__ignoreMap":229},[233,234,236],"span",{"class":82,"line":235},1,[233,237,239],{"class":238},"sjoCn","# startup.py\n",[233,241,243,247,251,254],{"class":82,"line":242},2,[233,244,246],{"class":245},"snl16","from",[233,248,250],{"class":249},"s95oV"," qgis.core ",[233,252,253],{"class":245},"import",[233,255,256],{"class":249}," QgsApplication, QgsExpression, QgsSettings, QgsMessageLog, Qgis\n",[233,258,260,262,265,267],{"class":82,"line":259},3,[233,261,246],{"class":245},[233,263,264],{"class":249}," qgis.utils ",[233,266,253],{"class":245},[233,268,269],{"class":249}," iface, qgsfunction\n",[233,271,273],{"class":82,"line":272},4,[233,274,276],{"emptyLinePlaceholder":275},true,"\n",[233,278,280],{"class":82,"line":279},5,[233,281,276],{"emptyLinePlaceholder":275},[233,283,285,289,292,296,299,303,306,309,311],{"class":82,"line":284},6,[233,286,288],{"class":287},"svObZ","@qgsfunction",[233,290,291],{"class":249},"(",[233,293,295],{"class":294},"s9osk","group",[233,297,298],{"class":245},"=",[233,300,302],{"class":301},"sU2Wk","\"Company\"",[233,304,305],{"class":249},", ",[233,307,308],{"class":294},"referenced_columns",[233,310,298],{"class":245},[233,312,313],{"class":249},"[])\n",[233,315,317,320,323],{"class":82,"line":316},7,[233,318,319],{"class":245},"def",[233,321,322],{"class":287}," asset_url",[233,324,325],{"class":249},"(asset_id, feature, parent):\n",[233,327,329],{"class":82,"line":328},8,[233,330,331],{"class":301},"    \"\"\"Returns the asset system URL for an asset id.\"\"\"\n",[233,333,335,338,341,344,348,351,354],{"class":82,"line":334},9,[233,336,337],{"class":245},"    return",[233,339,340],{"class":245}," f",[233,342,343],{"class":301},"\"https:\u002F\u002Fassets.example.gov\u002Fassets\u002F",[233,345,347],{"class":346},"sDLfK","{",[233,349,350],{"class":249},"asset_id",[233,352,353],{"class":346},"}",[233,355,356],{"class":301},"\"\n",[233,358,360],{"class":82,"line":359},10,[233,361,276],{"emptyLinePlaceholder":275},[233,363,365],{"class":82,"line":364},11,[233,366,276],{"emptyLinePlaceholder":275},[233,368,370,373,375],{"class":82,"line":369},12,[233,371,372],{"class":249},"settings ",[233,374,298],{"class":245},[233,376,377],{"class":249}," QgsSettings()\n",[233,379,381,384,387,389,392],{"class":82,"line":380},13,[233,382,383],{"class":249},"settings.setValue(",[233,385,386],{"class":301},"\"Projections\u002FdefaultProjectCrs\"",[233,388,305],{"class":249},[233,390,391],{"class":301},"\"EPSG:27700\"",[233,393,394],{"class":249},")\n",[233,396,398,400,403,405,408],{"class":82,"line":397},14,[233,399,383],{"class":249},[233,401,402],{"class":301},"\"proxy\u002FproxyExcludedUrls\"",[233,404,305],{"class":249},[233,406,407],{"class":301},"\"https:\u002F\u002Fassets.example.gov|https:\u002F\u002Fgis.example.gov\"",[233,409,394],{"class":249},[233,411,413],{"class":82,"line":412},15,[233,414,276],{"emptyLinePlaceholder":275},[233,416,418],{"class":82,"line":417},16,[233,419,276],{"emptyLinePlaceholder":275},[233,421,423,425,428],{"class":82,"line":422},17,[233,424,319],{"class":245},[233,426,427],{"class":287}," after_ui_ready",[233,429,430],{"class":249},"():\n",[233,432,434,437,440,442,445],{"class":82,"line":433},18,[233,435,436],{"class":249},"    QgsMessageLog.logMessage(",[233,438,439],{"class":301},"\"company startup.py applied\"",[233,441,305],{"class":249},[233,443,444],{"class":301},"\"Startup\"",[233,446,447],{"class":249},", Qgis.MessageLevel.Info)\n",[233,449,451,454,456,458,461],{"class":82,"line":450},19,[233,452,453],{"class":249},"    iface.messageBar().pushInfo(",[233,455,444],{"class":301},[233,457,305],{"class":249},[233,459,460],{"class":301},"\"Company settings loaded\"",[233,462,394],{"class":249},[233,464,466],{"class":82,"line":465},20,[233,467,276],{"emptyLinePlaceholder":275},[233,469,471],{"class":82,"line":470},21,[233,472,276],{"emptyLinePlaceholder":275},[233,474,476],{"class":82,"line":475},22,[233,477,478],{"class":249},"iface.initializationCompleted.connect(after_ui_ready)\n",[14,480,481,484,485,487,488,492,493,496,497,499,500,502],{},[187,482,483],{},"Breakdown:"," Registering an expression function with ",[18,486,288],{}," at start-up makes it available in every project and every expression dialog for the session, exactly as described in ",[26,489,491],{"href":490},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fregister-custom-expression-function-pyqgis\u002F","registering a custom expression function",". Settings written through ",[18,494,495],{},"QgsSettings"," take effect for the current profile and persist, so this is also a way to enforce defaults across a team. ",[18,498,20],{}," 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 ",[18,501,152],{},", 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.",[14,504,505],{},[34,506,509,512,515,518,522,530,536,540,544,548,553,556,558,562,567,572,576,580],{"viewBox":507,"role":37,"ariaLabel":508,"xmlns":39},"0 0 760 256","Where startup.py lives relative to profiles: one startup.py in the QGIS settings folder applies to every profile, while per-profile Python folders hold plugins and are separate for each profile",[41,510,511],{},"One startup.py, many profiles",[45,513,514],{},"A folder tree. The QGIS3 settings folder contains startup.py and a profiles directory. profiles contains default and fieldwork, each with its own python\u002Fplugins 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.",[49,516],{"x":51,"y":51,"width":52,"height":517,"fill":54},"256",[73,519,521],{"x":75,"y":520,"style":77,"fill":78,"textAnchor":79},"26","startup.py sits above the profiles",[49,523],{"x":524,"y":525,"width":526,"height":527,"rx":93,"fill":528,"stroke":529,"style":114},"24","44","420","192","#fffdf7","#59645f",[73,531,535],{"x":532,"y":533,"style":534,"fill":78},"48","74","font-size:10.5px;font-family:monospace","QGIS3\u002F",[73,537,20],{"x":538,"y":122,"style":539,"fill":98},"72","font-size:10.5px;font-weight:bold;font-family:monospace",[73,541,543],{"x":538,"y":542,"style":534,"fill":71},"126","profiles\u002F",[73,545,547],{"x":546,"y":85,"style":534,"fill":71},"96","default\u002F",[73,549,552],{"x":550,"y":132,"style":551,"fill":529},"120","font-size:10px;font-family:monospace","python\u002Fplugins\u002F · QGIS\u002FQGIS3.ini",[73,554,555],{"x":546,"y":136,"style":534,"fill":71},"fieldwork\u002F",[73,557,552],{"x":550,"y":139,"style":551,"fill":529},[49,559],{"x":560,"y":525,"width":561,"height":527,"rx":93,"fill":133,"stroke":98,"style":114},"468","268",[73,563,566],{"x":564,"y":538,"style":565,"fill":98,"textAnchor":79},"602","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","runs for every profile",[73,568,571],{"x":564,"y":569,"style":570,"fill":71,"textAnchor":79},"104","text-anchor:middle;font-size:10px;font-family:sans-serif","check the active one",[73,573,575],{"x":564,"y":574,"style":570,"fill":71,"textAnchor":79},"124","if behaviour differs:",[73,577,579],{"x":564,"y":578,"style":170,"fill":71,"textAnchor":79},"156","QgsApplication.instance()",[73,581,583],{"x":564,"y":582,"style":170,"fill":71,"textAnchor":79},"174",".qgisSettingsDirPath()",[14,585,586,587,590,591,594,595,598],{},"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: ",[18,588,589],{},"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 ",[18,592,593],{},"try","\u002F",[18,596,597],{},"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.",[176,600,602],{"id":601},"pyqgis_startup-run-code-before-qgis-initialises","PYQGIS_STARTUP: run code before QGIS initialises",[14,604,605,606,608,609,612],{},"The ",[18,607,119],{}," 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 ",[18,610,611],{},"sys.path"," to a shared library folder, setting environment variables that libraries read at import time — not for anything that touches QGIS objects.",[224,614,616],{"className":226,"code":615,"language":228,"meta":229,"style":229},"# \u002Fsrv\u002Fgis\u002Fpyqgis_startup.py, launched with PYQGIS_STARTUP=\u002Fsrv\u002Fgis\u002Fpyqgis_startup.py\nimport os\nimport sys\n\nSHARED = \"\u002Fsrv\u002Fgis\u002Fpython-lib\"\nif os.path.isdir(SHARED) and SHARED not in sys.path:\n    sys.path.insert(0, SHARED)\n\nos.environ.setdefault(\"PROJ_NETWORK\", \"ON\")\nos.environ.setdefault(\"GDAL_HTTP_TIMEOUT\", \"30\")\n",[18,617,618,623,630,637,641,652,680,693,697,712],{"__ignoreMap":229},[233,619,620],{"class":82,"line":235},[233,621,622],{"class":238},"# \u002Fsrv\u002Fgis\u002Fpyqgis_startup.py, launched with PYQGIS_STARTUP=\u002Fsrv\u002Fgis\u002Fpyqgis_startup.py\n",[233,624,625,627],{"class":82,"line":242},[233,626,253],{"class":245},[233,628,629],{"class":249}," os\n",[233,631,632,634],{"class":82,"line":259},[233,633,253],{"class":245},[233,635,636],{"class":249}," sys\n",[233,638,639],{"class":82,"line":272},[233,640,276],{"emptyLinePlaceholder":275},[233,642,643,646,649],{"class":82,"line":279},[233,644,645],{"class":346},"SHARED",[233,647,648],{"class":245}," =",[233,650,651],{"class":301}," \"\u002Fsrv\u002Fgis\u002Fpython-lib\"\n",[233,653,654,657,660,662,665,668,671,674,677],{"class":82,"line":284},[233,655,656],{"class":245},"if",[233,658,659],{"class":249}," os.path.isdir(",[233,661,645],{"class":346},[233,663,664],{"class":249},") ",[233,666,667],{"class":245},"and",[233,669,670],{"class":346}," SHARED",[233,672,673],{"class":245}," not",[233,675,676],{"class":245}," in",[233,678,679],{"class":249}," sys.path:\n",[233,681,682,685,687,689,691],{"class":82,"line":316},[233,683,684],{"class":249},"    sys.path.insert(",[233,686,51],{"class":346},[233,688,305],{"class":249},[233,690,645],{"class":346},[233,692,394],{"class":249},[233,694,695],{"class":82,"line":328},[233,696,276],{"emptyLinePlaceholder":275},[233,698,699,702,705,707,710],{"class":82,"line":334},[233,700,701],{"class":249},"os.environ.setdefault(",[233,703,704],{"class":301},"\"PROJ_NETWORK\"",[233,706,305],{"class":249},[233,708,709],{"class":301},"\"ON\"",[233,711,394],{"class":249},[233,713,714,716,719,721,724],{"class":82,"line":359},[233,715,701],{"class":249},[233,717,718],{"class":301},"\"GDAL_HTTP_TIMEOUT\"",[233,720,305],{"class":249},[233,722,723],{"class":301},"\"30\"",[233,725,394],{"class":249},[14,727,728,730,731,733,734,738],{},[187,729,483],{}," Putting a shared folder on ",[18,732,611],{}," 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 ",[26,735,737],{"href":736},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Finstall-python-packages-into-qgis\u002F","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.",[176,740,742],{"id":741},"project-macros-run-code-when-a-project-opens","Project macros: run code when a project opens",[14,744,745,746,750,751,305,754,757,758,760],{},"Project macros belong to one project file rather than the application. In ",[747,748,749],"em",{},"Project → Properties → Macros"," you define up to three functions — ",[18,752,753],{},"openProject",[18,755,756],{},"saveProject"," and ",[18,759,174],{}," — and QGIS calls them at those moments. They can also be written from Python.",[14,762,763],{},[34,764,767,770,773,775,778,782,786,790,793,797,800,804,807,811,814,817,820,824,827],{"viewBox":765,"role":37,"ariaLabel":766,"xmlns":39},"0 0 760 236","Macro trust settings: QGIS asks, allows, or blocks project macros depending on the user's security setting, so macros must not be the only place important behaviour lives",[41,768,769],{},"Macros only run when the user allows them",[45,771,772],{},"A 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.",[49,774],{"x":51,"y":51,"width":52,"height":143,"fill":54},[73,776,777],{"x":75,"y":520,"style":77,"fill":78,"textAnchor":79},"Whether macros run is the user's decision",[49,779],{"x":524,"y":532,"width":780,"height":85,"rx":93,"fill":113,"stroke":781,"style":114},"166","#b91c1c",[73,783,785],{"x":784,"y":112,"style":565,"fill":781,"textAnchor":79},"107","Never",[73,787,789],{"x":784,"y":788,"style":570,"fill":71,"textAnchor":79},"112","macros ignored",[49,791],{"x":792,"y":532,"width":780,"height":85,"rx":93,"fill":113,"stroke":94,"style":114},"206",[73,794,796],{"x":795,"y":112,"style":565,"fill":94,"textAnchor":79},"289","Ask",[73,798,799],{"x":795,"y":788,"style":570,"fill":71,"textAnchor":79},"message bar prompt",[73,801,803],{"x":795,"y":802,"style":570,"fill":71,"textAnchor":79},"132","per project",[49,805],{"x":806,"y":532,"width":780,"height":85,"rx":93,"fill":149,"stroke":102,"style":114},"388",[73,808,810],{"x":809,"y":112,"style":565,"fill":102,"textAnchor":79},"471","This session",[73,812,813],{"x":809,"y":788,"style":570,"fill":71,"textAnchor":79},"allowed until",[73,815,816],{"x":809,"y":802,"style":570,"fill":71,"textAnchor":79},"QGIS closes",[49,818],{"x":819,"y":532,"width":780,"height":85,"rx":93,"fill":162,"stroke":106,"style":114},"570",[73,821,823],{"x":822,"y":112,"style":565,"fill":166,"textAnchor":79},"653","Always",[73,825,826],{"x":822,"y":788,"style":570,"fill":71,"textAnchor":79},"runs silently",[73,828,831],{"x":75,"y":829,"style":830,"fill":529,"textAnchor":79},"222","text-anchor:middle;font-size:10.5px;font-family:sans-serif","design macros as convenience, never as enforcement",[224,833,835],{"className":226,"code":834,"language":228,"meta":229,"style":229},"from qgis.core import QgsProject\n\nMACROS = '''\nfrom qgis.core import QgsProject, QgsExpressionContextUtils\nfrom qgis.utils import iface\nfrom datetime import datetime, timezone\n\ndef openProject():\n    iface.messageBar().pushInfo(\"Flood model\", \"Remember to refresh the gauge layer\")\n\ndef saveProject():\n    project = QgsProject.instance()\n    QgsExpressionContextUtils.setProjectVariable(\n        project, \"last_saved_utc\", datetime.now(timezone.utc).strftime(\"%Y-%m-%d %H:%M\"))\n    QgsExpressionContextUtils.setProjectVariable(\n        project, \"last_saved_by\", QgsExpressionContextUtils.globalScope().variable(\"user_full_name\"))\n\ndef closeProject():\n    pass\n'''\n\nproject = QgsProject.instance()\nproject.writeEntry(\"Macros\", \"\u002FpythonCode\", MACROS)\nproject.write()\n",[18,836,837,848,852,862,867,872,877,881,886,891,895,900,905,910,921,925,930,934,939,944,949,953,963,983],{"__ignoreMap":229},[233,838,839,841,843,845],{"class":82,"line":235},[233,840,246],{"class":245},[233,842,250],{"class":249},[233,844,253],{"class":245},[233,846,847],{"class":249}," QgsProject\n",[233,849,850],{"class":82,"line":242},[233,851,276],{"emptyLinePlaceholder":275},[233,853,854,857,859],{"class":82,"line":259},[233,855,856],{"class":346},"MACROS",[233,858,648],{"class":245},[233,860,861],{"class":301}," '''\n",[233,863,864],{"class":82,"line":272},[233,865,866],{"class":301},"from qgis.core import QgsProject, QgsExpressionContextUtils\n",[233,868,869],{"class":82,"line":279},[233,870,871],{"class":301},"from qgis.utils import iface\n",[233,873,874],{"class":82,"line":284},[233,875,876],{"class":301},"from datetime import datetime, timezone\n",[233,878,879],{"class":82,"line":316},[233,880,276],{"emptyLinePlaceholder":275},[233,882,883],{"class":82,"line":328},[233,884,885],{"class":301},"def openProject():\n",[233,887,888],{"class":82,"line":334},[233,889,890],{"class":301},"    iface.messageBar().pushInfo(\"Flood model\", \"Remember to refresh the gauge layer\")\n",[233,892,893],{"class":82,"line":359},[233,894,276],{"emptyLinePlaceholder":275},[233,896,897],{"class":82,"line":364},[233,898,899],{"class":301},"def saveProject():\n",[233,901,902],{"class":82,"line":369},[233,903,904],{"class":301},"    project = QgsProject.instance()\n",[233,906,907],{"class":82,"line":380},[233,908,909],{"class":301},"    QgsExpressionContextUtils.setProjectVariable(\n",[233,911,912,915,918],{"class":82,"line":397},[233,913,914],{"class":301},"        project, \"last_saved_utc\", datetime.now(timezone.utc).strftime(\"%Y-%m-",[233,916,917],{"class":346},"%d",[233,919,920],{"class":301}," %H:%M\"))\n",[233,922,923],{"class":82,"line":412},[233,924,909],{"class":301},[233,926,927],{"class":82,"line":417},[233,928,929],{"class":301},"        project, \"last_saved_by\", QgsExpressionContextUtils.globalScope().variable(\"user_full_name\"))\n",[233,931,932],{"class":82,"line":422},[233,933,276],{"emptyLinePlaceholder":275},[233,935,936],{"class":82,"line":433},[233,937,938],{"class":301},"def closeProject():\n",[233,940,941],{"class":82,"line":450},[233,942,943],{"class":301},"    pass\n",[233,945,946],{"class":82,"line":465},[233,947,948],{"class":301},"'''\n",[233,950,951],{"class":82,"line":470},[233,952,276],{"emptyLinePlaceholder":275},[233,954,955,958,960],{"class":82,"line":475},[233,956,957],{"class":249},"project ",[233,959,298],{"class":245},[233,961,962],{"class":249}," QgsProject.instance()\n",[233,964,966,969,972,974,977,979,981],{"class":82,"line":965},23,[233,967,968],{"class":249},"project.writeEntry(",[233,970,971],{"class":301},"\"Macros\"",[233,973,305],{"class":249},[233,975,976],{"class":301},"\"\u002FpythonCode\"",[233,978,305],{"class":249},[233,980,856],{"class":346},[233,982,394],{"class":249},[233,984,986],{"class":82,"line":985},24,[233,987,988],{"class":249},"project.write()\n",[14,990,991,993,994,997,998,1001,1002,1006,1007,1010],{},[187,992,483],{}," Macros are stored in the project under the ",[18,995,996],{},"Macros\u002FpythonCode"," 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 ",[18,999,1000],{},"@last_saved_utc"," — the idea behind ",[26,1003,1005],{"href":1004},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fuse-project-variables-and-metadata-pyqgis\u002F","using project variables and metadata",". Whether macros run at all depends on each user's ",[747,1008,1009],{},"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.",[176,1012,1014],{"id":1013},"when-to-write-a-plugin-instead","When to write a plugin instead",[14,1016,1017],{},"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.",[14,1019,1020,1021,1024,1025,1029],{},"The step up is small. A plugin with an empty ",[18,1022,1023],{},"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. ",[26,1026,1028],{"href":1027},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fcreate-qgis-plugin-with-plugin-builder\u002F","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.",[176,1031,1033],{"id":1032},"qgis-version-compatibility","QGIS version compatibility",[14,1035,1036,757,1038,1040,1041,1044,1045,1048],{},[18,1037,20],{},[18,1039,119],{}," have been supported throughout QGIS 3.x and continue in the QGIS 4 series, though the settings folder name differs there. ",[18,1042,1043],{},"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. ",[18,1046,1047],{},"Qgis.MessageLevel.Info"," is the scoped spelling required on QGIS 4.",[176,1050,1052],{"id":1051},"troubleshooting","Troubleshooting",[181,1054,1055,1068,1080,1089,1098],{},[184,1056,1057,1060,1061,1063,1064,1067],{},[187,1058,1059],{},"startup.py does not run."," It is inside a profile folder instead of the settings folder above ",[18,1062,209],{},", or it raised an exception — check the ",[747,1065,1066],{},"Python"," tab of the message log.",[184,1069,1070,1076,1077,1079],{},[187,1071,1072,1075],{},[18,1073,1074],{},"iface"," is None or the main window is missing."," The code ran too early; move it into ",[18,1078,152],{},".",[184,1081,1082,1085,1086,1088],{},[187,1083,1084],{},"Settings keep reverting after users change them."," ",[18,1087,20],{}," writes them on every start; write only when unset if users may override.",[184,1090,1091,1094,1095,1079],{},[187,1092,1093],{},"Macros never run."," The user's macro setting is never or ask; check ",[747,1096,1097],{},"Settings → Options → General",[184,1099,1100,1103],{},[187,1101,1102],{},"PYQGIS_STARTUP has no effect."," The variable is set for a different process than the one that launches QGIS.",[176,1105,1107],{"id":1106},"conclusion","Conclusion",[14,1109,1110,1111,1113,1114,1116,1117,1119],{},"Use ",[18,1112,119],{}," to adjust the Python environment before QGIS loads, ",[18,1115,20],{}," for application-wide setup with interface work deferred to ",[18,1118,152],{},", 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.",[176,1121,1123],{"id":1122},"frequently-asked-questions","Frequently Asked Questions",[14,1125,1126,1129,1130,757,1133,1136],{},[187,1127,1128],{},"Can startup.py load a plugin?","\nIt can enable one through ",[18,1131,1132],{},"qgis.utils.loadPlugin",[18,1134,1135],{},"startPlugin",", but it is cleaner to enable plugins through settings or the Plugin Manager.",[14,1138,1139,1142],{},[187,1140,1141],{},"Does startup.py run in qgis_process or standalone scripts?","\nNo. It is part of QGIS Desktop start-up; standalone scripts run their own initialisation code.",[14,1144,1145,1148],{},[187,1146,1147],{},"Can macros use code from a plugin?","\nYes, 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.",[14,1150,1151,1154,1155,1157],{},[187,1152,1153],{},"How do I distribute startup.py to a whole team?","\nThrough the same deployment mechanism that installs QGIS, or by setting ",[18,1156,119],{}," to a shared file. For anything substantial, a plugin in a private repository is easier to manage.",[176,1159,1161],{"id":1160},"related","Related",[181,1163,1164,1169,1175,1181,1186],{},[184,1165,1166,1168],{},[26,1167,29],{"href":28}," — the guide this recipe belongs to",[184,1170,1171],{},[26,1172,1174],{"href":1173},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002Frun-and-save-scripts-in-qgis-python-editor\u002F","Run and Save Scripts in the QGIS Python Editor",[184,1176,1177],{},[26,1178,1180],{"href":1179},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002Fuse-iface-to-control-qgis-interface-pyqgis\u002F","Use iface to Control the QGIS Interface in PyQGIS",[184,1182,1183],{},[26,1184,1185],{"href":490},"Register a Custom Expression Function in PyQGIS",[184,1187,1188],{},[26,1189,1190],{"href":1004},"Use Project Variables and Metadata in PyQGIS",[1192,1193,1194],"style",{},"html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":229,"searchDepth":242,"depth":242,"links":1196},[1197,1198,1199,1200,1201,1202,1203,1204,1205,1206],{"id":178,"depth":242,"text":179},{"id":199,"depth":242,"text":200},{"id":601,"depth":242,"text":602},{"id":741,"depth":242,"text":742},{"id":1013,"depth":242,"text":1014},{"id":1032,"depth":242,"text":1033},{"id":1051,"depth":242,"text":1052},{"id":1106,"depth":242,"text":1107},{"id":1122,"depth":242,"text":1123},{"id":1160,"depth":242,"text":1161},"Run your own Python every time QGIS starts or a project opens — the startup.py file in the QGIS settings folder, the PYQGIS_STARTUP environment variable, waiting for initializationCompleted, project macros for openProject and saveProject, and when a small plugin is the better answer.","md",{"slug":1210,"type":1211,"breadcrumb":1212,"datePublished":1213,"dateModified":1213},"run-python-at-qgis-startup-pyqgis","article","Run Python at QGIS Startup","2026-09-17","\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002Frun-python-at-qgis-startup-pyqgis",{"title":5,"description":1207},"pyqgis-fundamentals-environment-setup\u002Fqgis-python-console-basics\u002Frun-python-at-qgis-startup-pyqgis\u002Findex","vxgxxdvtf79SJjirGvsrd2Qv_u3yk-zUQRwJg1f2idQ",1789632906589]