[{"data":1,"prerenderedAt":1818},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fload-csv-as-point-layer-pyqgis":3},{"id":4,"title":5,"body":6,"description":1807,"extension":1808,"meta":1809,"navigation":299,"path":1814,"seo":1815,"stem":1816,"__hash__":1817},"docs\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fload-csv-as-point-layer-pyqgis\u002Findex.md","Load a CSV as a Point Layer in PyQGIS",{"type":7,"value":8,"toc":1794},"minimark",[9,13,26,35,208,213,230,234,241,599,628,634,742,746,749,880,893,896,900,903,1017,1049,1052,1146,1290,1311,1315,1322,1399,1419,1434,1438,1441,1540,1557,1561,1594,1598,1682,1686,1689,1693,1715,1721,1746,1755,1759,1790],[10,11,5],"h1",{"id":12},"load-a-csv-as-a-point-layer-in-pyqgis",[14,15,16,17,21,22,25],"p",{},"Sooner or later somebody hands you a spreadsheet export with a latitude column and a longitude column and asks for a map. QGIS reads that file directly through the ",[18,19,20],"code",{},"delimitedtext"," provider, and the whole job is a single ",[18,23,24],{},"QgsVectorLayer"," call — provided the URI says exactly what the file contains. Leave out the CRS and the points land off the coast of Africa; leave out the type detection and every numeric column arrives as text; forget which character separates the decimals and half the rows silently lose their geometry.",[14,27,28,29,34],{},"This recipe belongs to ",[30,31,33],"a",{"href":32},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002F","Layer Data Sources & Formats in PyQGIS",". It builds the URI piece by piece, covers WKT columns and awkward regional formats, and shows how to count the rows that did not become points.",[14,36,37],{},[38,39,44,48,52,59,76,85,95,101,105,109,114,119,123,126,129,132,137,141,144,147,150,156,160,163,167,171,178,182,185,188,197,203],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 300","img","A delimited text URI broken into its parts: the file path, the parsing options, the geometry columns and the CRS, each feeding the provider that produces a point layer","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"Anatomy of a delimitedtext URI",[49,50,51],"desc",{},"The URI begins with a file URL, then query parameters. Parsing options such as type, delimiter and detectTypes control how rows are read. Geometry options name the x and y fields or a WKT field. The crs parameter says what the numbers mean. All of them feed the delimitedtext provider, which yields a point layer.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","300","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"csvUriArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Four questions the URI has to answer",[53,86],{"x":87,"y":88,"width":89,"height":90,"rx":91,"fill":92,"stroke":93,"style":94},"24","52","160","112","10","#fffdf7","#59645f","stroke-width:2",[77,96,100],{"x":97,"y":98,"style":99,"fill":82,"textAnchor":83},"104","78","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","where is it?",[77,102,104],{"x":97,"y":97,"style":103,"fill":75,"textAnchor":83},"text-anchor:middle;font-size:10px;font-family:monospace","file:\u002F\u002F\u002Fdata\u002F",[77,106,108],{"x":97,"y":107,"style":103,"fill":75,"textAnchor":83},"122","sites.csv",[77,110,113],{"x":97,"y":111,"style":112,"fill":93,"textAnchor":83},"148","text-anchor:middle;font-size:9.5px;font-family:sans-serif","a URL, not a path",[53,115],{"x":116,"y":88,"width":89,"height":90,"rx":91,"fill":117,"stroke":118,"style":94},"204","#eef7f4","#0f766e",[77,120,122],{"x":121,"y":98,"style":99,"fill":118,"textAnchor":83},"284","how to parse?",[77,124,125],{"x":121,"y":97,"style":103,"fill":75,"textAnchor":83},"type=csv",[77,127,128],{"x":121,"y":107,"style":103,"fill":75,"textAnchor":83},"detectTypes=yes",[77,130,131],{"x":121,"y":111,"style":112,"fill":93,"textAnchor":83},"delimiter, decimals",[53,133],{"x":134,"y":88,"width":89,"height":90,"rx":91,"fill":135,"stroke":136,"style":94},"384","#eff3ff","#2563eb",[77,138,140],{"x":139,"y":98,"style":99,"fill":136,"textAnchor":83},"464","which columns?",[77,142,143],{"x":139,"y":97,"style":103,"fill":75,"textAnchor":83},"xField=lon",[77,145,146],{"x":139,"y":107,"style":103,"fill":75,"textAnchor":83},"yField=lat",[77,148,149],{"x":139,"y":111,"style":112,"fill":93,"textAnchor":83},"or wktField=geom",[53,151],{"x":152,"y":88,"width":153,"height":90,"rx":91,"fill":154,"stroke":155,"style":94},"564","172","#fdf2e2","#b45309",[77,157,159],{"x":158,"y":98,"style":99,"fill":155,"textAnchor":83},"650","what do they mean?",[77,161,162],{"x":158,"y":97,"style":103,"fill":75,"textAnchor":83},"crs=EPSG:4326",[77,164,166],{"x":158,"y":165,"style":112,"fill":93,"textAnchor":83},"130","omit it and QGIS",[77,168,170],{"x":158,"y":169,"style":112,"fill":93,"textAnchor":83},"146","guesses the project CRS",[172,173],"line",{"x1":97,"y1":174,"x2":175,"y2":176,"stroke":75,"style":177},"164","340","212","stroke-width:1.6;marker-end:url(#csvUriArrow)",[172,179],{"x1":121,"y1":174,"x2":180,"y2":181,"stroke":75,"style":177},"360","208",[172,183],{"x1":139,"y1":174,"x2":184,"y2":181,"stroke":75,"style":177},"400",[172,186],{"x1":158,"y1":174,"x2":187,"y2":176,"stroke":75,"style":177},"420",[53,189],{"x":190,"y":191,"width":192,"height":193,"rx":91,"fill":194,"stroke":195,"style":196},"250","216","260","62","#e8efe6","#15803d","stroke-width:2.5",[77,198,202],{"x":79,"y":199,"style":200,"fill":201,"textAnchor":83},"242","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","#166534","delimitedtext provider",[77,204,207],{"x":79,"y":205,"style":206,"fill":75,"textAnchor":83},"264","text-anchor:middle;font-size:10.5px;font-family:sans-serif","a read-only point layer over the file",[209,210,212],"h2",{"id":211},"prerequisites","Prerequisites",[214,215,216,224,227],"ul",{},[217,218,219,223],"li",{},[220,221,222],"strong",{},"QGIS 3.40 LTR"," or newer; everything here also runs unchanged on the QGIS 4 series.",[217,225,226],{},"A CSV with a header row and either two coordinate columns or one WKT column.",[217,228,229],{},"Knowing what coordinate system the numbers are in. If nobody can tell you, look at the value ranges: longitudes between −180 and 180 with several decimal places are almost certainly WGS 84; six- or seven-digit integers are projected metres.",[209,231,233],{"id":232},"build-the-uri-and-load-the-layer","Build the URI and load the layer",[14,235,236,237,240],{},"The provider reads the file lazily, so constructing the layer is cheap and the first real work happens on the first iteration or render. The URI is a ",[18,238,239],{},"file:\u002F\u002F"," URL followed by query parameters, and assembling it from a dictionary keeps it readable and correctly escaped.",[242,243,248],"pre",{"className":244,"code":245,"language":246,"meta":247,"style":247},"language-python shiki shiki-themes github-dark","from pathlib import Path\nfrom urllib.parse import urlencode\nfrom qgis.core import QgsVectorLayer, QgsProject\n\ncsv_path = Path(\"\u002Fdata\u002Finbox\u002Fsensor_sites.csv\")\n\nparams = {\n    \"type\": \"csv\",\n    \"delimiter\": \",\",\n    \"detectTypes\": \"yes\",\n    \"xField\": \"longitude\",\n    \"yField\": \"latitude\",\n    \"crs\": \"EPSG:4326\",\n    \"spatialIndex\": \"yes\",\n    \"subsetIndex\": \"no\",\n    \"watchFile\": \"no\",\n}\nuri = f\"{csv_path.as_uri()}?{urlencode(params)}\"\n\nsites = QgsVectorLayer(uri, \"sensor_sites\", \"delimitedtext\")\nif not sites.isValid():\n    raise RuntimeError(f\"could not open {csv_path}: {sites.error().summary()}\")\n\nQgsProject.instance().addMapLayer(sites)\nprint(sites.featureCount(), \"rows,\", sites.fields().names())\n","python","",[18,249,250,268,281,294,301,320,325,336,351,364,377,390,403,416,428,441,453,459,496,501,523,535,573,578,584],{"__ignoreMap":247},[251,252,254,258,262,265],"span",{"class":172,"line":253},1,[251,255,257],{"class":256},"snl16","from",[251,259,261],{"class":260},"s95oV"," pathlib ",[251,263,264],{"class":256},"import",[251,266,267],{"class":260}," Path\n",[251,269,271,273,276,278],{"class":172,"line":270},2,[251,272,257],{"class":256},[251,274,275],{"class":260}," urllib.parse ",[251,277,264],{"class":256},[251,279,280],{"class":260}," urlencode\n",[251,282,284,286,289,291],{"class":172,"line":283},3,[251,285,257],{"class":256},[251,287,288],{"class":260}," qgis.core ",[251,290,264],{"class":256},[251,292,293],{"class":260}," QgsVectorLayer, QgsProject\n",[251,295,297],{"class":172,"line":296},4,[251,298,300],{"emptyLinePlaceholder":299},true,"\n",[251,302,304,307,310,313,317],{"class":172,"line":303},5,[251,305,306],{"class":260},"csv_path ",[251,308,309],{"class":256},"=",[251,311,312],{"class":260}," Path(",[251,314,316],{"class":315},"sU2Wk","\"\u002Fdata\u002Finbox\u002Fsensor_sites.csv\"",[251,318,319],{"class":260},")\n",[251,321,323],{"class":172,"line":322},6,[251,324,300],{"emptyLinePlaceholder":299},[251,326,328,331,333],{"class":172,"line":327},7,[251,329,330],{"class":260},"params ",[251,332,309],{"class":256},[251,334,335],{"class":260}," {\n",[251,337,339,342,345,348],{"class":172,"line":338},8,[251,340,341],{"class":315},"    \"type\"",[251,343,344],{"class":260},": ",[251,346,347],{"class":315},"\"csv\"",[251,349,350],{"class":260},",\n",[251,352,354,357,359,362],{"class":172,"line":353},9,[251,355,356],{"class":315},"    \"delimiter\"",[251,358,344],{"class":260},[251,360,361],{"class":315},"\",\"",[251,363,350],{"class":260},[251,365,367,370,372,375],{"class":172,"line":366},10,[251,368,369],{"class":315},"    \"detectTypes\"",[251,371,344],{"class":260},[251,373,374],{"class":315},"\"yes\"",[251,376,350],{"class":260},[251,378,380,383,385,388],{"class":172,"line":379},11,[251,381,382],{"class":315},"    \"xField\"",[251,384,344],{"class":260},[251,386,387],{"class":315},"\"longitude\"",[251,389,350],{"class":260},[251,391,393,396,398,401],{"class":172,"line":392},12,[251,394,395],{"class":315},"    \"yField\"",[251,397,344],{"class":260},[251,399,400],{"class":315},"\"latitude\"",[251,402,350],{"class":260},[251,404,406,409,411,414],{"class":172,"line":405},13,[251,407,408],{"class":315},"    \"crs\"",[251,410,344],{"class":260},[251,412,413],{"class":315},"\"EPSG:4326\"",[251,415,350],{"class":260},[251,417,419,422,424,426],{"class":172,"line":418},14,[251,420,421],{"class":315},"    \"spatialIndex\"",[251,423,344],{"class":260},[251,425,374],{"class":315},[251,427,350],{"class":260},[251,429,431,434,436,439],{"class":172,"line":430},15,[251,432,433],{"class":315},"    \"subsetIndex\"",[251,435,344],{"class":260},[251,437,438],{"class":315},"\"no\"",[251,440,350],{"class":260},[251,442,444,447,449,451],{"class":172,"line":443},16,[251,445,446],{"class":315},"    \"watchFile\"",[251,448,344],{"class":260},[251,450,438],{"class":315},[251,452,350],{"class":260},[251,454,456],{"class":172,"line":455},17,[251,457,458],{"class":260},"}\n",[251,460,462,465,467,470,473,477,480,483,486,488,491,493],{"class":172,"line":461},18,[251,463,464],{"class":260},"uri ",[251,466,309],{"class":256},[251,468,469],{"class":256}," f",[251,471,472],{"class":315},"\"",[251,474,476],{"class":475},"sDLfK","{",[251,478,479],{"class":260},"csv_path.as_uri()",[251,481,482],{"class":475},"}",[251,484,485],{"class":315},"?",[251,487,476],{"class":475},[251,489,490],{"class":260},"urlencode(params)",[251,492,482],{"class":475},[251,494,495],{"class":315},"\"\n",[251,497,499],{"class":172,"line":498},19,[251,500,300],{"emptyLinePlaceholder":299},[251,502,504,507,509,512,515,518,521],{"class":172,"line":503},20,[251,505,506],{"class":260},"sites ",[251,508,309],{"class":256},[251,510,511],{"class":260}," QgsVectorLayer(uri, ",[251,513,514],{"class":315},"\"sensor_sites\"",[251,516,517],{"class":260},", ",[251,519,520],{"class":315},"\"delimitedtext\"",[251,522,319],{"class":260},[251,524,526,529,532],{"class":172,"line":525},21,[251,527,528],{"class":256},"if",[251,530,531],{"class":256}," not",[251,533,534],{"class":260}," sites.isValid():\n",[251,536,538,541,544,547,550,553,555,558,560,562,564,567,569,571],{"class":172,"line":537},22,[251,539,540],{"class":256},"    raise",[251,542,543],{"class":475}," RuntimeError",[251,545,546],{"class":260},"(",[251,548,549],{"class":256},"f",[251,551,552],{"class":315},"\"could not open ",[251,554,476],{"class":475},[251,556,557],{"class":260},"csv_path",[251,559,482],{"class":475},[251,561,344],{"class":315},[251,563,476],{"class":475},[251,565,566],{"class":260},"sites.error().summary()",[251,568,482],{"class":475},[251,570,472],{"class":315},[251,572,319],{"class":260},[251,574,576],{"class":172,"line":575},23,[251,577,300],{"emptyLinePlaceholder":299},[251,579,581],{"class":172,"line":580},24,[251,582,583],{"class":260},"QgsProject.instance().addMapLayer(sites)\n",[251,585,587,590,593,596],{"class":172,"line":586},25,[251,588,589],{"class":475},"print",[251,591,592],{"class":260},"(sites.featureCount(), ",[251,594,595],{"class":315},"\"rows,\"",[251,597,598],{"class":260},", sites.fields().names())\n",[14,600,601,604,605,608,609,612,613,615,616,619,620,623,624,627],{},[220,602,603],{},"Breakdown:"," ",[18,606,607],{},"Path.as_uri()"," produces a correctly formed ",[18,610,611],{},"file:\u002F\u002F\u002F"," URL on every platform, including the drive letter and forward slashes Windows needs, which is the part people usually get wrong by hand. ",[18,614,128],{}," makes the provider scan the column contents and assign integer, double, date or string types; without it every field is a string and a later ",[18,617,618],{},"\"population\" > 1000"," expression compares text. ",[18,621,622],{},"spatialIndex=yes"," builds an in-memory index when the layer opens, which is what keeps panning and spatial filters fast on a file with tens of thousands of rows. ",[18,625,626],{},"watchFile=no"," stops QGIS reloading the layer whenever another program touches the file — useful in an editor, a nuisance in a script.",[14,629,630,631,633],{},"A ",[18,632,20],{}," layer is read-only. You can style it, filter it and feed it to Processing, but you cannot add a field or edit a value; the file itself is the source of truth, and edits belong either in the spreadsheet or in a copy.",[14,635,636],{},[38,637,640,643,646,649,656,660,664,670,675,678,683,687,690,694,699,703,707,712,715,719,723,726,730,734,738],{"viewBox":638,"role":41,"ariaLabel":639,"xmlns":43},"0 0 760 290","How each CSV row becomes a feature or is dropped: rows with valid numbers get a point, rows with empty or unparsable coordinates keep their attributes but get no geometry",[45,641,642],{},"Rows in, features out",[49,644,645],{},"Five sample rows pass through the provider. Three have numeric coordinates and become point features. One has an empty latitude and one uses a decimal comma the URI did not declare; both become features with a null geometry rather than being rejected, so the feature count still matches the row count.",[53,647],{"x":55,"y":55,"width":56,"height":648,"fill":58},"290",[60,650,651],{},[63,652,654],{"id":653,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"csvRowArrow",[72,655],{"d":74,"fill":75},[77,657,659],{"x":79,"y":658,"style":81,"fill":82,"textAnchor":83},"26","A bad row is not rejected — it loses its geometry",[53,661],{"x":87,"y":662,"width":57,"height":663,"rx":91,"fill":92,"stroke":93,"style":94},"44","226",[77,665,669],{"x":666,"y":667,"style":668,"fill":82,"textAnchor":83},"174","68","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","sensor_sites.csv",[77,671,674],{"x":662,"y":672,"style":673,"fill":75},"98","font-size:10px;font-family:monospace","S01, -1.5491, 53.8008",[77,676,677],{"x":662,"y":165,"style":673,"fill":75},"S02, -1.5520, 53.7962",[77,679,682],{"x":662,"y":680,"style":673,"fill":681},"162","#b91c1c","S03, -1.5433,",[77,684,686],{"x":662,"y":685,"style":673,"fill":681},"194","S04, \"-1,5402\", \"53,8011\"",[77,688,689],{"x":662,"y":663,"style":673,"fill":75},"S05, -1.5389, 53.7990",[77,691,693],{"x":666,"y":692,"style":112,"fill":93,"textAnchor":83},"256","5 rows",[172,695],{"x1":696,"y1":697,"x2":187,"y2":697,"stroke":75,"style":698},"330","157","stroke-width:2;marker-end:url(#csvRowArrow)",[53,700],{"x":701,"y":662,"width":702,"height":97,"rx":91,"fill":194,"stroke":195,"style":94},"430","306",[77,704,706],{"x":705,"y":667,"style":668,"fill":201,"textAnchor":83},"583","3 features with a point",[708,709],"circle",{"cx":710,"cy":711,"r":69,"fill":195},"500","108",[708,713],{"cx":705,"cy":714,"r":69,"fill":195},"100",[708,716],{"cx":717,"cy":718,"r":69,"fill":195},"666","114",[77,720,722],{"x":705,"y":721,"style":112,"fill":75,"textAnchor":83},"136","S01, S02, S05",[53,724],{"x":701,"y":725,"width":702,"height":97,"rx":91,"fill":154,"stroke":681,"style":94},"166",[77,727,729],{"x":705,"y":728,"style":668,"fill":681,"textAnchor":83},"190","2 features, null geometry",[77,731,733],{"x":705,"y":191,"style":732,"fill":75,"textAnchor":83},"text-anchor:middle;font-size:10px;font-family:sans-serif","S03 empty latitude",[77,735,737],{"x":705,"y":736,"style":732,"fill":75,"textAnchor":83},"236","S04 decimal comma not declared",[77,739,741],{"x":705,"y":740,"style":112,"fill":93,"textAnchor":83},"258","invisible on the map, present in the table",[209,743,745],{"id":744},"count-the-rows-that-did-not-become-points","Count the rows that did not become points",[14,747,748],{},"The provider does not fail on a bad row. It creates the feature, fills in the attributes, and leaves the geometry empty. The feature count matches the row count, the layer is valid, and the missing points only show up when somebody asks why a site is absent from the map. Check for them immediately after loading.",[242,750,752],{"className":244,"code":751,"language":246,"meta":247,"style":247},"from qgis.core import QgsFeatureRequest\n\nrequest = QgsFeatureRequest().setFilterExpression(\"$geometry IS NULL\")\n\nmissing = [f[\"site_id\"] for f in sites.getFeatures(request)]\nprint(f\"{len(missing)} of {sites.featureCount()} rows have no geometry\")\nfor site_id in missing[:20]:\n    print(\"  \", site_id)\n",[18,753,754,765,769,784,788,816,849,867],{"__ignoreMap":247},[251,755,756,758,760,762],{"class":172,"line":253},[251,757,257],{"class":256},[251,759,288],{"class":260},[251,761,264],{"class":256},[251,763,764],{"class":260}," QgsFeatureRequest\n",[251,766,767],{"class":172,"line":270},[251,768,300],{"emptyLinePlaceholder":299},[251,770,771,774,776,779,782],{"class":172,"line":283},[251,772,773],{"class":260},"request ",[251,775,309],{"class":256},[251,777,778],{"class":260}," QgsFeatureRequest().setFilterExpression(",[251,780,781],{"class":315},"\"$geometry IS NULL\"",[251,783,319],{"class":260},[251,785,786],{"class":172,"line":296},[251,787,300],{"emptyLinePlaceholder":299},[251,789,790,793,795,798,801,804,807,810,813],{"class":172,"line":303},[251,791,792],{"class":260},"missing ",[251,794,309],{"class":256},[251,796,797],{"class":260}," [f[",[251,799,800],{"class":315},"\"site_id\"",[251,802,803],{"class":260},"] ",[251,805,806],{"class":256},"for",[251,808,809],{"class":260}," f ",[251,811,812],{"class":256},"in",[251,814,815],{"class":260}," sites.getFeatures(request)]\n",[251,817,818,820,822,824,826,829,832,834,837,839,842,844,847],{"class":172,"line":322},[251,819,589],{"class":475},[251,821,546],{"class":260},[251,823,549],{"class":256},[251,825,472],{"class":315},[251,827,828],{"class":475},"{len",[251,830,831],{"class":260},"(missing)",[251,833,482],{"class":475},[251,835,836],{"class":315}," of ",[251,838,476],{"class":475},[251,840,841],{"class":260},"sites.featureCount()",[251,843,482],{"class":475},[251,845,846],{"class":315}," rows have no geometry\"",[251,848,319],{"class":260},[251,850,851,853,856,858,861,864],{"class":172,"line":327},[251,852,806],{"class":256},[251,854,855],{"class":260}," site_id ",[251,857,812],{"class":256},[251,859,860],{"class":260}," missing[:",[251,862,863],{"class":475},"20",[251,865,866],{"class":260},"]:\n",[251,868,869,872,874,877],{"class":172,"line":338},[251,870,871],{"class":475},"    print",[251,873,546],{"class":260},[251,875,876],{"class":315},"\"  \"",[251,878,879],{"class":260},", site_id)\n",[14,881,882,884,885,888,889,892],{},[220,883,603],{}," Filtering on ",[18,886,887],{},"$geometry IS NULL"," asks the expression engine rather than Python to find the empty rows, so the loop only touches the ones you want. Do not add the ",[18,890,891],{},"NoGeometry"," flag to this particular request, tempting as it is for speed — without geometry fetched, every feature looks empty and the filter matches all of them. Printing the business identifier rather than the feature id matters because delimited text feature ids are row numbers, and row numbers shift the moment somebody sorts the spreadsheet.",[14,894,895],{},"Two causes account for almost every empty geometry: a blank coordinate cell, and a number the provider could not parse. The second one has a fix in the URI.",[209,897,899],{"id":898},"regional-formats-wkt-and-awkward-files","Regional formats, WKT and awkward files",[14,901,902],{},"European exports often use a semicolon as the delimiter and a comma as the decimal separator. The provider handles both, but only if told.",[242,904,906],{"className":244,"code":905,"language":246,"meta":247,"style":247},"params.update({\n    \"delimiter\": \";\",\n    \"decimalPoint\": \",\",\n    \"encoding\": \"Windows-1252\",\n    \"skipLines\": \"2\",\n    \"trimFields\": \"yes\",\n})\nuri = f\"{csv_path.as_uri()}?{urlencode(params)}\"\nsites = QgsVectorLayer(uri, \"sensor_sites\", \"delimitedtext\")\n",[18,907,908,913,924,935,947,959,970,975,1001],{"__ignoreMap":247},[251,909,910],{"class":172,"line":253},[251,911,912],{"class":260},"params.update({\n",[251,914,915,917,919,922],{"class":172,"line":270},[251,916,356],{"class":315},[251,918,344],{"class":260},[251,920,921],{"class":315},"\";\"",[251,923,350],{"class":260},[251,925,926,929,931,933],{"class":172,"line":283},[251,927,928],{"class":315},"    \"decimalPoint\"",[251,930,344],{"class":260},[251,932,361],{"class":315},[251,934,350],{"class":260},[251,936,937,940,942,945],{"class":172,"line":296},[251,938,939],{"class":315},"    \"encoding\"",[251,941,344],{"class":260},[251,943,944],{"class":315},"\"Windows-1252\"",[251,946,350],{"class":260},[251,948,949,952,954,957],{"class":172,"line":303},[251,950,951],{"class":315},"    \"skipLines\"",[251,953,344],{"class":260},[251,955,956],{"class":315},"\"2\"",[251,958,350],{"class":260},[251,960,961,964,966,968],{"class":172,"line":322},[251,962,963],{"class":315},"    \"trimFields\"",[251,965,344],{"class":260},[251,967,374],{"class":315},[251,969,350],{"class":260},[251,971,972],{"class":172,"line":327},[251,973,974],{"class":260},"})\n",[251,976,977,979,981,983,985,987,989,991,993,995,997,999],{"class":172,"line":338},[251,978,464],{"class":260},[251,980,309],{"class":256},[251,982,469],{"class":256},[251,984,472],{"class":315},[251,986,476],{"class":475},[251,988,479],{"class":260},[251,990,482],{"class":475},[251,992,485],{"class":315},[251,994,476],{"class":475},[251,996,490],{"class":260},[251,998,482],{"class":475},[251,1000,495],{"class":315},[251,1002,1003,1005,1007,1009,1011,1013,1015],{"class":172,"line":353},[251,1004,506],{"class":260},[251,1006,309],{"class":256},[251,1008,511],{"class":260},[251,1010,514],{"class":315},[251,1012,517],{"class":260},[251,1014,520],{"class":315},[251,1016,319],{"class":260},[14,1018,1019,604,1021,1024,1025,1028,1029,1032,1033,1036,1037,1040,1041,1044,1045,1048],{},[220,1020,603],{},[18,1022,1023],{},"decimalPoint=,"," applies only to the coordinate columns and to type detection, so ",[18,1026,1027],{},"53,8011"," becomes a number instead of a failed parse. ",[18,1030,1031],{},"encoding"," matters for place names: a file written by Excel on a Western European Windows machine is usually Windows-1252, and reading it as UTF-8 turns every accented character into a replacement glyph. ",[18,1034,1035],{},"skipLines"," drops title rows some exports put above the real header, and ",[18,1038,1039],{},"trimFields"," removes the padding spaces that otherwise make ",[18,1042,1043],{},"\" S01\""," and ",[18,1046,1047],{},"\"S01\""," different values in a join.",[14,1050,1051],{},"When the geometry is not a point — a boundary exported from a database, or a line drawn in another tool — the file usually carries it as Well-Known Text in a single column. Swap the coordinate parameters for a WKT field and tell the provider what shape to expect.",[14,1053,1054],{},[38,1055,1058,1061,1064,1067,1074,1077,1080,1084,1091,1093,1097,1101,1105,1109,1113,1117,1119,1122,1125,1128,1131,1134,1137,1140,1143],{"viewBox":1056,"role":41,"ariaLabel":1057,"xmlns":43},"0 0 760 270","Choosing between coordinate columns and a WKT column: point data with separate x and y fields uses xField and yField, anything else or mixed data uses wktField with an explicit geomType",[45,1059,1060],{},"Coordinates or WKT?",[49,1062,1063],{},"A decision between two URI styles. If the file has separate x and y numeric columns, use xField and yField and the layer is always points. If the file has a single text column of WKT, use wktField, and set geomType to point, line or polygon so the provider does not guess from the first rows. Files with no geometry at all use geomType none and load as a table.",[53,1065],{"x":55,"y":55,"width":56,"height":1066,"fill":58},"270",[60,1068,1069],{},[63,1070,1072],{"id":1071,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"csvWktArrow",[72,1073],{"d":74,"fill":75},[77,1075,1076],{"x":79,"y":658,"style":81,"fill":82,"textAnchor":83},"Pick the geometry parameters from the file, not the goal",[53,1078],{"x":1066,"y":662,"width":1079,"height":662,"rx":67,"fill":92,"stroke":93,"style":94},"220",[77,1081,1083],{"x":79,"y":1082,"style":668,"fill":82,"textAnchor":83},"71","what holds the geometry?",[172,1085],{"x1":1086,"y1":1087,"x2":1088,"y2":1089,"stroke":75,"style":1090},"310","88","140","128","stroke-width:1.8;marker-end:url(#csvWktArrow)",[172,1092],{"x1":79,"y1":1087,"x2":79,"y2":1089,"stroke":75,"style":1090},[172,1094],{"x1":1095,"y1":1087,"x2":1096,"y2":1089,"stroke":75,"style":1090},"450","620",[53,1098],{"x":1099,"y":1100,"width":1079,"height":90,"rx":91,"fill":194,"stroke":195,"style":94},"30","134",[77,1102,1104],{"x":1088,"y":1103,"style":668,"fill":201,"textAnchor":83},"158","two number columns",[77,1106,1108],{"x":1088,"y":1107,"style":103,"fill":75,"textAnchor":83},"184","xField + yField",[77,1110,1112],{"x":1088,"y":1111,"style":103,"fill":75,"textAnchor":83},"206","zField optional",[77,1114,1116],{"x":1088,"y":1115,"style":112,"fill":93,"textAnchor":83},"230","always a point layer",[53,1118],{"x":1066,"y":1100,"width":1079,"height":90,"rx":91,"fill":135,"stroke":136,"style":94},[77,1120,1121],{"x":79,"y":1103,"style":668,"fill":136,"textAnchor":83},"one WKT text column",[77,1123,1124],{"x":79,"y":1107,"style":103,"fill":75,"textAnchor":83},"wktField=geom",[77,1126,1127],{"x":79,"y":1111,"style":103,"fill":75,"textAnchor":83},"geomType=polygon",[77,1129,1130],{"x":79,"y":1115,"style":112,"fill":93,"textAnchor":83},"state the type, never guess",[53,1132],{"x":1133,"y":1100,"width":1079,"height":90,"rx":91,"fill":92,"stroke":93,"style":94},"510",[77,1135,1136],{"x":1096,"y":1103,"style":668,"fill":82,"textAnchor":83},"no geometry at all",[77,1138,1139],{"x":1096,"y":1107,"style":103,"fill":75,"textAnchor":83},"geomType=none",[77,1141,1142],{"x":1096,"y":181,"style":112,"fill":93,"textAnchor":83},"loads as a plain table,",[77,1144,1145],{"x":1096,"y":663,"style":112,"fill":93,"textAnchor":83},"ready for a join",[242,1147,1149],{"className":244,"code":1148,"language":246,"meta":247,"style":247},"wkt_params = {\n    \"type\": \"csv\",\n    \"delimiter\": \",\",\n    \"detectTypes\": \"yes\",\n    \"wktField\": \"geom_wkt\",\n    \"geomType\": \"polygon\",\n    \"crs\": \"EPSG:27700\",\n}\nwkt_uri = f\"{Path('\u002Fdata\u002Finbox\u002Fparcels.csv').as_uri()}?{urlencode(wkt_params)}\"\nparcels = QgsVectorLayer(wkt_uri, \"parcels\", \"delimitedtext\")\nprint(parcels.wkbType(), parcels.featureCount())\n",[18,1150,1151,1160,1170,1180,1190,1202,1214,1225,1229,1264,1283],{"__ignoreMap":247},[251,1152,1153,1156,1158],{"class":172,"line":253},[251,1154,1155],{"class":260},"wkt_params ",[251,1157,309],{"class":256},[251,1159,335],{"class":260},[251,1161,1162,1164,1166,1168],{"class":172,"line":270},[251,1163,341],{"class":315},[251,1165,344],{"class":260},[251,1167,347],{"class":315},[251,1169,350],{"class":260},[251,1171,1172,1174,1176,1178],{"class":172,"line":283},[251,1173,356],{"class":315},[251,1175,344],{"class":260},[251,1177,361],{"class":315},[251,1179,350],{"class":260},[251,1181,1182,1184,1186,1188],{"class":172,"line":296},[251,1183,369],{"class":315},[251,1185,344],{"class":260},[251,1187,374],{"class":315},[251,1189,350],{"class":260},[251,1191,1192,1195,1197,1200],{"class":172,"line":303},[251,1193,1194],{"class":315},"    \"wktField\"",[251,1196,344],{"class":260},[251,1198,1199],{"class":315},"\"geom_wkt\"",[251,1201,350],{"class":260},[251,1203,1204,1207,1209,1212],{"class":172,"line":322},[251,1205,1206],{"class":315},"    \"geomType\"",[251,1208,344],{"class":260},[251,1210,1211],{"class":315},"\"polygon\"",[251,1213,350],{"class":260},[251,1215,1216,1218,1220,1223],{"class":172,"line":327},[251,1217,408],{"class":315},[251,1219,344],{"class":260},[251,1221,1222],{"class":315},"\"EPSG:27700\"",[251,1224,350],{"class":260},[251,1226,1227],{"class":172,"line":338},[251,1228,458],{"class":260},[251,1230,1231,1234,1236,1238,1240,1242,1245,1248,1251,1253,1255,1257,1260,1262],{"class":172,"line":353},[251,1232,1233],{"class":260},"wkt_uri ",[251,1235,309],{"class":256},[251,1237,469],{"class":256},[251,1239,472],{"class":315},[251,1241,476],{"class":475},[251,1243,1244],{"class":260},"Path(",[251,1246,1247],{"class":315},"'\u002Fdata\u002Finbox\u002Fparcels.csv'",[251,1249,1250],{"class":260},").as_uri()",[251,1252,482],{"class":475},[251,1254,485],{"class":315},[251,1256,476],{"class":475},[251,1258,1259],{"class":260},"urlencode(wkt_params)",[251,1261,482],{"class":475},[251,1263,495],{"class":315},[251,1265,1266,1269,1271,1274,1277,1279,1281],{"class":172,"line":366},[251,1267,1268],{"class":260},"parcels ",[251,1270,309],{"class":256},[251,1272,1273],{"class":260}," QgsVectorLayer(wkt_uri, ",[251,1275,1276],{"class":315},"\"parcels\"",[251,1278,517],{"class":260},[251,1280,520],{"class":315},[251,1282,319],{"class":260},[251,1284,1285,1287],{"class":172,"line":379},[251,1286,589],{"class":475},[251,1288,1289],{"class":260},"(parcels.wkbType(), parcels.featureCount())\n",[14,1291,1292,1294,1295,1298,1299,1302,1303,1305,1306,1310],{},[220,1293,603],{}," Without ",[18,1296,1297],{},"geomType",", the provider inspects the first rows to decide the layer type, and a file whose first record happens to be a ",[18,1300,1301],{},"POINT"," produces a point layer on which every polygon row has no geometry. Stating the type removes the guess. For a lookup table with no location at all — a list of sensor calibrations, say — ",[18,1304,1139],{}," gives a geometryless layer that can sit on the right-hand side of ",[30,1307,1309],{"href":1308},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fjoin-attributes-by-field-value-pyqgis\u002F","a join by field value",".",[209,1312,1314],{"id":1313},"filter-rows-and-pick-up-a-re-exported-file","Filter rows and pick up a re-exported file",[14,1316,1317,1318,1321],{},"A CSV often holds more than you want on the map — decommissioned sensors, test rows, a year of readings when you need a week. A subset string filters the layer without touching the file, and on this provider it behaves like a ",[18,1319,1320],{},"WHERE"," clause written in the QGIS expression language.",[242,1323,1325],{"className":244,"code":1324,"language":246,"meta":247,"style":247},"sites.setSubsetString(\"\\\"status\\\" = 'active' AND \\\"installed\\\" >= '2024-01-01'\")\nprint(sites.featureCount(), \"active sites\")\n\n# later, after someone overwrites the CSV with a fresh export\nsites.dataProvider().reloadData()\nsites.triggerRepaint()\nprint(sites.featureCount(), \"after reload\")\n",[18,1326,1327,1357,1368,1372,1378,1383,1388],{"__ignoreMap":247},[251,1328,1329,1332,1334,1337,1340,1342,1345,1347,1350,1352,1355],{"class":172,"line":253},[251,1330,1331],{"class":260},"sites.setSubsetString(",[251,1333,472],{"class":315},[251,1335,1336],{"class":475},"\\\"",[251,1338,1339],{"class":315},"status",[251,1341,1336],{"class":475},[251,1343,1344],{"class":315}," = 'active' AND ",[251,1346,1336],{"class":475},[251,1348,1349],{"class":315},"installed",[251,1351,1336],{"class":475},[251,1353,1354],{"class":315}," >= '2024-01-01'\"",[251,1356,319],{"class":260},[251,1358,1359,1361,1363,1366],{"class":172,"line":270},[251,1360,589],{"class":475},[251,1362,592],{"class":260},[251,1364,1365],{"class":315},"\"active sites\"",[251,1367,319],{"class":260},[251,1369,1370],{"class":172,"line":283},[251,1371,300],{"emptyLinePlaceholder":299},[251,1373,1374],{"class":172,"line":296},[251,1375,1377],{"class":1376},"sjoCn","# later, after someone overwrites the CSV with a fresh export\n",[251,1379,1380],{"class":172,"line":303},[251,1381,1382],{"class":260},"sites.dataProvider().reloadData()\n",[251,1384,1385],{"class":172,"line":322},[251,1386,1387],{"class":260},"sites.triggerRepaint()\n",[251,1389,1390,1392,1394,1397],{"class":172,"line":327},[251,1391,589],{"class":475},[251,1393,592],{"class":260},[251,1395,1396],{"class":315},"\"after reload\"",[251,1398,319],{"class":260},[14,1400,1401,1403,1404,1407,1408,1411,1412,1415,1416,1310],{},[220,1402,603],{}," On a large file the first filtered count is slow because the provider has to scan every row; ",[18,1405,1406],{},"subsetIndex=yes"," in the URI makes it build an index of matching rows once so that subsequent requests only read those lines, which is worth turning on when a script applies one filter and then iterates many times. Date comparisons only work if ",[18,1409,1410],{},"detectTypes"," recognised the column as a date — otherwise the comparison is between strings, which happens to sort correctly for ISO ",[18,1413,1414],{},"YYYY-MM-DD"," values and silently fails for ",[18,1417,1418],{},"17\u002F09\u002F2026",[14,1420,1421,1424,1425,1428,1429,1433],{},[18,1422,1423],{},"reloadData()"," is the explicit form of what ",[18,1426,1427],{},"watchFile=yes"," does automatically: it drops the provider's cached rows, re-reads the file and re-runs detection. Call it when a script knows the file has changed — at the start of each iteration of ",[30,1430,1432],{"href":1431},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fschedule-pyqgis-scripts-with-cron\u002F","a scheduled job",", for instance — and check the feature count afterwards, because a re-export with a changed header row gives a valid layer with every geometry empty. Field names in the subset string must match the new header exactly; if a column was renamed the layer becomes invalid rather than unfiltered, which at least fails loudly.",[209,1435,1437],{"id":1436},"copy-it-into-a-real-format","Copy it into a real format",[14,1439,1440],{},"Keep the CSV layer for a quick look; copy it before doing anything else. A GeoPackage copy is editable, carries real field types that do not depend on re-detection, keeps a persistent spatial index, and does not change underneath you when the spreadsheet is re-exported.",[242,1442,1444],{"className":244,"code":1443,"language":246,"meta":247,"style":247},"import processing\n\nresult = processing.run(\"native:savefeatures\", {\n    \"INPUT\": sites,\n    \"OUTPUT\": \"\u002Fdata\u002Fwork\u002Fsensor_sites.gpkg\",\n    \"LAYER_NAME\": \"sensor_sites\",\n})\ncopied = QgsVectorLayer(result[\"OUTPUT\"], \"sensor_sites\", \"ogr\")\nprint(copied.isValid(), copied.featureCount())\n",[18,1445,1446,1453,1457,1473,1481,1493,1504,1508,1533],{"__ignoreMap":247},[251,1447,1448,1450],{"class":172,"line":253},[251,1449,264],{"class":256},[251,1451,1452],{"class":260}," processing\n",[251,1454,1455],{"class":172,"line":270},[251,1456,300],{"emptyLinePlaceholder":299},[251,1458,1459,1462,1464,1467,1470],{"class":172,"line":283},[251,1460,1461],{"class":260},"result ",[251,1463,309],{"class":256},[251,1465,1466],{"class":260}," processing.run(",[251,1468,1469],{"class":315},"\"native:savefeatures\"",[251,1471,1472],{"class":260},", {\n",[251,1474,1475,1478],{"class":172,"line":296},[251,1476,1477],{"class":315},"    \"INPUT\"",[251,1479,1480],{"class":260},": sites,\n",[251,1482,1483,1486,1488,1491],{"class":172,"line":303},[251,1484,1485],{"class":315},"    \"OUTPUT\"",[251,1487,344],{"class":260},[251,1489,1490],{"class":315},"\"\u002Fdata\u002Fwork\u002Fsensor_sites.gpkg\"",[251,1492,350],{"class":260},[251,1494,1495,1498,1500,1502],{"class":172,"line":322},[251,1496,1497],{"class":315},"    \"LAYER_NAME\"",[251,1499,344],{"class":260},[251,1501,514],{"class":315},[251,1503,350],{"class":260},[251,1505,1506],{"class":172,"line":327},[251,1507,974],{"class":260},[251,1509,1510,1513,1515,1518,1521,1524,1526,1528,1531],{"class":172,"line":338},[251,1511,1512],{"class":260},"copied ",[251,1514,309],{"class":256},[251,1516,1517],{"class":260}," QgsVectorLayer(result[",[251,1519,1520],{"class":315},"\"OUTPUT\"",[251,1522,1523],{"class":260},"], ",[251,1525,514],{"class":315},[251,1527,517],{"class":260},[251,1529,1530],{"class":315},"\"ogr\"",[251,1532,319],{"class":260},[251,1534,1535,1537],{"class":172,"line":353},[251,1536,589],{"class":475},[251,1538,1539],{"class":260},"(copied.isValid(), copied.featureCount())\n",[14,1541,1542,604,1544,1547,1548,1551,1552,1556],{},[220,1543,603],{},[18,1545,1546],{},"native:savefeatures"," is the Processing wrapper around the vector file writer and accepts any layer, so it avoids re-specifying fields and CRS. Rows with null geometry are copied too, which is what you want for an audit trail and not what you want for analysis — follow it with ",[18,1549,1550],{},"native:removenullgeometries"," if downstream tools choke on empty shapes. For more control over the output container, ",[30,1553,1555],{"href":1554},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fwrite-vector-layer-to-geopackage-pyqgis\u002F","writing to a GeoPackage"," covers the writer options directly.",[209,1558,1560],{"id":1559},"qgis-version-compatibility","QGIS version compatibility",[14,1562,1563,1564,1566,1567,517,1570,1044,1572,1574,1575,1578,1579,1582,1583,1586,1587,1589,1590,1593],{},"The ",[18,1565,20],{}," URI parameters used here have been stable since QGIS 3.0. ",[18,1568,1569],{},"decimalPoint",[18,1571,1035],{},[18,1573,1039],{}," date back to the 2.x provider. Enum values such as ",[18,1576,1577],{},"QgsFeatureRequest.NoGeometry"," have scoped equivalents under ",[18,1580,1581],{},"Qgis"," (here ",[18,1584,1585],{},"Qgis.FeatureRequestFlag.NoGeometry","); on the QGIS 4 series the scoped forms are the ones to use. ",[18,1588,1546],{}," arrived in 3.24 — on older releases use ",[18,1591,1592],{},"QgsVectorFileWriter.writeAsVectorFormatV3"," instead.",[209,1595,1597],{"id":1596},"troubleshooting","Troubleshooting",[214,1599,1600,1613,1625,1640,1659,1667],{},[217,1601,1602,1605,1606,1609,1610,1310],{},[220,1603,1604],{},"Points appear near 0°, 0°."," The coordinates are projected metres but the URI says ",[18,1607,1608],{},"EPSG:4326",", or the x and y fields are swapped. Latitude is ",[18,1611,1612],{},"yField",[217,1614,1615,604,1618,1620,1621,1624],{},[220,1616,1617],{},"Everything is a string.",[18,1619,128],{}," is missing, or the column contains one non-numeric value such as ",[18,1622,1623],{},"n\u002Fa",", which demotes the whole column to text.",[217,1626,1627,1633,1634,1637,1638,1310],{},[220,1628,1629,1632],{},[18,1630,1631],{},"isValid()"," is False."," The URL is malformed — usually a Windows path pasted as ",[18,1635,1636],{},"file:\u002F\u002FC:\\data",". Build it with ",[18,1639,607],{},[217,1641,1642,1648,1649,1651,1652,1655,1656,1310],{},[220,1643,1644,1645,1310],{},"Accented names show as ",[18,1646,1647],{},"�"," Wrong ",[18,1650,1031],{},"; try ",[18,1653,1654],{},"Windows-1252"," or ",[18,1657,1658],{},"ISO-8859-1",[217,1660,1661,1664,1665,1310],{},[220,1662,1663],{},"The layer reloads while a script runs."," Set ",[18,1666,626],{},[217,1668,1669,1676,1677,1044,1680,1310],{},[220,1670,1671,1672,1675],{},"A column called ",[18,1673,1674],{},"x"," is used as geometry when you did not ask."," Without explicit field names the provider auto-detects common coordinate headers; always pass ",[18,1678,1679],{},"xField",[18,1681,1612],{},[209,1683,1685],{"id":1684},"conclusion","Conclusion",[14,1687,1688],{},"Build the URI from a dictionary, state the delimiter, the coordinate fields and the CRS explicitly, and turn on type detection. Then count the null geometries straight away, because the provider will never tell you about them. Once the layer looks right, copy it to GeoPackage and do your real work there.",[209,1690,1692],{"id":1691},"frequently-asked-questions","Frequently Asked Questions",[14,1694,1695,1698,1699,1702,1703,517,1706,1709,1710,1044,1712,1714],{},[220,1696,1697],{},"Can I load a CSV that has no header row?","\nYes — add ",[18,1700,1701],{},"useHeader=no",". Fields are then named ",[18,1704,1705],{},"field_1",[18,1707,1708],{},"field_2"," and so on, and ",[18,1711,1679],{},[18,1713,1612],{}," must use those names.",[14,1716,1717,1720],{},[220,1718,1719],{},"Why can I not edit the layer?","\nThe delimited text provider is read-only by design. Copy it to a GeoPackage or a memory layer to edit.",[14,1722,1723,1726,1727,1729,1730,1733,1734,1737,1738,1741,1742,1745],{},[220,1724,1725],{},"How do I load a tab-separated file?","\nUse ",[18,1728,125],{}," with the two-character delimiter ",[18,1731,1732],{},"\\t"," — a backslash then a ",[18,1735,1736],{},"t",", written ",[18,1739,1740],{},"\"\\\\t\""," in Python — which the provider reads as a tab, or use ",[18,1743,1744],{},"type=regexp"," with a delimiter pattern for anything irregular.",[14,1747,1748,1751,1752,1754],{},[220,1749,1750],{},"Does the layer update when the CSV changes?","\nOnly if ",[18,1753,1427],{},", and even then only while QGIS has it open. A copy in GeoPackage never changes on its own.",[209,1756,1758],{"id":1757},"related","Related",[214,1760,1761,1766,1772,1778,1784],{},[217,1762,1763,1765],{},[30,1764,33],{"href":32}," — the guide this recipe belongs to",[217,1767,1768],{},[30,1769,1771],{"href":1770},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fdecode-and-build-data-source-uris-pyqgis\u002F","Decode and Build Data Source URIs in PyQGIS",[217,1773,1774],{},[30,1775,1777],{"href":1776},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fimport-geotagged-photos-as-points-pyqgis\u002F","Import Geotagged Photos as Points in PyQGIS",[217,1779,1780],{},[30,1781,1783],{"href":1782},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fexport-attribute-table-to-csv-pyqgis\u002F","Export an Attribute Table to CSV with PyQGIS",[217,1785,1786],{},[30,1787,1789],{"href":1788},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fhandling-missing-crs-in-pyqgis\u002F","Handling Missing CRS in PyQGIS",[1791,1792,1793],"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 .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}",{"title":247,"searchDepth":270,"depth":270,"links":1795},[1796,1797,1798,1799,1800,1801,1802,1803,1804,1805,1806],{"id":211,"depth":270,"text":212},{"id":232,"depth":270,"text":233},{"id":744,"depth":270,"text":745},{"id":898,"depth":270,"text":899},{"id":1313,"depth":270,"text":1314},{"id":1436,"depth":270,"text":1437},{"id":1559,"depth":270,"text":1560},{"id":1596,"depth":270,"text":1597},{"id":1684,"depth":270,"text":1685},{"id":1691,"depth":270,"text":1692},{"id":1757,"depth":270,"text":1758},"Turn a CSV of coordinates or WKT into a real point layer with the delimitedtext provider — building the URI, forcing field types, handling decimal commas, and knowing when to copy the result into a GeoPackage.","md",{"slug":1810,"type":1811,"breadcrumb":1812,"datePublished":1813,"dateModified":1813},"load-csv-as-point-layer-pyqgis","article","Load a CSV as a Point Layer","2026-09-17","\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fload-csv-as-point-layer-pyqgis",{"title":5,"description":1807},"spatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fload-csv-as-point-layer-pyqgis\u002Findex","Y4049gc_xqxDvZeXGv0EXDXLPwuHAW2qugR3ecmALv0",1789632908450]