[{"data":1,"prerenderedAt":1574},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fset-project-crs-and-datum-transform-pyqgis":3},{"id":4,"title":5,"body":6,"description":1563,"extension":1564,"meta":1565,"navigation":258,"path":1570,"seo":1571,"stem":1572,"__hash__":1573},"docs\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fset-project-crs-and-datum-transform-pyqgis\u002Findex.md","Set the Project CRS and Datum Transform in PyQGIS",{"type":7,"value":8,"toc":1549},"minimark",[9,13,17,26,191,196,218,222,225,326,349,352,383,400,404,411,414,552,564,568,571,862,871,874,878,881,976,989,1072,1076,1084,1208,1220,1224,1227,1294,1318,1322,1328,1413,1417,1460,1464,1467,1471,1477,1487,1493,1512,1516,1545],[10,11,5],"h1",{"id":12},"set-the-project-crs-and-datum-transform-in-pyqgis",[14,15,16],"p",{},"Every layer in a QGIS project can be in a different coordinate reference system, and QGIS reprojects each one to the project CRS as it draws. That is on-the-fly reprojection, and it works so well that most people never think about it — until two datasets that should line up are offset by a couple of metres, which is a datum transformation choice nobody made explicitly.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","Coordinate Reference Systems in PyQGIS",". It covers setting the project CRS from Python, understanding what a datum transform is and why the default may not be the accurate one, pinning a specific transform in the project's transform context, and making a headless script produce the same numbers as the desktop.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,91,96,101,105,109,114,118,122,130,133,137,146,152,157,161,166,170,174,177,180,186],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 320","img","Layers in three different coordinate systems being reprojected on the fly to one project CRS, with the datum transform between different datums being the step that carries an accuracy choice","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Where a datum transform enters the picture",[40,41,42],"desc",{},"Three layers in different coordinate systems are drawn into one project CRS. A change of projection within the same datum is exact. A change between datums requires a transformation, and several are usually available with accuracies ranging from a few metres to a few centimetres. QGIS picks one unless the project pins a choice.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","320","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"crsArrow","0 0 10 10","8","5","7","auto-start-reverse",[63,64],"path",{"d":65,"fill":66},"M0 0 L10 5 L0 10 z","#2f3b35",[68,69,75],"text",{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Same datum is exact; different datum is a choice",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"20","58","180","52","#eff3ff","#2563eb","stroke-width:2",[68,86,90],{"x":87,"y":88,"style":89,"fill":83,"textAnchor":74},"110","80","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","parcels",[68,92,95],{"x":87,"y":93,"style":94,"fill":66,"textAnchor":74},"98","text-anchor:middle;font-size:10px;font-family:monospace","EPSG:27700 · OSGB36",[44,97],{"x":78,"y":98,"width":80,"height":81,"rx":58,"fill":99,"stroke":100,"style":84},"130","#edf8e9","#15803d",[68,102,104],{"x":87,"y":103,"style":89,"fill":100,"textAnchor":74},"152","survey points",[68,106,108],{"x":87,"y":107,"style":94,"fill":66,"textAnchor":74},"170","EPSG:4326 · WGS84",[44,110],{"x":78,"y":111,"width":80,"height":81,"rx":58,"fill":112,"stroke":113,"style":84},"202","#fdf2e2","#b45309",[68,115,117],{"x":87,"y":116,"style":89,"fill":113,"textAnchor":74},"224","basemap tiles",[68,119,121],{"x":87,"y":120,"style":94,"fill":66,"textAnchor":74},"242","EPSG:3857 · WGS84",[123,124],"line",{"x1":125,"y1":126,"x2":127,"y2":128,"stroke":66,"style":129},"204","84","264","140","stroke-width:2;marker-end:url(#crsArrow)",[123,131],{"x1":125,"y1":132,"x2":127,"y2":132,"stroke":66,"style":129},"156",[123,134],{"x1":125,"y1":135,"x2":127,"y2":136,"stroke":66,"style":129},"228","172",[44,138],{"x":139,"y":140,"width":141,"height":88,"rx":142,"fill":143,"stroke":144,"style":145},"272","118","200","10","#eef7f4","#0f766e","stroke-width:2.5",[68,147,151],{"x":148,"y":149,"style":150,"fill":144,"textAnchor":74},"372","146","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","transform context",[68,153,156],{"x":148,"y":154,"style":155,"fill":66,"textAnchor":74},"168","text-anchor:middle;font-size:10px;font-family:sans-serif","which operation, per pair",[68,158,160],{"x":148,"y":159,"style":155,"fill":66,"textAnchor":74},"188","stored in the project",[123,162],{"x1":163,"y1":164,"x2":165,"y2":164,"stroke":66,"style":129},"476","158","524",[44,167],{"x":168,"y":140,"width":125,"height":88,"rx":142,"fill":169,"stroke":73,"style":145},"532","#fffdf7",[68,171,173],{"x":172,"y":149,"style":150,"fill":73,"textAnchor":74},"634","project CRS",[68,175,176],{"x":172,"y":154,"style":94,"fill":66,"textAnchor":74},"EPSG:27700",[68,178,179],{"x":172,"y":159,"style":155,"fill":66,"textAnchor":74},"what the canvas draws in",[44,181],{"x":182,"y":127,"width":183,"height":184,"rx":58,"fill":112,"stroke":185,"style":84},"150","460","42","#b91c1c",[68,187,190],{"x":70,"y":188,"style":189,"fill":66,"textAnchor":74},"290","text-anchor:middle;font-size:10.5px;font-family:sans-serif","OSGB36 to WGS84 has options from ~5 m to ~0.1 m accuracy",[192,193,195],"h2",{"id":194},"prerequisites","Prerequisites",[197,198,199,207,215],"ul",{},[200,201,202,206],"li",{},[203,204,205],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer, built against PROJ 6 or later — every 3.x release since 3.10 is.",[200,208,209,210,214],{},"Layers whose CRSs are correctly declared. A transform cannot fix a layer with the wrong CRS assigned; see ",[21,211,213],{"href":212},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fhandling-missing-crs-in-pyqgis\u002F","handling missing CRS",".",[200,216,217],{},"For the highest-accuracy transforms, the relevant PROJ grid files installed. QGIS reports which are missing.",[192,219,221],{"id":220},"set-the-project-crs","Set the project CRS",[14,223,224],{},"Two lines, and one of them is easy to forget.",[226,227,232],"pre",{"className":228,"code":229,"language":230,"meta":231,"style":231},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsCoordinateReferenceSystem\n\nproject = QgsProject.instance()\ncrs = QgsCoordinateReferenceSystem(\"EPSG:27700\")\nif not crs.isValid():\n    raise ValueError(\"EPSG:27700 not recognised — check the PROJ database\")\nproject.setCrs(crs)\n","python","",[233,234,235,253,260,272,290,302,320],"code",{"__ignoreMap":231},[236,237,239,243,247,250],"span",{"class":123,"line":238},1,[236,240,242],{"class":241},"snl16","from",[236,244,246],{"class":245},"s95oV"," qgis.core ",[236,248,249],{"class":241},"import",[236,251,252],{"class":245}," QgsProject, QgsCoordinateReferenceSystem\n",[236,254,256],{"class":123,"line":255},2,[236,257,259],{"emptyLinePlaceholder":258},true,"\n",[236,261,263,266,269],{"class":123,"line":262},3,[236,264,265],{"class":245},"project ",[236,267,268],{"class":241},"=",[236,270,271],{"class":245}," QgsProject.instance()\n",[236,273,275,278,280,283,287],{"class":123,"line":274},4,[236,276,277],{"class":245},"crs ",[236,279,268],{"class":241},[236,281,282],{"class":245}," QgsCoordinateReferenceSystem(",[236,284,286],{"class":285},"sU2Wk","\"EPSG:27700\"",[236,288,289],{"class":245},")\n",[236,291,293,296,299],{"class":123,"line":292},5,[236,294,295],{"class":241},"if",[236,297,298],{"class":241}," not",[236,300,301],{"class":245}," crs.isValid():\n",[236,303,305,308,312,315,318],{"class":123,"line":304},6,[236,306,307],{"class":241},"    raise",[236,309,311],{"class":310},"sDLfK"," ValueError",[236,313,314],{"class":245},"(",[236,316,317],{"class":285},"\"EPSG:27700 not recognised — check the PROJ database\"",[236,319,289],{"class":245},[236,321,323],{"class":123,"line":322},7,[236,324,325],{"class":245},"project.setCrs(crs)\n",[14,327,328,331,332,335,336,340,341,344,345,348],{},[203,329,330],{},"Breakdown:"," ",[233,333,334],{},"QgsCoordinateReferenceSystem"," accepts an authority string, a WKT definition or a PROJ string, and returns an ",[337,338,339],"em",{},"invalid"," object rather than raising when it cannot resolve one — so the ",[233,342,343],{},"isValid()"," check is what turns a silent no-op into an error. ",[233,346,347],{},"setCrs()"," changes the canvas CRS and triggers a redraw; it does not touch any layer's own CRS, which is the point of on-the-fly reprojection.",[14,350,351],{},"Reading it back is equally direct, and worth doing in any script that will run against projects it did not create:",[226,353,355],{"className":228,"code":354,"language":230,"meta":231,"style":231},"print(project.crs().authid(), project.crs().description())\nprint(\"units:\", project.crs().mapUnits(), \"geographic:\", project.crs().isGeographic())\n",[233,356,357,365],{"__ignoreMap":231},[236,358,359,362],{"class":123,"line":238},[236,360,361],{"class":310},"print",[236,363,364],{"class":245},"(project.crs().authid(), project.crs().description())\n",[236,366,367,369,371,374,377,380],{"class":123,"line":255},[236,368,361],{"class":310},[236,370,314],{"class":245},[236,372,373],{"class":285},"\"units:\"",[236,375,376],{"class":245},", project.crs().mapUnits(), ",[236,378,379],{"class":285},"\"geographic:\"",[236,381,382],{"class":245},", project.crs().isGeographic())\n",[14,384,385,331,387,390,391,395,396,399],{},[203,386,330],{},[233,388,389],{},"mapUnits()"," is what determines whether a distance in an expression means metres, feet or degrees, which affects ",[21,392,394],{"href":393},"\u002Fspatial-data-processing-automation\u002Fgeometry-operations-and-predicates\u002Fbuffer-geometry-pyqgis\u002F","buffers",", scale ranges and geometry generators alike. A script that assumes metres and finds ",[233,397,398],{},"isGeographic()"," true should stop rather than produce numbers.",[192,401,403],{"id":402},"what-a-datum-transform-actually-is","What a datum transform actually is",[14,405,406,407,410],{},"A projection change within one datum — British National Grid to a different projection of OSGB36 — is pure mathematics and exact. A change ",[337,408,409],{},"between"," datums is not: the two reference frames genuinely differ, and converting between them requires an empirically derived transformation.",[14,412,413],{},"For OSGB36 to WGS84 there are several. A seven-parameter Helmert transformation is fast, available everywhere and accurate to about five metres. The OSTN15 grid shift file is accurate to about a tenth of a metre and requires the grid file to be installed. QGIS picks one automatically, preferring the most accurate available — but \"available\" depends on which grids are installed, so the same project can produce different coordinates on two machines.",[226,415,417],{"className":228,"code":416,"language":230,"meta":231,"style":231},"from qgis.core import QgsDatumTransform, QgsCoordinateReferenceSystem\n\nsource = QgsCoordinateReferenceSystem(\"EPSG:27700\")\ndest = QgsCoordinateReferenceSystem(\"EPSG:4326\")\n\nfor op in QgsDatumTransform.operations(source, dest):\n    status = \"available\" if op.isAvailable else \"MISSING GRID\"\n    print(f\"{op.accuracy:>6.2f} m  {status:\u003C12} {op.proj}\")\n",[233,418,419,430,434,447,461,465,479,501],{"__ignoreMap":231},[236,420,421,423,425,427],{"class":123,"line":238},[236,422,242],{"class":241},[236,424,246],{"class":245},[236,426,249],{"class":241},[236,428,429],{"class":245}," QgsDatumTransform, QgsCoordinateReferenceSystem\n",[236,431,432],{"class":123,"line":255},[236,433,259],{"emptyLinePlaceholder":258},[236,435,436,439,441,443,445],{"class":123,"line":262},[236,437,438],{"class":245},"source ",[236,440,268],{"class":241},[236,442,282],{"class":245},[236,444,286],{"class":285},[236,446,289],{"class":245},[236,448,449,452,454,456,459],{"class":123,"line":274},[236,450,451],{"class":245},"dest ",[236,453,268],{"class":241},[236,455,282],{"class":245},[236,457,458],{"class":285},"\"EPSG:4326\"",[236,460,289],{"class":245},[236,462,463],{"class":123,"line":292},[236,464,259],{"emptyLinePlaceholder":258},[236,466,467,470,473,476],{"class":123,"line":304},[236,468,469],{"class":241},"for",[236,471,472],{"class":245}," op ",[236,474,475],{"class":241},"in",[236,477,478],{"class":245}," QgsDatumTransform.operations(source, dest):\n",[236,480,481,484,486,489,492,495,498],{"class":123,"line":322},[236,482,483],{"class":245},"    status ",[236,485,268],{"class":241},[236,487,488],{"class":285}," \"available\"",[236,490,491],{"class":241}," if",[236,493,494],{"class":245}," op.isAvailable ",[236,496,497],{"class":241},"else",[236,499,500],{"class":285}," \"MISSING GRID\"\n",[236,502,504,507,509,512,515,518,521,524,527,530,532,535,538,540,543,546,548,550],{"class":123,"line":503},8,[236,505,506],{"class":310},"    print",[236,508,314],{"class":245},[236,510,511],{"class":241},"f",[236,513,514],{"class":285},"\"",[236,516,517],{"class":310},"{",[236,519,520],{"class":245},"op.accuracy",[236,522,523],{"class":241},":>6.2f",[236,525,526],{"class":310},"}",[236,528,529],{"class":285}," m  ",[236,531,517],{"class":310},[236,533,534],{"class":245},"status",[236,536,537],{"class":241},":\u003C12",[236,539,526],{"class":310},[236,541,542],{"class":310}," {",[236,544,545],{"class":245},"op.proj",[236,547,526],{"class":310},[236,549,514],{"class":285},[236,551,289],{"class":245},[14,553,554,331,556,559,560,563],{},[203,555,330],{},[233,557,558],{},"operations()"," lists every candidate PROJ pipeline with its stated accuracy and whether the required grids are present. An accuracy of ",[233,561,562],{},"-1"," means unknown, which in practice means a ballpark transformation and should be treated as the worst option. Printing this list once per CRS pair a project uses is the single most useful diagnostic in this whole area, because it turns \"the layers are offset\" into a specific missing file.",[192,565,567],{"id":566},"auditing-a-projects-crs-pairs","Auditing a project's CRS pairs",[14,569,570],{},"Before pinning anything it helps to know which pairs a project actually exercises, because that is a much shorter list than \"every CRS in the database\".",[226,572,574],{"className":228,"code":573,"language":230,"meta":231,"style":231},"from itertools import product\nfrom qgis.core import QgsProject, QgsDatumTransform\n\nproject = QgsProject.instance()\ntarget = project.crs()\nsources = {layer.crs().authid(): layer.crs() for layer in project.mapLayers().values()}\n\nfor authid, crs in sorted(sources.items()):\n    if authid == target.authid():\n        continue\n    ops = QgsDatumTransform.operations(crs, target)\n    best = max((o for o in ops if o.isAvailable), key=lambda o: -o.accuracy, default=None)\n    unavailable = [o for o in ops if not o.isAvailable]\n    print(f\"{authid} → {target.authid()}: \"\n          f\"{len(ops)} operation(s), best available ±{best.accuracy if best else '?'} m, \"\n          f\"{len(unavailable)} needing grids\")\n",[233,575,576,588,599,603,611,621,641,645,660,675,681,692,747,773,804,843],{"__ignoreMap":231},[236,577,578,580,583,585],{"class":123,"line":238},[236,579,242],{"class":241},[236,581,582],{"class":245}," itertools ",[236,584,249],{"class":241},[236,586,587],{"class":245}," product\n",[236,589,590,592,594,596],{"class":123,"line":255},[236,591,242],{"class":241},[236,593,246],{"class":245},[236,595,249],{"class":241},[236,597,598],{"class":245}," QgsProject, QgsDatumTransform\n",[236,600,601],{"class":123,"line":262},[236,602,259],{"emptyLinePlaceholder":258},[236,604,605,607,609],{"class":123,"line":274},[236,606,265],{"class":245},[236,608,268],{"class":241},[236,610,271],{"class":245},[236,612,613,616,618],{"class":123,"line":292},[236,614,615],{"class":245},"target ",[236,617,268],{"class":241},[236,619,620],{"class":245}," project.crs()\n",[236,622,623,626,628,631,633,636,638],{"class":123,"line":304},[236,624,625],{"class":245},"sources ",[236,627,268],{"class":241},[236,629,630],{"class":245}," {layer.crs().authid(): layer.crs() ",[236,632,469],{"class":241},[236,634,635],{"class":245}," layer ",[236,637,475],{"class":241},[236,639,640],{"class":245}," project.mapLayers().values()}\n",[236,642,643],{"class":123,"line":322},[236,644,259],{"emptyLinePlaceholder":258},[236,646,647,649,652,654,657],{"class":123,"line":503},[236,648,469],{"class":241},[236,650,651],{"class":245}," authid, crs ",[236,653,475],{"class":241},[236,655,656],{"class":310}," sorted",[236,658,659],{"class":245},"(sources.items()):\n",[236,661,663,666,669,672],{"class":123,"line":662},9,[236,664,665],{"class":241},"    if",[236,667,668],{"class":245}," authid ",[236,670,671],{"class":241},"==",[236,673,674],{"class":245}," target.authid():\n",[236,676,678],{"class":123,"line":677},10,[236,679,680],{"class":241},"        continue\n",[236,682,684,687,689],{"class":123,"line":683},11,[236,685,686],{"class":245},"    ops ",[236,688,268],{"class":241},[236,690,691],{"class":245}," QgsDatumTransform.operations(crs, target)\n",[236,693,695,698,700,703,706,708,711,713,716,718,721,725,728,731,734,737,740,742,745],{"class":123,"line":694},12,[236,696,697],{"class":245},"    best ",[236,699,268],{"class":241},[236,701,702],{"class":310}," max",[236,704,705],{"class":245},"((o ",[236,707,469],{"class":241},[236,709,710],{"class":245}," o ",[236,712,475],{"class":241},[236,714,715],{"class":245}," ops ",[236,717,295],{"class":241},[236,719,720],{"class":245}," o.isAvailable), ",[236,722,724],{"class":723},"s9osk","key",[236,726,727],{"class":241},"=lambda",[236,729,730],{"class":245}," o: ",[236,732,733],{"class":241},"-",[236,735,736],{"class":245},"o.accuracy, ",[236,738,739],{"class":723},"default",[236,741,268],{"class":241},[236,743,744],{"class":310},"None",[236,746,289],{"class":245},[236,748,750,753,755,758,760,762,764,766,768,770],{"class":123,"line":749},13,[236,751,752],{"class":245},"    unavailable ",[236,754,268],{"class":241},[236,756,757],{"class":245}," [o ",[236,759,469],{"class":241},[236,761,710],{"class":245},[236,763,475],{"class":241},[236,765,715],{"class":245},[236,767,295],{"class":241},[236,769,298],{"class":241},[236,771,772],{"class":245}," o.isAvailable]\n",[236,774,776,778,780,782,784,786,789,791,794,796,799,801],{"class":123,"line":775},14,[236,777,506],{"class":310},[236,779,314],{"class":245},[236,781,511],{"class":241},[236,783,514],{"class":285},[236,785,517],{"class":310},[236,787,788],{"class":245},"authid",[236,790,526],{"class":310},[236,792,793],{"class":285}," → ",[236,795,517],{"class":310},[236,797,798],{"class":245},"target.authid()",[236,800,526],{"class":310},[236,802,803],{"class":285},": \"\n",[236,805,807,810,812,815,818,820,823,825,828,830,833,835,838,840],{"class":123,"line":806},15,[236,808,809],{"class":241},"          f",[236,811,514],{"class":285},[236,813,814],{"class":310},"{len",[236,816,817],{"class":245},"(ops)",[236,819,526],{"class":310},[236,821,822],{"class":285}," operation(s), best available ±",[236,824,517],{"class":310},[236,826,827],{"class":245},"best.accuracy ",[236,829,295],{"class":241},[236,831,832],{"class":245}," best ",[236,834,497],{"class":241},[236,836,837],{"class":285}," '?'",[236,839,526],{"class":310},[236,841,842],{"class":285}," m, \"\n",[236,844,846,848,850,852,855,857,860],{"class":123,"line":845},16,[236,847,809],{"class":241},[236,849,514],{"class":285},[236,851,814],{"class":310},[236,853,854],{"class":245},"(unavailable)",[236,856,526],{"class":310},[236,858,859],{"class":285}," needing grids\"",[236,861,289],{"class":245},[14,863,864,866,867,870],{},[203,865,330],{}," Keying the sources by ",[233,868,869],{},"authid()"," deduplicates twenty layers in the same CRS down to one pair, which is what makes the output readable. Sorting the available operations by accuracy ascending — hence the negated key — finds the best one QGIS would actually pick, and counting the unavailable ones tells you how much better it could get with a download. Running this once when a project is set up, and pasting the result into the project's documentation, is a five-minute habit that pre-empts a whole class of later confusion.",[14,872,873],{},"The pairs that need attention are the ones crossing a datum boundary. A national grid to WGS84, an old local datum to a modern one, anything involving a pre-1990 reference frame — these are where the options differ by metres. Projections that share a datum, such as UTM zones within WGS84, have exactly one exact answer and need no pinning at all.",[192,875,877],{"id":876},"pin-the-transform-in-the-project","Pin the transform in the project",[14,879,880],{},"The transform context is stored with the project, so pinning a choice makes it reproducible.",[226,882,884],{"className":228,"code":883,"language":230,"meta":231,"style":231},"from qgis.core import QgsProject, QgsCoordinateTransformContext\n\ncontext = QgsProject.instance().transformContext()\ncontext.addCoordinateOperation(\n    QgsCoordinateReferenceSystem(\"EPSG:27700\"),\n    QgsCoordinateReferenceSystem(\"EPSG:4326\"),\n    \"+proj=pipeline +step +inv +proj=tmerc +lat_0=49 +lon_0=-2 \"\n    \"+k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy \"\n    \"+step +proj=hgridshift +grids=uk_os_OSTN15_NTv2_OSGBtoETRS.tif \"\n    \"+step +proj=unitconvert +xy_in=rad +xy_out=deg\",\n)\nQgsProject.instance().setTransformContext(context)\nQgsProject.instance().setDirty(True)\n",[233,885,886,897,901,911,916,926,934,939,944,949,957,961,966],{"__ignoreMap":231},[236,887,888,890,892,894],{"class":123,"line":238},[236,889,242],{"class":241},[236,891,246],{"class":245},[236,893,249],{"class":241},[236,895,896],{"class":245}," QgsProject, QgsCoordinateTransformContext\n",[236,898,899],{"class":123,"line":255},[236,900,259],{"emptyLinePlaceholder":258},[236,902,903,906,908],{"class":123,"line":262},[236,904,905],{"class":245},"context ",[236,907,268],{"class":241},[236,909,910],{"class":245}," QgsProject.instance().transformContext()\n",[236,912,913],{"class":123,"line":274},[236,914,915],{"class":245},"context.addCoordinateOperation(\n",[236,917,918,921,923],{"class":123,"line":292},[236,919,920],{"class":245},"    QgsCoordinateReferenceSystem(",[236,922,286],{"class":285},[236,924,925],{"class":245},"),\n",[236,927,928,930,932],{"class":123,"line":304},[236,929,920],{"class":245},[236,931,458],{"class":285},[236,933,925],{"class":245},[236,935,936],{"class":123,"line":322},[236,937,938],{"class":285},"    \"+proj=pipeline +step +inv +proj=tmerc +lat_0=49 +lon_0=-2 \"\n",[236,940,941],{"class":123,"line":503},[236,942,943],{"class":285},"    \"+k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy \"\n",[236,945,946],{"class":123,"line":662},[236,947,948],{"class":285},"    \"+step +proj=hgridshift +grids=uk_os_OSTN15_NTv2_OSGBtoETRS.tif \"\n",[236,950,951,954],{"class":123,"line":677},[236,952,953],{"class":285},"    \"+step +proj=unitconvert +xy_in=rad +xy_out=deg\"",[236,955,956],{"class":245},",\n",[236,958,959],{"class":123,"line":683},[236,960,289],{"class":245},[236,962,963],{"class":123,"line":694},[236,964,965],{"class":245},"QgsProject.instance().setTransformContext(context)\n",[236,967,968,971,974],{"class":123,"line":749},[236,969,970],{"class":245},"QgsProject.instance().setDirty(",[236,972,973],{"class":310},"True",[236,975,289],{"class":245},[14,977,978,980,981,984,985,988],{},[203,979,330],{}," The operation is a PROJ pipeline string, which is exactly what ",[233,982,983],{},"QgsDatumTransform.operations()"," printed above — copy the one you want rather than composing it by hand. ",[233,986,987],{},"addCoordinateOperation()"," registers it for that ordered CRS pair; the reverse direction is derived automatically. Storing it in the project means every layer, every algorithm and every export in that project uses the same transformation, which is the property that makes results comparable between machines. Without this, QGIS silently falls back to the best operation each machine happens to have.",[14,990,991],{},[29,992,995,998,1001,1004,1011,1014,1018,1022,1026,1032,1036,1041,1044,1048,1053,1057,1059,1063,1066,1069],{"viewBox":993,"role":32,"ariaLabel":994,"xmlns":34},"0 0 760 292","Two machines running the same project produce different coordinates because one has the high accuracy grid installed and the other falls back to a lower accuracy transformation",[36,996,997],{},"The same project, two machines, two answers",[40,999,1000],{},"Without a pinned transform, a machine with the grid shift file installed uses the high accuracy operation while a machine without it silently falls back to a seven parameter transformation. The resulting coordinates differ by several metres, and neither machine reports a problem.",[44,1002],{"x":46,"y":46,"width":47,"height":1003,"fill":49},"292",[51,1005,1006],{},[54,1007,1009],{"id":1008,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"dtArrow",[63,1010],{"d":65,"fill":66},[68,1012,1013],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Neither machine warns you",[44,1015],{"x":188,"y":1016,"width":80,"height":1017,"rx":58,"fill":169,"stroke":144,"style":145},"50","46",[68,1019,1021],{"x":70,"y":1020,"style":89,"fill":144,"textAnchor":74},"70","one project file",[68,1023,1025],{"x":70,"y":1024,"style":155,"fill":66,"textAnchor":74},"88","no pinned transform",[123,1027],{"x1":1028,"y1":1029,"x2":1030,"y2":128,"stroke":66,"style":1031},"330","96","230","stroke-width:2;marker-end:url(#dtArrow)",[123,1033],{"x1":1034,"y1":1029,"x2":1035,"y2":128,"stroke":66,"style":1031},"430","530",[44,1037],{"x":1038,"y":149,"width":1039,"height":1040,"rx":142,"fill":99,"stroke":100,"style":145},"30","300","112",[68,1042,1043],{"x":80,"y":136,"style":150,"fill":100,"textAnchor":74},"machine A · grid installed",[68,1045,1047],{"x":80,"y":1046,"style":155,"fill":66,"textAnchor":74},"196","OSTN15 grid shift, ±0.1 m",[68,1049,1052],{"x":80,"y":1050,"style":1051,"fill":66,"textAnchor":74},"220","text-anchor:middle;font-size:10.5px;font-family:monospace","−2.114953, 52.516771",[68,1054,1056],{"x":80,"y":1055,"style":155,"fill":100,"textAnchor":74},"244","correct",[44,1058],{"x":1034,"y":149,"width":1039,"height":1040,"rx":142,"fill":112,"stroke":185,"style":145},[68,1060,1062],{"x":1061,"y":136,"style":150,"fill":185,"textAnchor":74},"580","machine B · grid missing",[68,1064,1065],{"x":1061,"y":1046,"style":155,"fill":66,"textAnchor":74},"7-parameter Helmert, ±5 m",[68,1067,1068],{"x":1061,"y":1050,"style":1051,"fill":66,"textAnchor":74},"−2.114891, 52.516734",[68,1070,1071],{"x":1061,"y":1055,"style":155,"fill":185,"textAnchor":74},"about 5 m out",[192,1073,1075],{"id":1074},"making-a-headless-script-match-the-desktop","Making a headless script match the desktop",[14,1077,1078,1079,1083],{},"A ",[21,1080,1082],{"href":1081},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002F","standalone script"," that reads a project inherits its transform context, which is the whole reason for pinning it. A script that builds a project from scratch does not, and must set the context itself.",[226,1085,1087],{"className":228,"code":1086,"language":230,"meta":231,"style":231},"from qgis.core import (\n    QgsCoordinateTransform, QgsCoordinateTransformContext,\n    QgsCoordinateReferenceSystem, QgsPointXY,\n)\n\ncontext = QgsCoordinateTransformContext()\ncontext.addCoordinateOperation(source, dest, PIPELINE)\n\ntransform = QgsCoordinateTransform(source, dest, context)\npoint = transform.transform(QgsPointXY(432150, 189400))\nprint(f\"{point.x():.6f}, {point.y():.6f}\")\n",[233,1088,1089,1100,1105,1110,1114,1118,1127,1137,1141,1151,1173],{"__ignoreMap":231},[236,1090,1091,1093,1095,1097],{"class":123,"line":238},[236,1092,242],{"class":241},[236,1094,246],{"class":245},[236,1096,249],{"class":241},[236,1098,1099],{"class":245}," (\n",[236,1101,1102],{"class":123,"line":255},[236,1103,1104],{"class":245},"    QgsCoordinateTransform, QgsCoordinateTransformContext,\n",[236,1106,1107],{"class":123,"line":262},[236,1108,1109],{"class":245},"    QgsCoordinateReferenceSystem, QgsPointXY,\n",[236,1111,1112],{"class":123,"line":274},[236,1113,289],{"class":245},[236,1115,1116],{"class":123,"line":292},[236,1117,259],{"emptyLinePlaceholder":258},[236,1119,1120,1122,1124],{"class":123,"line":304},[236,1121,905],{"class":245},[236,1123,268],{"class":241},[236,1125,1126],{"class":245}," QgsCoordinateTransformContext()\n",[236,1128,1129,1132,1135],{"class":123,"line":322},[236,1130,1131],{"class":245},"context.addCoordinateOperation(source, dest, ",[236,1133,1134],{"class":310},"PIPELINE",[236,1136,289],{"class":245},[236,1138,1139],{"class":123,"line":503},[236,1140,259],{"emptyLinePlaceholder":258},[236,1142,1143,1146,1148],{"class":123,"line":662},[236,1144,1145],{"class":245},"transform ",[236,1147,268],{"class":241},[236,1149,1150],{"class":245}," QgsCoordinateTransform(source, dest, context)\n",[236,1152,1153,1156,1158,1161,1164,1167,1170],{"class":123,"line":677},[236,1154,1155],{"class":245},"point ",[236,1157,268],{"class":241},[236,1159,1160],{"class":245}," transform.transform(QgsPointXY(",[236,1162,1163],{"class":310},"432150",[236,1165,1166],{"class":245},", ",[236,1168,1169],{"class":310},"189400",[236,1171,1172],{"class":245},"))\n",[236,1174,1175,1177,1179,1181,1183,1185,1188,1191,1193,1195,1197,1200,1202,1204,1206],{"class":123,"line":683},[236,1176,361],{"class":310},[236,1178,314],{"class":245},[236,1180,511],{"class":241},[236,1182,514],{"class":285},[236,1184,517],{"class":310},[236,1186,1187],{"class":245},"point.x()",[236,1189,1190],{"class":241},":.6f",[236,1192,526],{"class":310},[236,1194,1166],{"class":285},[236,1196,517],{"class":310},[236,1198,1199],{"class":245},"point.y()",[236,1201,1190],{"class":241},[236,1203,526],{"class":310},[236,1205,514],{"class":285},[236,1207,289],{"class":245},[14,1209,1210,331,1212,1215,1216,1219],{},[203,1211,330],{},[233,1213,1214],{},"QgsCoordinateTransform"," accepts either a context or a ",[233,1217,1218],{},"QgsProject","; passing a bare project uses that project's pinned operations, which is usually what you want inside QGIS. Constructing the context explicitly is what makes a script deterministic regardless of what the machine has installed — and if the pinned operation's grid is missing, the transform fails loudly instead of quietly substituting a worse one. That failure is a feature: it names the file to install.",[192,1221,1223],{"id":1222},"warn-when-a-grid-is-missing","Warn when a grid is missing",[14,1225,1226],{},"QGIS emits a signal when it falls back, and connecting to it turns an invisible problem into a log line.",[226,1228,1230],{"className":228,"code":1229,"language":230,"meta":231,"style":231},"def on_missing(source_crs, dest_crs, error):\n    print(f\"datum transform problem {source_crs.authid()} → {dest_crs.authid()}: {error}\")\n\nQgsProject.instance().transformContext().missingRequiredGridWarning.connect(on_missing)\n",[233,1231,1232,1244,1285,1289],{"__ignoreMap":231},[236,1233,1234,1237,1241],{"class":123,"line":238},[236,1235,1236],{"class":241},"def",[236,1238,1240],{"class":1239},"svObZ"," on_missing",[236,1242,1243],{"class":245},"(source_crs, dest_crs, error):\n",[236,1245,1246,1248,1250,1252,1255,1257,1260,1262,1264,1266,1269,1271,1274,1276,1279,1281,1283],{"class":123,"line":255},[236,1247,506],{"class":310},[236,1249,314],{"class":245},[236,1251,511],{"class":241},[236,1253,1254],{"class":285},"\"datum transform problem ",[236,1256,517],{"class":310},[236,1258,1259],{"class":245},"source_crs.authid()",[236,1261,526],{"class":310},[236,1263,793],{"class":285},[236,1265,517],{"class":310},[236,1267,1268],{"class":245},"dest_crs.authid()",[236,1270,526],{"class":310},[236,1272,1273],{"class":285},": ",[236,1275,517],{"class":310},[236,1277,1278],{"class":245},"error",[236,1280,526],{"class":310},[236,1282,514],{"class":285},[236,1284,289],{"class":245},[236,1286,1287],{"class":123,"line":262},[236,1288,259],{"emptyLinePlaceholder":258},[236,1290,1291],{"class":123,"line":274},[236,1292,1293],{"class":245},"QgsProject.instance().transformContext().missingRequiredGridWarning.connect(on_missing)\n",[14,1295,1296,1298,1299,1166,1302,1166,1305,1308,1309,1312,1313,1317],{},[203,1297,330],{}," The context emits several related signals — ",[233,1300,1301],{},"missingRequiredGridWarning",[233,1303,1304],{},"missingPreferredGridWarning",[233,1306,1307],{},"coordinateOperationCreationError"," and ",[233,1310,1311],{},"missingGridUsedByContextHandler"," — covering the cases where the pinned operation is unavailable, where a better one exists but is not installed, and where the pipeline itself is malformed. Connecting all four in a plugin's start-up and routing them to the ",[21,1314,1316],{"href":1315},"\u002Fpyqgis-fundamentals-environment-setup\u002Fdebugging-pyqgis-scripts\u002Flog-messages-to-qgis-message-log-pyqgis\u002F","message log"," is a few lines that pay for themselves the first time a colleague's exports are five metres out.",[192,1319,1321],{"id":1320},"qgis-version-compatibility","QGIS version compatibility",[14,1323,1324,1325,1327],{},"The examples target ",[203,1326,205],{}," (Python 3.12).",[1329,1330,1331,1347],"table",{},[1332,1333,1334],"thead",{},[1335,1336,1337,1341,1344],"tr",{},[1338,1339,1340],"th",{},"QGIS version",[1338,1342,1343],{},"Python",[1338,1345,1346],{},"Notes",[1348,1349,1350,1369,1380,1392,1403],"tbody",{},[1335,1351,1352,1356,1359],{},[1353,1354,1355],"td",{},"3.16 LTR",[1353,1357,1358],{},"3.7",[1353,1360,1361,1362,1365,1366,214],{},"PROJ 6 pipeline model; ",[233,1363,1364],{},"QgsCoordinateTransformContext"," with ",[233,1367,1368],{},"addCoordinateOperation",[1335,1370,1371,1374,1377],{},[1353,1372,1373],{},"3.22 LTR",[1353,1375,1376],{},"3.9",[1353,1378,1379],{},"Grid files distributed as GeoTIFF rather than NTv2 in newer PROJ builds.",[1335,1381,1382,1385,1387],{},[1353,1383,1384],{},"3.28 LTR",[1353,1386,1376],{},[1353,1388,1389,1391],{},[233,1390,983],{}," reports availability per operation.",[1335,1393,1394,1397,1400],{},[1353,1395,1396],{},"3.34 LTR",[1353,1398,1399],{},"3.12",[1353,1401,1402],{},"Baseline for this page.",[1335,1404,1405,1408,1410],{},[1353,1406,1407],{},"3.40+",[1353,1409,1399],{},[1353,1411,1412],{},"Improved automatic download of missing grids where PROJ network access is enabled.",[192,1414,1416],{"id":1415},"troubleshooting","Troubleshooting",[197,1418,1419,1425,1436,1442,1448,1454],{},[200,1420,1421,1424],{},[203,1422,1423],{},"Two layers are offset by a few metres."," A datum transformation is being chosen automatically and differs between machines. Pin it.",[200,1426,1427,1432,1433,1435],{},[203,1428,1429,1431],{},[233,1430,334],{}," is invalid."," The authority code is wrong or the PROJ database is incomplete. Check ",[233,1434,343],{}," before use.",[200,1437,1438,1441],{},[203,1439,1440],{},"Coordinates changed after a QGIS upgrade."," PROJ's preferred operation changed. Pinning the pipeline makes this a non-event.",[200,1443,1444,1447],{},[203,1445,1446],{},"The transform raises on one machine only."," The pinned operation's grid file is not installed there. The error names it.",[200,1449,1450,1453],{},[203,1451,1452],{},"A script gives different numbers from the desktop."," The script built its own context without the project's pinned operations. Pass the project instead.",[200,1455,1456,1459],{},[203,1457,1458],{},"Everything is offset by hundreds of metres."," That is not a datum transform issue — a layer has the wrong CRS declared.",[192,1461,1463],{"id":1462},"conclusion","Conclusion",[14,1465,1466],{},"Set the project CRS with a validity check, list the available operations for every CRS pair the project uses, pin the one you want in the transform context, and connect the missing-grid signals so a fallback is visible. A pinned transform is what makes a project produce the same coordinates on every machine that opens it.",[192,1468,1470],{"id":1469},"frequently-asked-questions","Frequently Asked Questions",[14,1472,1473,1476],{},[203,1474,1475],{},"Does the project CRS change my data?","\nNo. It changes only what the canvas draws in and what new layers default to. The layers keep their own CRS, and exports reproject at write time.",[14,1478,1479,1482,1483,1486],{},[203,1480,1481],{},"Where are PROJ grid files installed?","\nIn the PROJ data directory, which ",[233,1484,1485],{},"pyproj.datadir.get_data_dir()"," or the PROJ CLI reports. QGIS also offers to download missing grids when PROJ network access is enabled in the settings.",[14,1488,1489,1492],{},[203,1490,1491],{},"Should I set the project CRS to match my data or my output?","\nTo your primary working CRS — usually the one most of the data is in and the one the deliverable needs. Anything else adds a reprojection to every draw for no benefit.",[14,1494,1495,1498,1499,1502,1503,1506,1507,1511],{},[203,1496,1497],{},"How do I reproject the data itself rather than on the fly?","\nUse ",[233,1500,1501],{},"native:reprojectlayer"," or, for rasters, ",[233,1504,1505],{},"gdal:warpreproject"," — see ",[21,1508,1510],{"href":1509},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fbatch-reprojecting-raster-datasets\u002F","batch reprojecting raster datasets",". Both honour the project's transform context when run inside QGIS.",[192,1513,1515],{"id":1514},"related","Related",[197,1517,1518,1523,1529,1534,1539],{},[200,1519,1520,1522],{},[21,1521,24],{"href":23}," — the guide this recipe belongs to",[200,1524,1525],{},[21,1526,1528],{"href":1527},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Ftransform-point-coordinates-pyqgis\u002F","Transform Point Coordinates in PyQGIS",[200,1530,1531],{},[21,1532,1533],{"href":212},"Handling a Missing CRS in PyQGIS",[200,1535,1536],{},[21,1537,1538],{"href":1509},"Batch Reprojecting Raster Datasets",[200,1540,1541],{},[21,1542,1544],{"href":1543},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fuse-project-variables-and-metadata-pyqgis\u002F","Use Project Variables and Metadata in PyQGIS",[1546,1547,1548],"style",{},"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 .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);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":231,"searchDepth":255,"depth":255,"links":1550},[1551,1552,1553,1554,1555,1556,1557,1558,1559,1560,1561,1562],{"id":194,"depth":255,"text":195},{"id":220,"depth":255,"text":221},{"id":402,"depth":255,"text":403},{"id":566,"depth":255,"text":567},{"id":876,"depth":255,"text":877},{"id":1074,"depth":255,"text":1075},{"id":1222,"depth":255,"text":1223},{"id":1320,"depth":255,"text":1321},{"id":1415,"depth":255,"text":1416},{"id":1462,"depth":255,"text":1463},{"id":1469,"depth":255,"text":1470},{"id":1514,"depth":255,"text":1515},"Control on-the-fly reprojection from Python — set the project CRS, choose a datum transform explicitly with QgsCoordinateTransformContext, and stop QGIS picking a metre-accurate shortcut.","md",{"slug":1566,"type":1567,"breadcrumb":1568,"datePublished":1569,"dateModified":1569},"set-project-crs-and-datum-transform-pyqgis","article","Project CRS & Datum Transforms","2026-08-27","\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fset-project-crs-and-datum-transform-pyqgis",{"title":5,"description":1563},"spatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fset-project-crs-and-datum-transform-pyqgis\u002Findex","WuApRsviL3U7txo-nyMVsYaqmDuhvMekCJk6GWoaqIw",1787823363611]