[{"data":1,"prerenderedAt":1555},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Ftransform-point-coordinates-pyqgis":3},{"id":4,"title":5,"body":6,"description":1544,"extension":1545,"meta":1546,"navigation":289,"path":1551,"seo":1552,"stem":1553,"__hash__":1554},"docs\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Ftransform-point-coordinates-pyqgis\u002Findex.md","Transform Point Coordinates Between CRS in PyQGIS",{"type":7,"value":8,"toc":1530},"minimark",[9,13,22,31,197,202,219,223,226,421,445,449,452,558,574,578,581,646,654,658,661,778,943,967,971,974,1035,1061,1065,1068,1152,1268,1288,1292,1298,1363,1366,1370,1424,1428,1434,1438,1448,1454,1460,1470,1479,1485,1491,1497,1501,1526],[10,11,5],"h1",{"id":12},"transform-point-coordinates-between-crs-in-pyqgis",[14,15,16,17,21],"p",{},"Converting a latitude and longitude into a projected easting and northing is three lines of PyQGIS, and every one of them has a detail that matters. The transform needs a ",[18,19,20],"em",{},"context"," or it may choose a different datum path from the one the GUI uses. It is expensive to construct and cheap to reuse, so building it inside a loop is the difference between a script that finishes and one that crawls. And it raises rather than returning a sentinel when a coordinate falls outside the target projection's valid area.",[14,23,24,25,30],{},"This page is a focused recipe within ",[26,27,29],"a",{"href":28},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002F","Coordinate Reference Systems in PyQGIS",". It covers a single transform, reusing one across many points, inverting the direction, and handling the failures that a real dataset will produce.",[14,32,33],{},[34,35,40,44,48,55,72,81,90,96,101,105,109,113,119,122,127,135,142,146,150,156,161,164,167,171,175,181,185,189,195],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 252","img","A coordinate transform built from a source CRS, a target CRS and a transform context, converting a geographic point into a projected one","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"The three inputs a transform needs",[45,46,47],"desc",{},"A source coordinate reference system, a target coordinate reference system and a transform context combine into a QgsCoordinateTransform. A geographic point in degrees passes through it and emerges as a projected point in metres. The context is highlighted as the argument that decides which datum path is used when several exist.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","252","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"crsPtArrow","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","26","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Three inputs build the transform; one point goes through it",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":63,"fill":87,"stroke":88,"style":89},"16","46","188","48","#fffdf7","#2563eb","stroke-width:2",[73,91,95],{"x":92,"y":93,"style":94,"fill":88,"textAnchor":79},"110","68","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","source CRS",[73,97,100],{"x":92,"y":98,"style":99,"fill":71,"textAnchor":79},"86","text-anchor:middle;font-size:11px;font-family:monospace","EPSG:4326",[49,102],{"x":83,"y":103,"width":85,"height":86,"rx":63,"fill":87,"stroke":104,"style":89},"106","#15803d",[73,106,108],{"x":92,"y":107,"style":94,"fill":104,"textAnchor":79},"128","target CRS",[73,110,112],{"x":92,"y":111,"style":99,"fill":71,"textAnchor":79},"146","EPSG:32633",[49,114],{"x":83,"y":115,"width":85,"height":116,"rx":63,"fill":87,"stroke":117,"style":118},"166","56","#b45309","stroke-width:2.5",[73,120,121],{"x":92,"y":85,"style":94,"fill":117,"textAnchor":79},"transform context",[73,123,126],{"x":92,"y":124,"style":125,"fill":71,"textAnchor":79},"206","text-anchor:middle;font-size:11px;font-family:sans-serif","picks the datum path",[49,128],{"x":129,"y":98,"width":130,"height":131,"rx":132,"fill":133,"stroke":134,"style":118},"256","216","96","10","#26322d","#0f766e",[73,136,141],{"x":137,"y":138,"style":139,"fill":140,"textAnchor":79},"364","118","text-anchor:middle;font-size:12.5px;font-weight:bold;font-family:sans-serif","#d9f99d","QgsCoordinateTransform",[73,143,145],{"x":137,"y":144,"style":125,"fill":140,"textAnchor":79},"142","expensive to build",[73,147,149],{"x":137,"y":148,"style":125,"fill":140,"textAnchor":79},"160","cheap to reuse",[49,151],{"x":152,"y":153,"width":154,"height":155,"rx":132,"fill":87,"stroke":88,"style":118},"524","52","220","60",[73,157,160],{"x":158,"y":159,"style":94,"fill":88,"textAnchor":79},"634","76","in — degrees",[73,162,163],{"x":158,"y":131,"style":99,"fill":71,"textAnchor":79},"12.4924, 41.8902",[49,165],{"x":152,"y":166,"width":154,"height":155,"rx":132,"fill":87,"stroke":104,"style":118},"152",[73,168,170],{"x":158,"y":169,"style":94,"fill":104,"textAnchor":79},"176","out — metres",[73,172,174],{"x":158,"y":173,"style":99,"fill":71,"textAnchor":79},"196","789 038, 4 641 418",[176,177],"line",{"x1":178,"y1":179,"x2":53,"y2":103,"stroke":71,"style":180},"204","70","stroke-width:2;marker-end:url(#crsPtArrow)",[176,182],{"x1":178,"y1":183,"x2":53,"y2":184,"stroke":71,"style":180},"130","132",[176,186],{"x1":178,"y1":187,"x2":53,"y2":148,"stroke":117,"style":188},"192","stroke-width:2.5;marker-end:url(#crsPtArrow)",[176,190],{"x1":191,"y1":192,"x2":193,"y2":194,"stroke":88,"style":188},"520","88","476","116",[176,196],{"x1":193,"y1":166,"x2":191,"y2":169,"stroke":104,"style":188},[198,199,201],"h2",{"id":200},"prerequisites","Prerequisites",[203,204,205,213,216],"ul",{},[206,207,208,212],"li",{},[209,210,211],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer.",[206,214,215],{},"The EPSG codes of both systems. Prefer codes over raw WKT, which drifts between PROJ versions.",[206,217,218],{},"For datum shifts between older national grids and WGS84, the PROJ transformation grids installed alongside QGIS.",[198,220,222],{"id":221},"transform-a-single-point","Transform a single point",[14,224,225],{},"Three objects: two CRSs and a context.",[227,228,233],"pre",{"className":229,"code":230,"language":231,"meta":232,"style":232},"language-python shiki shiki-themes github-dark","from qgis.core import (\n    QgsCoordinateReferenceSystem,\n    QgsCoordinateTransform,\n    QgsPointXY,\n    QgsProject,\n)\n\nsource = QgsCoordinateReferenceSystem(\"EPSG:4326\")\ntarget = QgsCoordinateReferenceSystem(\"EPSG:32633\")\ntransform = QgsCoordinateTransform(source, target, QgsProject.instance())\n\npoint = QgsPointXY(12.4924, 41.8902)          # longitude, latitude\nprojected = transform.transform(point)\nprint(f\"{projected.x():.2f}, {projected.y():.2f}\")\n","python","",[234,235,236,254,260,266,272,278,284,291,309,324,335,340,368,379],"code",{"__ignoreMap":232},[237,238,240,244,248,251],"span",{"class":176,"line":239},1,[237,241,243],{"class":242},"snl16","from",[237,245,247],{"class":246},"s95oV"," qgis.core ",[237,249,250],{"class":242},"import",[237,252,253],{"class":246}," (\n",[237,255,257],{"class":176,"line":256},2,[237,258,259],{"class":246},"    QgsCoordinateReferenceSystem,\n",[237,261,263],{"class":176,"line":262},3,[237,264,265],{"class":246},"    QgsCoordinateTransform,\n",[237,267,269],{"class":176,"line":268},4,[237,270,271],{"class":246},"    QgsPointXY,\n",[237,273,275],{"class":176,"line":274},5,[237,276,277],{"class":246},"    QgsProject,\n",[237,279,281],{"class":176,"line":280},6,[237,282,283],{"class":246},")\n",[237,285,287],{"class":176,"line":286},7,[237,288,290],{"emptyLinePlaceholder":289},true,"\n",[237,292,294,297,300,303,307],{"class":176,"line":293},8,[237,295,296],{"class":246},"source ",[237,298,299],{"class":242},"=",[237,301,302],{"class":246}," QgsCoordinateReferenceSystem(",[237,304,306],{"class":305},"sU2Wk","\"EPSG:4326\"",[237,308,283],{"class":246},[237,310,312,315,317,319,322],{"class":176,"line":311},9,[237,313,314],{"class":246},"target ",[237,316,299],{"class":242},[237,318,302],{"class":246},[237,320,321],{"class":305},"\"EPSG:32633\"",[237,323,283],{"class":246},[237,325,327,330,332],{"class":176,"line":326},10,[237,328,329],{"class":246},"transform ",[237,331,299],{"class":242},[237,333,334],{"class":246}," QgsCoordinateTransform(source, target, QgsProject.instance())\n",[237,336,338],{"class":176,"line":337},11,[237,339,290],{"emptyLinePlaceholder":289},[237,341,343,346,348,351,355,358,361,364],{"class":176,"line":342},12,[237,344,345],{"class":246},"point ",[237,347,299],{"class":242},[237,349,350],{"class":246}," QgsPointXY(",[237,352,354],{"class":353},"sDLfK","12.4924",[237,356,357],{"class":246},", ",[237,359,360],{"class":353},"41.8902",[237,362,363],{"class":246},")          ",[237,365,367],{"class":366},"sjoCn","# longitude, latitude\n",[237,369,371,374,376],{"class":176,"line":370},13,[237,372,373],{"class":246},"projected ",[237,375,299],{"class":242},[237,377,378],{"class":246}," transform.transform(point)\n",[237,380,382,385,388,391,394,397,400,403,406,408,410,413,415,417,419],{"class":176,"line":381},14,[237,383,384],{"class":353},"print",[237,386,387],{"class":246},"(",[237,389,390],{"class":242},"f",[237,392,393],{"class":305},"\"",[237,395,396],{"class":353},"{",[237,398,399],{"class":246},"projected.x()",[237,401,402],{"class":242},":.2f",[237,404,405],{"class":353},"}",[237,407,357],{"class":305},[237,409,396],{"class":353},[237,411,412],{"class":246},"projected.y()",[237,414,402],{"class":242},[237,416,405],{"class":353},[237,418,393],{"class":305},[237,420,283],{"class":246},[14,422,423,426,427,430,431,434,435,437,438,440,441,444],{},[209,424,425],{},"Breakdown:"," ",[234,428,429],{},"QgsPointXY"," takes ",[209,432,433],{},"x then y"," — longitude before latitude for a geographic CRS. Writing them the way people say them out loud is the single most common error here, and it puts the result somewhere plausible rather than raising. The third argument to ",[234,436,141],{}," is the transform ",[18,439,20],{},": passing ",[234,442,443],{},"QgsProject.instance()"," makes the script use the same datum path the GUI would, which matters whenever more than one path exists between two datums. Omitting it can introduce sub-metre shifts that nobody notices until the data is overlaid with someone else's.",[198,446,448],{"id":447},"reuse-the-transform","Reuse the transform",[14,450,451],{},"Constructing a transform resolves a datum path through PROJ, which is far too expensive to repeat per point.",[227,453,455],{"className":229,"code":454,"language":231,"meta":232,"style":232},"from qgis.core import QgsCoordinateTransform, QgsProject\n\nproject = QgsProject.instance()\ntransform = QgsCoordinateTransform(source, target, project)   # once\n\nconverted = []\nfor feature in layer.getFeatures():\n    point = feature.geometry().asPoint()\n    converted.append(transform.transform(point))\n\nprint(len(converted), \"point(s) converted\")\n",[234,456,457,468,472,482,494,498,508,522,532,537,541],{"__ignoreMap":232},[237,458,459,461,463,465],{"class":176,"line":239},[237,460,243],{"class":242},[237,462,247],{"class":246},[237,464,250],{"class":242},[237,466,467],{"class":246}," QgsCoordinateTransform, QgsProject\n",[237,469,470],{"class":176,"line":256},[237,471,290],{"emptyLinePlaceholder":289},[237,473,474,477,479],{"class":176,"line":262},[237,475,476],{"class":246},"project ",[237,478,299],{"class":242},[237,480,481],{"class":246}," QgsProject.instance()\n",[237,483,484,486,488,491],{"class":176,"line":268},[237,485,329],{"class":246},[237,487,299],{"class":242},[237,489,490],{"class":246}," QgsCoordinateTransform(source, target, project)   ",[237,492,493],{"class":366},"# once\n",[237,495,496],{"class":176,"line":274},[237,497,290],{"emptyLinePlaceholder":289},[237,499,500,503,505],{"class":176,"line":280},[237,501,502],{"class":246},"converted ",[237,504,299],{"class":242},[237,506,507],{"class":246}," []\n",[237,509,510,513,516,519],{"class":176,"line":286},[237,511,512],{"class":242},"for",[237,514,515],{"class":246}," feature ",[237,517,518],{"class":242},"in",[237,520,521],{"class":246}," layer.getFeatures():\n",[237,523,524,527,529],{"class":176,"line":293},[237,525,526],{"class":246},"    point ",[237,528,299],{"class":242},[237,530,531],{"class":246}," feature.geometry().asPoint()\n",[237,533,534],{"class":176,"line":311},[237,535,536],{"class":246},"    converted.append(transform.transform(point))\n",[237,538,539],{"class":176,"line":326},[237,540,290],{"emptyLinePlaceholder":289},[237,542,543,545,547,550,553,556],{"class":176,"line":337},[237,544,384],{"class":353},[237,546,387],{"class":246},[237,548,549],{"class":353},"len",[237,551,552],{"class":246},"(converted), ",[237,554,555],{"class":305},"\"point(s) converted\"",[237,557,283],{"class":246},[14,559,560,562,563,566,567,569,570,573],{},[209,561,425],{}," Building the transform outside the loop is the whole optimisation, and on a large layer it is the difference between seconds and minutes. ",[234,564,565],{},"geometry().asPoint()"," returns a ",[234,568,429],{}," for a single-point geometry and raises for anything else, so guard with ",[234,571,572],{},"wkbType()"," on a layer that might contain multi-points. The transform object is stateless with respect to the points passed through it, so reusing it is entirely safe.",[198,575,577],{"id":576},"invert-the-direction","Invert the direction",[14,579,580],{},"The same transform runs backwards, which avoids constructing a second one.",[227,582,584],{"className":229,"code":583,"language":231,"meta":232,"style":232},"from qgis.core import QgsCoordinateTransform\n\nback = transform.transform(projected, QgsCoordinateTransform.ReverseTransform)\nprint(f\"{back.x():.6f}, {back.y():.6f}\")\n",[234,585,586,597,601,611],{"__ignoreMap":232},[237,587,588,590,592,594],{"class":176,"line":239},[237,589,243],{"class":242},[237,591,247],{"class":246},[237,593,250],{"class":242},[237,595,596],{"class":246}," QgsCoordinateTransform\n",[237,598,599],{"class":176,"line":256},[237,600,290],{"emptyLinePlaceholder":289},[237,602,603,606,608],{"class":176,"line":262},[237,604,605],{"class":246},"back ",[237,607,299],{"class":242},[237,609,610],{"class":246}," transform.transform(projected, QgsCoordinateTransform.ReverseTransform)\n",[237,612,613,615,617,619,621,623,626,629,631,633,635,638,640,642,644],{"class":176,"line":268},[237,614,384],{"class":353},[237,616,387],{"class":246},[237,618,390],{"class":242},[237,620,393],{"class":305},[237,622,396],{"class":353},[237,624,625],{"class":246},"back.x()",[237,627,628],{"class":242},":.6f",[237,630,405],{"class":353},[237,632,357],{"class":305},[237,634,396],{"class":353},[237,636,637],{"class":246},"back.y()",[237,639,628],{"class":242},[237,641,405],{"class":353},[237,643,393],{"class":305},[237,645,283],{"class":246},[14,647,648,426,650,653],{},[209,649,425],{},[234,651,652],{},"ReverseTransform"," sends the point from target back to source using the same resolved datum path, which is what makes a round trip return the original coordinates rather than something a few centimetres away. Constructing a fresh transform with the CRSs swapped would work but might resolve a different path, so reusing this one is both faster and more consistent.",[198,655,657],{"id":656},"handle-coordinates-outside-the-valid-area","Handle coordinates outside the valid area",[14,659,660],{},"Every projection has a domain. A UTM zone is defined for a 6° band; feed it a point from the other side of the world and PROJ raises.",[14,662,663],{},[34,664,667,670,673,676,679,688,692,698,717,720,725,728,731,735,738,741,746,750,753,756,759,762,765,768,772,775],{"viewBox":665,"role":37,"ariaLabel":666,"xmlns":39},"0 0 760 254","A UTM zone's valid band shown against points inside it, at its edge, and far outside, with the outcome of transforming each",[41,668,669],{},"Inside, at the edge, and beyond a projection's domain",[45,671,672],{},"A world strip shows a UTM zone's six degree band highlighted. A point inside the band transforms accurately. A point just outside the band still transforms but with growing distortion. A point on the far side of the world raises a QgsCsException rather than returning a value.",[49,674],{"x":51,"y":51,"width":52,"height":675,"fill":54},"254",[73,677,678],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"A transform can succeed, degrade, or raise",[49,680],{"x":681,"y":682,"width":683,"height":684,"rx":685,"fill":87,"stroke":686,"style":687},"40","50","680","90","6","#59645f","stroke-width:1.5",[49,689],{"x":690,"y":682,"width":179,"height":684,"fill":104,"fillOpacity":691,"stroke":104,"style":89},"330",0.18,[73,693,697],{"x":694,"y":695,"style":696,"fill":104,"textAnchor":79},"365","44","text-anchor:middle;font-size:10.5px;font-family:sans-serif","EPSG:32633 band",[699,700,702,706,711],"g",{"fill":686,"style":701},"font-size:10px;font-family:sans-serif",[73,703,705],{"x":84,"y":704,"fill":686},"134","−180°",[73,707,710],{"x":708,"y":704,"style":709,"fill":686,"textAnchor":79},"376","text-anchor:middle","12°",[73,712,716],{"x":713,"y":704,"style":714,"fill":686,"textAnchor":715},"714","text-anchor:end","end","180°",[718,719],"circle",{"cx":694,"cy":98,"r":65,"fill":104},[73,721,724],{"x":694,"y":722,"style":723,"fill":104,"textAnchor":79},"72","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","A",[718,726],{"cx":727,"cy":98,"r":65,"fill":117},"452",[73,729,730],{"x":727,"y":722,"style":723,"fill":117,"textAnchor":79},"B",[718,732],{"cx":733,"cy":98,"r":65,"fill":734},"120","#b91c1c",[73,736,737],{"x":733,"y":722,"style":723,"fill":734,"textAnchor":79},"C",[49,739],{"x":681,"y":740,"width":130,"height":722,"rx":63,"fill":87,"stroke":104,"style":118},"158",[73,742,745],{"x":743,"y":744,"style":94,"fill":104,"textAnchor":79},"148","182","A · inside the band",[73,747,749],{"x":743,"y":748,"style":125,"fill":71,"textAnchor":79},"202","accurate to millimetres",[73,751,752],{"x":743,"y":154,"style":125,"fill":104,"textAnchor":79},"returns a point",[49,754],{"x":755,"y":740,"width":130,"height":722,"rx":63,"fill":87,"stroke":117,"style":118},"272",[73,757,758],{"x":75,"y":744,"style":94,"fill":117,"textAnchor":79},"B · just outside",[73,760,761],{"x":75,"y":748,"style":125,"fill":71,"textAnchor":79},"distortion grows with distance",[73,763,764],{"x":75,"y":154,"style":125,"fill":117,"textAnchor":79},"returns a point anyway",[49,766],{"x":767,"y":740,"width":130,"height":722,"rx":63,"fill":87,"stroke":734,"style":118},"504",[73,769,771],{"x":770,"y":744,"style":94,"fill":734,"textAnchor":79},"612","C · far outside",[73,773,774],{"x":770,"y":748,"style":125,"fill":71,"textAnchor":79},"no valid solution",[73,776,777],{"x":770,"y":154,"style":125,"fill":734,"textAnchor":79},"raises QgsCsException",[227,779,781],{"className":229,"code":780,"language":231,"meta":232,"style":232},"from qgis.core import QgsCsException\n\nconverted, failed = [], []\nfor feature in layer.getFeatures():\n    try:\n        converted.append(transform.transform(feature.geometry().asPoint()))\n    except QgsCsException as exc:\n        failed.append((feature.id(), str(exc)))\n\nprint(f\"{len(converted)} converted, {len(failed)} failed\")\nfor fid, message in failed[:5]:\n    print(f\"  feature {fid}: {message}\")\n",[234,782,783,794,798,808,818,826,831,845,856,860,893,910],{"__ignoreMap":232},[237,784,785,787,789,791],{"class":176,"line":239},[237,786,243],{"class":242},[237,788,247],{"class":246},[237,790,250],{"class":242},[237,792,793],{"class":246}," QgsCsException\n",[237,795,796],{"class":176,"line":256},[237,797,290],{"emptyLinePlaceholder":289},[237,799,800,803,805],{"class":176,"line":262},[237,801,802],{"class":246},"converted, failed ",[237,804,299],{"class":242},[237,806,807],{"class":246}," [], []\n",[237,809,810,812,814,816],{"class":176,"line":268},[237,811,512],{"class":242},[237,813,515],{"class":246},[237,815,518],{"class":242},[237,817,521],{"class":246},[237,819,820,823],{"class":176,"line":274},[237,821,822],{"class":242},"    try",[237,824,825],{"class":246},":\n",[237,827,828],{"class":176,"line":280},[237,829,830],{"class":246},"        converted.append(transform.transform(feature.geometry().asPoint()))\n",[237,832,833,836,839,842],{"class":176,"line":286},[237,834,835],{"class":242},"    except",[237,837,838],{"class":246}," QgsCsException ",[237,840,841],{"class":242},"as",[237,843,844],{"class":246}," exc:\n",[237,846,847,850,853],{"class":176,"line":293},[237,848,849],{"class":246},"        failed.append((feature.id(), ",[237,851,852],{"class":353},"str",[237,854,855],{"class":246},"(exc)))\n",[237,857,858],{"class":176,"line":311},[237,859,290],{"emptyLinePlaceholder":289},[237,861,862,864,866,868,870,873,876,878,881,883,886,888,891],{"class":176,"line":326},[237,863,384],{"class":353},[237,865,387],{"class":246},[237,867,390],{"class":242},[237,869,393],{"class":305},[237,871,872],{"class":353},"{len",[237,874,875],{"class":246},"(converted)",[237,877,405],{"class":353},[237,879,880],{"class":305}," converted, ",[237,882,872],{"class":353},[237,884,885],{"class":246},"(failed)",[237,887,405],{"class":353},[237,889,890],{"class":305}," failed\"",[237,892,283],{"class":246},[237,894,895,897,900,902,905,907],{"class":176,"line":337},[237,896,512],{"class":242},[237,898,899],{"class":246}," fid, message ",[237,901,518],{"class":242},[237,903,904],{"class":246}," failed[:",[237,906,64],{"class":353},[237,908,909],{"class":246},"]:\n",[237,911,912,915,917,919,922,924,927,929,932,934,937,939,941],{"class":176,"line":342},[237,913,914],{"class":353},"    print",[237,916,387],{"class":246},[237,918,390],{"class":242},[237,920,921],{"class":305},"\"  feature ",[237,923,396],{"class":353},[237,925,926],{"class":246},"fid",[237,928,405],{"class":353},[237,930,931],{"class":305},": ",[237,933,396],{"class":353},[237,935,936],{"class":246},"message",[237,938,405],{"class":353},[237,940,393],{"class":305},[237,942,283],{"class":246},[14,944,945,426,947,950,951,954,955,958,959,962,963,966],{},[209,946,425],{},[234,948,949],{},"QgsCsException"," is the specific exception PROJ failures surface as — catching bare ",[234,952,953],{},"Exception"," here would also swallow the ",[234,956,957],{},"TypeError"," from ",[234,960,961],{},"asPoint()"," on a non-point geometry, hiding a different bug. Collecting the failures rather than aborting means one bad row does not lose the other ten thousand, and reporting the first few with their IDs gives you something to investigate. Case B in the diagram is the dangerous one: it does ",[18,964,965],{},"not"," raise, so a point outside the zone silently returns a distorted coordinate — which is why choosing a projection appropriate to the data's extent matters more than error handling does.",[198,968,970],{"id":969},"transform-whole-geometries","Transform whole geometries",[14,972,973],{},"For anything beyond a point, transform the geometry rather than its vertices.",[227,975,977],{"className":229,"code":976,"language":231,"meta":232,"style":232},"from qgis.core import QgsGeometry\n\ngeometry = QgsGeometry(feature.geometry())\nif geometry.transform(transform) != 0:\n    raise RuntimeError(\"geometry transform failed\")\n",[234,978,979,990,994,1004,1020],{"__ignoreMap":232},[237,980,981,983,985,987],{"class":176,"line":239},[237,982,243],{"class":242},[237,984,247],{"class":246},[237,986,250],{"class":242},[237,988,989],{"class":246}," QgsGeometry\n",[237,991,992],{"class":176,"line":256},[237,993,290],{"emptyLinePlaceholder":289},[237,995,996,999,1001],{"class":176,"line":262},[237,997,998],{"class":246},"geometry ",[237,1000,299],{"class":242},[237,1002,1003],{"class":246}," QgsGeometry(feature.geometry())\n",[237,1005,1006,1009,1012,1015,1018],{"class":176,"line":268},[237,1007,1008],{"class":242},"if",[237,1010,1011],{"class":246}," geometry.transform(transform) ",[237,1013,1014],{"class":242},"!=",[237,1016,1017],{"class":353}," 0",[237,1019,825],{"class":246},[237,1021,1022,1025,1028,1030,1033],{"class":176,"line":274},[237,1023,1024],{"class":242},"    raise",[237,1026,1027],{"class":353}," RuntimeError",[237,1029,387],{"class":246},[237,1031,1032],{"class":305},"\"geometry transform failed\"",[237,1034,283],{"class":246},[14,1036,1037,426,1039,1042,1043,1046,1047,1049,1050,1053,1054,1057,1058,1060],{},[209,1038,425],{},[234,1040,1041],{},"QgsGeometry.transform()"," mutates the geometry ",[209,1044,1045],{},"in place"," and returns a status integer where ",[234,1048,51],{}," means success — the opposite convention from the point method, and a genuine trap. Copy-constructing with ",[234,1051,1052],{},"QgsGeometry(other)"," first means the feature's own geometry is not modified. For whole layers, ",[234,1055,1056],{},"native:reprojectlayer"," is the right tool and handles all of this internally; see ",[26,1059,29],{"href":28},".",[198,1062,1064],{"id":1063},"check-which-datum-path-was-chosen","Check which datum path was chosen",[14,1066,1067],{},"When two datums are related by more than one published transformation, PROJ picks one — and the choice can shift results by metres. Asking which path was used turns an invisible decision into a recorded one.",[14,1069,1070],{},[34,1071,1074,1077,1080,1083,1090,1093,1097,1103,1106,1109,1113,1116,1120,1123,1127,1130,1133,1137,1142,1147,1150],{"viewBox":1072,"role":37,"ariaLabel":1073,"xmlns":39},"0 0 760 246","Two available transformation paths between the same datum pair, one a coarse three-parameter shift and one a high-accuracy grid, producing results metres apart",[41,1075,1076],{},"Two paths between the same two datums",[45,1078,1079],{},"A source datum and a target datum are joined by two candidate transformations. A three-parameter Helmert shift is always available and accurate to a few metres. A national grid file is accurate to centimetres but only present if the grid has been installed. Choosing between them silently is what produces results that differ from a colleague's by metres.",[49,1081],{"x":51,"y":51,"width":52,"height":1082,"fill":54},"246",[56,1084,1085],{},[59,1086,1088],{"id":1087,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"crsPathArrow",[68,1089],{"d":70,"fill":71},[73,1091,1092],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"Same two datums, two answers metres apart",[49,1094],{"x":83,"y":1095,"width":166,"height":1096,"rx":132,"fill":87,"stroke":88,"style":118},"98","66",[73,1098,1102],{"x":1099,"y":1100,"style":1101,"fill":88,"textAnchor":79},"92","126","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","source datum",[73,1104,1105],{"x":1099,"y":743,"style":125,"fill":71,"textAnchor":79},"a national grid",[49,1107],{"x":1108,"y":1095,"width":166,"height":1096,"rx":132,"fill":87,"stroke":104,"style":118},"592",[73,1110,1112],{"x":1111,"y":1100,"style":1101,"fill":104,"textAnchor":79},"668","target datum",[73,1114,1115],{"x":1111,"y":743,"style":125,"fill":71,"textAnchor":79},"WGS84",[49,1117],{"x":1118,"y":84,"width":1119,"height":1096,"rx":132,"fill":87,"stroke":117,"style":118},"236","288",[73,1121,1122],{"x":75,"y":722,"style":94,"fill":117,"textAnchor":79},"3-parameter Helmert shift",[73,1124,1126],{"x":75,"y":1125,"style":125,"fill":71,"textAnchor":79},"94","always available · accurate to a few metres",[49,1128],{"x":1118,"y":1129,"width":1119,"height":1096,"rx":132,"fill":87,"stroke":104,"style":118},"150",[73,1131,1132],{"x":75,"y":169,"style":94,"fill":104,"textAnchor":79},"grid shift file",[73,1134,1136],{"x":75,"y":1135,"style":125,"fill":71,"textAnchor":79},"198","centimetre accuracy · only if installed",[176,1138],{"x1":1139,"y1":138,"x2":1140,"y2":98,"stroke":117,"style":1141},"168","232","stroke-width:2;marker-end:url(#crsPathArrow)",[176,1143],{"x1":1139,"y1":1144,"x2":1140,"y2":1145,"stroke":104,"style":1146},"144","180","stroke-width:2.5;marker-end:url(#crsPathArrow)",[176,1148],{"x1":152,"y1":98,"x2":1149,"y2":138,"stroke":117,"style":1141},"588",[176,1151],{"x1":152,"y1":1145,"x2":1149,"y2":1144,"stroke":104,"style":1146},[227,1153,1155],{"className":229,"code":1154,"language":231,"meta":232,"style":232},"from qgis.core import QgsDatumTransform\n\noperations = QgsDatumTransform.operations(source, target)\nfor op in operations:\n    print(f\"{'*' if op.isAvailable else ' '} {op.accuracy:>6.2f} m  {op.name}\")\nprint(\"in use:\", transform.coordinateOperation() or \"(PROJ default)\")\n",[234,1156,1157,1168,1172,1182,1194,1248],{"__ignoreMap":232},[237,1158,1159,1161,1163,1165],{"class":176,"line":239},[237,1160,243],{"class":242},[237,1162,247],{"class":246},[237,1164,250],{"class":242},[237,1166,1167],{"class":246}," QgsDatumTransform\n",[237,1169,1170],{"class":176,"line":256},[237,1171,290],{"emptyLinePlaceholder":289},[237,1173,1174,1177,1179],{"class":176,"line":262},[237,1175,1176],{"class":246},"operations ",[237,1178,299],{"class":242},[237,1180,1181],{"class":246}," QgsDatumTransform.operations(source, target)\n",[237,1183,1184,1186,1189,1191],{"class":176,"line":268},[237,1185,512],{"class":242},[237,1187,1188],{"class":246}," op ",[237,1190,518],{"class":242},[237,1192,1193],{"class":246}," operations:\n",[237,1195,1196,1198,1200,1202,1204,1206,1209,1212,1215,1218,1221,1223,1226,1229,1232,1234,1237,1239,1242,1244,1246],{"class":176,"line":274},[237,1197,914],{"class":353},[237,1199,387],{"class":246},[237,1201,390],{"class":242},[237,1203,393],{"class":305},[237,1205,396],{"class":353},[237,1207,1208],{"class":305},"'*'",[237,1210,1211],{"class":242}," if",[237,1213,1214],{"class":246}," op.isAvailable ",[237,1216,1217],{"class":242},"else",[237,1219,1220],{"class":305}," ' '",[237,1222,405],{"class":353},[237,1224,1225],{"class":353}," {",[237,1227,1228],{"class":246},"op.accuracy",[237,1230,1231],{"class":242},":>6.2f",[237,1233,405],{"class":353},[237,1235,1236],{"class":305}," m  ",[237,1238,396],{"class":353},[237,1240,1241],{"class":246},"op.name",[237,1243,405],{"class":353},[237,1245,393],{"class":305},[237,1247,283],{"class":246},[237,1249,1250,1252,1254,1257,1260,1263,1266],{"class":176,"line":280},[237,1251,384],{"class":353},[237,1253,387],{"class":246},[237,1255,1256],{"class":305},"\"in use:\"",[237,1258,1259],{"class":246},", transform.coordinateOperation() ",[237,1261,1262],{"class":242},"or",[237,1264,1265],{"class":305}," \"(PROJ default)\"",[237,1267,283],{"class":246},[14,1269,1270,426,1272,1275,1276,1279,1280,1283,1284,1287],{},[209,1271,425],{},[234,1273,1274],{},"QgsDatumTransform.operations()"," lists every published path between the two datums with its stated accuracy, and ",[234,1277,1278],{},"isAvailable"," is ",[234,1281,1282],{},"False"," for grid-based operations whose grid file is not installed — which is exactly the situation where PROJ silently falls back to a coarser path. ",[234,1285,1286],{},"coordinateOperation()"," returns the PROJ pipeline string actually in use, or an empty string when the default was taken. Logging that string alongside your results is what makes a discrepancy against someone else's numbers diagnosable rather than mysterious.",[198,1289,1291],{"id":1290},"qgis-version-compatibility","QGIS version compatibility",[14,1293,1294,1295,1297],{},"The examples target ",[209,1296,211],{}," (Python 3.12).",[1299,1300,1301,1317],"table",{},[1302,1303,1304],"thead",{},[1305,1306,1307,1311,1314],"tr",{},[1308,1309,1310],"th",{},"QGIS version",[1308,1312,1313],{},"Python",[1308,1315,1316],{},"Notes",[1318,1319,1320,1335,1346],"tbody",{},[1305,1321,1322,1326,1329],{},[1323,1324,1325],"td",{},"3.28 LTR",[1323,1327,1328],{},"3.9",[1323,1330,1331,1332,1334],{},"Identical API. ",[234,1333,141],{}," requires a context in all 3.x.",[1305,1336,1337,1340,1343],{},[1323,1338,1339],{},"3.34 LTR",[1323,1341,1342],{},"3.12",[1323,1344,1345],{},"Baseline for this page.",[1305,1347,1348,1351,1353],{},[1323,1349,1350],{},"3.40 \u002F 3.44",[1323,1352,1342],{},[1323,1354,1355,1358,1359,1362],{},[234,1356,1357],{},"Qgis.TransformDirection"," replaces ",[234,1360,1361],{},"QgsCoordinateTransform.ReverseTransform","; the old spelling still resolves.",[14,1364,1365],{},"The transform classes have been stable across the whole 3.x line.",[198,1367,1369],{"id":1368},"troubleshooting","Troubleshooting",[203,1371,1372,1381,1389,1397,1403,1412],{},[206,1373,1374,1377,1378,1380],{},[209,1375,1376],{},"The result is in the wrong hemisphere."," Latitude and longitude were passed in the wrong order. ",[234,1379,429],{}," takes x (longitude) first.",[206,1382,1383,1386,1387,1060],{},[209,1384,1385],{},"Coordinates are slightly off compared with the GUI."," No transform context was passed, so PROJ chose a different datum path. Pass ",[234,1388,443],{},[206,1390,1391,1396],{},[209,1392,1393,1395],{},[234,1394,949],{}," on some features."," Those coordinates fall outside the target projection's valid area. Choose a projection suited to the data's extent, or exclude the outliers.",[206,1398,1399,1402],{},[209,1400,1401],{},"The transform is very slow."," It is being constructed inside the loop. Build it once outside.",[206,1404,1405,1408,1409,1411],{},[209,1406,1407],{},"A round trip does not return the original coordinates."," Two separately constructed transforms may resolve different paths. Use ",[234,1410,652],{}," on the same object.",[206,1413,1414,1420,1421,1423],{},[209,1415,1416,1419],{},[234,1417,1418],{},"geometry.transform()"," seems to have done nothing."," It returns ",[234,1422,51],{}," on success and mutates in place — check the return value, and remember the geometry you passed has already changed.",[198,1425,1427],{"id":1426},"conclusion","Conclusion",[14,1429,1430,1431,1433],{},"Build the transform once with an explicit context, pass longitude before latitude, reuse the same object for the reverse direction, and catch ",[234,1432,949],{}," per feature so one out-of-domain coordinate does not lose the run. For anything larger than a handful of points, reproject the whole layer with a Processing algorithm instead.",[198,1435,1437],{"id":1436},"frequently-asked-questions","Frequently Asked Questions",[14,1439,1440,1443,1444,1447],{},[209,1441,1442],{},"Why do my transformed coordinates land in the wrong place?","\nAlmost always the argument order. ",[234,1445,1446],{},"QgsPointXY(x, y)"," takes longitude first and latitude second, which is the reverse of how coordinates are usually spoken and written.",[14,1449,1450,1453],{},[209,1451,1452],{},"Do I really need to pass a transform context?","\nYes. It tells PROJ which datum transformation path to use when several exist, so a scripted result matches what the GUI produces. Without it, sub-metre shifts can appear silently.",[14,1455,1456,1459],{},[209,1457,1458],{},"Why does my transform raise QgsCsException?","\nThe coordinate falls outside the target projection's valid area — a point from another continent fed to a UTM zone, for example. Catch it per feature and either exclude the point or pick a projection appropriate to the data's extent.",[14,1461,1462,1465,1466,1469],{},[209,1463,1464],{},"How do I transform in the opposite direction?","\nCall ",[234,1467,1468],{},"transform.transform(point, QgsCoordinateTransform.ReverseTransform)"," on the same object rather than constructing a swapped one. Reusing it guarantees the same datum path, so a round trip returns the original coordinates.",[14,1471,1472,1475,1476,1478],{},[209,1473,1474],{},"Is it faster to transform a whole geometry or its points one at a time?","\nThe whole geometry, substantially. ",[234,1477,1041],{}," walks the coordinates in C++ with one Python call, whereas transforming vertices individually crosses the language boundary once per point.",[14,1480,1481,1484],{},[209,1482,1483],{},"Can one transform object be reused across threads?","\nTreat it as single-threaded. Build a separate transform per worker rather than sharing one.",[14,1486,1487,1490],{},[209,1488,1489],{},"Does the transform cache anything?","\nYes — the resolved pipeline, which is why reuse is fast.",[14,1492,1493,1496],{},[209,1494,1495],{},"Is EPSG:4326 the same as WGS84?","\nEffectively yes for most purposes, though WGS84 has been realised several times and the differences are at the centimetre level.",[198,1498,1500],{"id":1499},"related","Related",[203,1502,1503,1508,1514,1520],{},[206,1504,1505,1507],{},[26,1506,29],{"href":28}," — the guide this recipe belongs to",[206,1509,1510],{},[26,1511,1513],{"href":1512},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fhandling-missing-crs-in-pyqgis\u002F","Handling Missing CRS in PyQGIS",[206,1515,1516],{},[26,1517,1519],{"href":1518},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fbatch-reprojecting-raster-datasets\u002F","Batch Reprojecting Raster Datasets in PyQGIS",[206,1521,1522],{},[26,1523,1525],{"href":1524},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fcapture-map-click-coordinates-pyqgis\u002F","Capture Map Click Coordinates in PyQGIS",[1527,1528,1529],"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 pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}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":232,"searchDepth":256,"depth":256,"links":1531},[1532,1533,1534,1535,1536,1537,1538,1539,1540,1541,1542,1543],{"id":200,"depth":256,"text":201},{"id":221,"depth":256,"text":222},{"id":447,"depth":256,"text":448},{"id":576,"depth":256,"text":577},{"id":656,"depth":256,"text":657},{"id":969,"depth":256,"text":970},{"id":1063,"depth":256,"text":1064},{"id":1290,"depth":256,"text":1291},{"id":1368,"depth":256,"text":1369},{"id":1426,"depth":256,"text":1427},{"id":1436,"depth":256,"text":1437},{"id":1499,"depth":256,"text":1500},"Convert coordinates between CRSs in PyQGIS with QgsCoordinateTransform. Reuse the transform, handle QgsCsException, invert the direction, and transform in bulk.","md",{"slug":1547,"type":1548,"breadcrumb":1549,"datePublished":1550,"dateModified":1550},"transform-point-coordinates-pyqgis","article","Transform Point Coordinates","2026-08-01","\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Ftransform-point-coordinates-pyqgis",{"title":5,"description":1544},"spatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Ftransform-point-coordinates-pyqgis\u002Findex","SAk01Z7-nU88d_DVs-zW8Xyhr7ioWdNvwUGtxyOhsSM",1785585990476]