[{"data":1,"prerenderedAt":1997},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fconvert-layer-to-geopandas-geodataframe-pyqgis":3},{"id":4,"title":5,"body":6,"description":1986,"extension":1987,"meta":1988,"navigation":238,"path":1993,"seo":1994,"stem":1995,"__hash__":1996},"docs\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fconvert-layer-to-geopandas-geodataframe-pyqgis\u002Findex.md","Convert a QGIS Layer to a GeoPandas GeoDataFrame",{"type":7,"value":8,"toc":1970},"minimark",[9,13,17,26,160,165,185,189,192,405,425,429,432,670,689,773,777,780,929,942,946,961,1176,1197,1201,1204,1280,1396,1410,1414,1417,1632,1645,1649,1657,1661,1664,1807,1830,1834,1848,1852,1887,1891,1898,1902,1911,1921,1927,1933,1937,1966],[10,11,5],"h1",{"id":12},"convert-a-qgis-layer-to-a-geopandas-geodataframe",[14,15,16],"p",{},"GeoPandas is where much of Python's spatial analysis happens: vectorised operations on whole tables, group-bys, merges, and direct access to the scientific stack. QGIS is where the data is loaded, cleaned, styled and inspected. Moving a layer from one to the other is a common step in scripts that use both — and there is more than one way to do it, with very different speed and fidelity.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002F","PyQGIS and the Python Data Stack",". It shows the fast route of reading the layer's source directly, the general route of converting features through WKB, how to keep the CRS and handle NULLs and dates, and how to choose between them.",[14,27,28],{},[29,30,35,39,43,50,67,76,85,90,98,102,109,114,119,124,129,133,136,139,144,147,154],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 280","img","Two routes from a QGIS layer to a GeoDataFrame: reading the underlying file directly, or iterating features and converting geometry through WKB","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Two routes from a layer to a GeoDataFrame",[40,41,42],"desc",{},"Route one: when the layer is backed by a file or database that GeoPandas can read, take the source path from the layer and read it with geopandas.read_file, applying the same subset and layer name. Route two: for memory layers, edited layers, joined or virtual fields, iterate features and convert geometry through WKB with shapely, building the frame from rows. Both end in a GeoDataFrame with the layer's CRS.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","280","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"gpdRoutesArrow","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-family:sans-serif;font-weight:bold","#17211d","middle","Read the source, or convert the features",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":58,"fill":82,"stroke":83,"style":84},"290","44","180","56","#fffdf7","#59645f","stroke-width:2",[68,86,89],{"x":70,"y":87,"style":88,"fill":73,"textAnchor":74},"75.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","QgsVectorLayer",[91,92],"line",{"x1":93,"y1":94,"x2":95,"y2":96,"stroke":66,"style":97},"330","100","190","130","stroke-width:1.8;marker-end:url(#gpdRoutesArrow)",[91,99],{"x1":100,"y1":94,"x2":101,"y2":96,"stroke":66,"style":97},"430","570",[44,103],{"x":104,"y":105,"width":93,"height":106,"rx":58,"fill":107,"stroke":108,"style":84},"24","134","92","#eef7f4","#0f766e",[68,110,113],{"x":111,"y":112,"style":88,"fill":108,"textAnchor":74},"189","159.78","read the source",[68,115,118],{"x":111,"y":116,"style":117,"fill":66,"textAnchor":74},"183.78","text-anchor:middle;font-size:10.0px;font-family:monospace","geopandas.read_file(path)",[68,120,123],{"x":111,"y":121,"style":122,"fill":83,"textAnchor":74},"207.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","files, GeoPackage, PostGIS",[44,125],{"x":126,"y":105,"width":93,"height":106,"rx":58,"fill":127,"stroke":128,"style":84},"406","#eff3ff","#2563eb",[68,130,132],{"x":131,"y":112,"style":88,"fill":128,"textAnchor":74},"571","convert features",[68,134,135],{"x":131,"y":116,"style":117,"fill":66,"textAnchor":74},"shapely.from_wkb(…)",[68,137,138],{"x":131,"y":121,"style":122,"fill":83,"textAnchor":74},"memory, edits, joins",[91,140],{"x1":95,"y1":141,"x2":142,"y2":143,"stroke":66,"style":97},"226","300","248",[91,145],{"x1":101,"y1":141,"x2":146,"y2":143,"stroke":66,"style":97},"460",[44,148],{"x":142,"y":149,"width":150,"height":151,"rx":58,"fill":152,"stroke":153,"style":84},"244","160","32","#e8efe6","#15803d",[68,155,159],{"x":70,"y":156,"style":157,"fill":158,"textAnchor":74},"263.6","text-anchor:middle;font-size:11.0px;font-family:sans-serif;font-weight:bold","#166534","GeoDataFrame + CRS",[161,162,164],"h2",{"id":163},"prerequisites","Prerequisites",[166,167,168,177],"ul",{},[169,170,171,172,176],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series, with GeoPandas and shapely 2 installed into QGIS's Python. ",[21,173,175],{"href":174},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Finstall-python-packages-into-qgis\u002F","Installing Python packages into QGIS"," covers the per-platform steps; on Linux and conda installations it is usually one command.",[169,178,179,180,184],{},"Check the versions from the QGIS Python console before starting: ",[181,182,183],"code",{},"import geopandas, shapely; print(geopandas.__version__, shapely.__version__)",".",[161,186,188],{"id":187},"route-1-read-the-layers-source-directly","Route 1: read the layer's source directly",[14,190,191],{},"If the layer is a plain file or database table, the fastest conversion is not a conversion at all: ask the layer where its data lives and let GeoPandas read it with its own I\u002FO, which is vectorised and much faster than iterating features.",[193,194,199],"pre",{"className":195,"code":196,"language":197,"meta":198,"style":198},"language-python shiki shiki-themes github-dark","import geopandas as gpd\nfrom qgis.core import QgsProject, QgsProviderRegistry\n\nlayer = QgsProject.instance().mapLayersByName(\"buildings\")[0]\nparts = QgsProviderRegistry.instance().decodeUri(layer.providerType(), layer.source())\nprint(parts)   # {'path': '\u002Fdata\u002Fcity.gpkg', 'layerName': 'buildings', 'subset': None, ...}\n\nif layer.providerType() == \"ogr\" and not layer.isModified():\n    gdf = gpd.read_file(parts[\"path\"], layer=parts.get(\"layerName\"))\n    if parts.get(\"subset\"):\n        print(\"warning: layer has a subset filter, apply it in pandas:\", parts[\"subset\"])\n    print(len(gdf), \"rows\", gdf.crs)\n","python","",[181,200,201,219,233,240,265,276,289,294,318,350,365,385],{"__ignoreMap":198},[202,203,205,209,213,216],"span",{"class":91,"line":204},1,[202,206,208],{"class":207},"snl16","import",[202,210,212],{"class":211},"s95oV"," geopandas ",[202,214,215],{"class":207},"as",[202,217,218],{"class":211}," gpd\n",[202,220,222,225,228,230],{"class":91,"line":221},2,[202,223,224],{"class":207},"from",[202,226,227],{"class":211}," qgis.core ",[202,229,208],{"class":207},[202,231,232],{"class":211}," QgsProject, QgsProviderRegistry\n",[202,234,236],{"class":91,"line":235},3,[202,237,239],{"emptyLinePlaceholder":238},true,"\n",[202,241,243,246,249,252,256,259,262],{"class":91,"line":242},4,[202,244,245],{"class":211},"layer ",[202,247,248],{"class":207},"=",[202,250,251],{"class":211}," QgsProject.instance().mapLayersByName(",[202,253,255],{"class":254},"sU2Wk","\"buildings\"",[202,257,258],{"class":211},")[",[202,260,46],{"class":261},"sDLfK",[202,263,264],{"class":211},"]\n",[202,266,268,271,273],{"class":91,"line":267},5,[202,269,270],{"class":211},"parts ",[202,272,248],{"class":207},[202,274,275],{"class":211}," QgsProviderRegistry.instance().decodeUri(layer.providerType(), layer.source())\n",[202,277,279,282,285],{"class":91,"line":278},6,[202,280,281],{"class":261},"print",[202,283,284],{"class":211},"(parts)   ",[202,286,288],{"class":287},"sjoCn","# {'path': '\u002Fdata\u002Fcity.gpkg', 'layerName': 'buildings', 'subset': None, ...}\n",[202,290,292],{"class":91,"line":291},7,[202,293,239],{"emptyLinePlaceholder":238},[202,295,297,300,303,306,309,312,315],{"class":91,"line":296},8,[202,298,299],{"class":207},"if",[202,301,302],{"class":211}," layer.providerType() ",[202,304,305],{"class":207},"==",[202,307,308],{"class":254}," \"ogr\"",[202,310,311],{"class":207}," and",[202,313,314],{"class":207}," not",[202,316,317],{"class":211}," layer.isModified():\n",[202,319,321,324,326,329,332,335,339,341,344,347],{"class":91,"line":320},9,[202,322,323],{"class":211},"    gdf ",[202,325,248],{"class":207},[202,327,328],{"class":211}," gpd.read_file(parts[",[202,330,331],{"class":254},"\"path\"",[202,333,334],{"class":211},"], ",[202,336,338],{"class":337},"s9osk","layer",[202,340,248],{"class":207},[202,342,343],{"class":211},"parts.get(",[202,345,346],{"class":254},"\"layerName\"",[202,348,349],{"class":211},"))\n",[202,351,353,356,359,362],{"class":91,"line":352},10,[202,354,355],{"class":207},"    if",[202,357,358],{"class":211}," parts.get(",[202,360,361],{"class":254},"\"subset\"",[202,363,364],{"class":211},"):\n",[202,366,368,371,374,377,380,382],{"class":91,"line":367},11,[202,369,370],{"class":261},"        print",[202,372,373],{"class":211},"(",[202,375,376],{"class":254},"\"warning: layer has a subset filter, apply it in pandas:\"",[202,378,379],{"class":211},", parts[",[202,381,361],{"class":254},[202,383,384],{"class":211},"])\n",[202,386,388,391,393,396,399,402],{"class":91,"line":387},12,[202,389,390],{"class":261},"    print",[202,392,373],{"class":211},[202,394,395],{"class":261},"len",[202,397,398],{"class":211},"(gdf), ",[202,400,401],{"class":254},"\"rows\"",[202,403,404],{"class":211},", gdf.crs)\n",[14,406,407,411,412,415,416,420,421,424],{},[408,409,410],"strong",{},"Breakdown:"," ",[181,413,414],{},"decodeUri"," splits the provider's source string into its parts — path, layer name, subset — without fragile string parsing, as explained in ",[21,417,419],{"href":418},"\u002Fspatial-data-processing-automation\u002Flayer-data-sources-and-formats\u002Fdecode-and-build-data-source-uris-pyqgis\u002F","decoding and building data source URIs",". GeoPandas reads GeoPackage, shapefile, GeoJSON and FlatGeobuf through pyogrio or fiona, typically ten to a hundred times faster than a Python loop over features. Two conditions must hold for the result to match what QGIS shows: the layer must have no unsaved edits (",[181,422,423],{},"isModified()","), and any subset filter or joined fields must be reproduced on the pandas side. When those conditions fail, use the second route.",[161,426,428],{"id":427},"route-2-convert-features-through-wkb","Route 2: convert features through WKB",[14,430,431],{},"Memory layers, layers with pending edits, virtual layers and layers with joined or expression fields only exist inside QGIS. For them, iterate the features and convert geometry through WKB — the binary format both libraries understand natively.",[193,433,435],{"className":195,"code":434,"language":197,"meta":198,"style":198},"import pandas as pd\nimport shapely\n\ndef layer_to_gdf(layer, request=None):\n    from qgis.core import QgsFeatureRequest\n    request = request or QgsFeatureRequest()\n    names = layer.fields().names()\n    rows, wkbs = [], []\n    for f in layer.getFeatures(request):\n        rows.append(f.attributes())\n        wkbs.append(bytes(f.geometry().asWkb()) if f.hasGeometry() else None)\n    df = pd.DataFrame(rows, columns=names)\n    geoms = shapely.from_wkb([w for w in wkbs], on_invalid=\"warn\")\n    return gpd.GeoDataFrame(df, geometry=geoms, crs=layer.crs().toWkt())\n\ngdf = layer_to_gdf(layer)\nprint(gdf.geom_type.value_counts(), gdf.crs.to_epsg())\n",[181,436,437,449,456,460,479,491,507,517,527,541,546,571,589,621,646,651,662],{"__ignoreMap":198},[202,438,439,441,444,446],{"class":91,"line":204},[202,440,208],{"class":207},[202,442,443],{"class":211}," pandas ",[202,445,215],{"class":207},[202,447,448],{"class":211}," pd\n",[202,450,451,453],{"class":91,"line":221},[202,452,208],{"class":207},[202,454,455],{"class":211}," shapely\n",[202,457,458],{"class":91,"line":235},[202,459,239],{"emptyLinePlaceholder":238},[202,461,462,465,469,472,474,477],{"class":91,"line":242},[202,463,464],{"class":207},"def",[202,466,468],{"class":467},"svObZ"," layer_to_gdf",[202,470,471],{"class":211},"(layer, request",[202,473,248],{"class":207},[202,475,476],{"class":261},"None",[202,478,364],{"class":211},[202,480,481,484,486,488],{"class":91,"line":267},[202,482,483],{"class":207},"    from",[202,485,227],{"class":211},[202,487,208],{"class":207},[202,489,490],{"class":211}," QgsFeatureRequest\n",[202,492,493,496,498,501,504],{"class":91,"line":278},[202,494,495],{"class":211},"    request ",[202,497,248],{"class":207},[202,499,500],{"class":211}," request ",[202,502,503],{"class":207},"or",[202,505,506],{"class":211}," QgsFeatureRequest()\n",[202,508,509,512,514],{"class":91,"line":291},[202,510,511],{"class":211},"    names ",[202,513,248],{"class":207},[202,515,516],{"class":211}," layer.fields().names()\n",[202,518,519,522,524],{"class":91,"line":296},[202,520,521],{"class":211},"    rows, wkbs ",[202,523,248],{"class":207},[202,525,526],{"class":211}," [], []\n",[202,528,529,532,535,538],{"class":91,"line":320},[202,530,531],{"class":207},"    for",[202,533,534],{"class":211}," f ",[202,536,537],{"class":207},"in",[202,539,540],{"class":211}," layer.getFeatures(request):\n",[202,542,543],{"class":91,"line":352},[202,544,545],{"class":211},"        rows.append(f.attributes())\n",[202,547,548,551,554,557,559,562,565,568],{"class":91,"line":367},[202,549,550],{"class":211},"        wkbs.append(",[202,552,553],{"class":261},"bytes",[202,555,556],{"class":211},"(f.geometry().asWkb()) ",[202,558,299],{"class":207},[202,560,561],{"class":211}," f.hasGeometry() ",[202,563,564],{"class":207},"else",[202,566,567],{"class":261}," None",[202,569,570],{"class":211},")\n",[202,572,573,576,578,581,584,586],{"class":91,"line":387},[202,574,575],{"class":211},"    df ",[202,577,248],{"class":207},[202,579,580],{"class":211}," pd.DataFrame(rows, ",[202,582,583],{"class":337},"columns",[202,585,248],{"class":207},[202,587,588],{"class":211},"names)\n",[202,590,592,595,597,600,603,606,608,611,614,616,619],{"class":91,"line":591},13,[202,593,594],{"class":211},"    geoms ",[202,596,248],{"class":207},[202,598,599],{"class":211}," shapely.from_wkb([w ",[202,601,602],{"class":207},"for",[202,604,605],{"class":211}," w ",[202,607,537],{"class":207},[202,609,610],{"class":211}," wkbs], ",[202,612,613],{"class":337},"on_invalid",[202,615,248],{"class":207},[202,617,618],{"class":254},"\"warn\"",[202,620,570],{"class":211},[202,622,624,627,630,633,635,638,641,643],{"class":91,"line":623},14,[202,625,626],{"class":207},"    return",[202,628,629],{"class":211}," gpd.GeoDataFrame(df, ",[202,631,632],{"class":337},"geometry",[202,634,248],{"class":207},[202,636,637],{"class":211},"geoms, ",[202,639,640],{"class":337},"crs",[202,642,248],{"class":207},[202,644,645],{"class":211},"layer.crs().toWkt())\n",[202,647,649],{"class":91,"line":648},15,[202,650,239],{"emptyLinePlaceholder":238},[202,652,654,657,659],{"class":91,"line":653},16,[202,655,656],{"class":211},"gdf ",[202,658,248],{"class":207},[202,660,661],{"class":211}," layer_to_gdf(layer)\n",[202,663,665,667],{"class":91,"line":664},17,[202,666,281],{"class":261},[202,668,669],{"class":211},"(gdf.geom_type.value_counts(), gdf.crs.to_epsg())\n",[14,671,672,674,675,678,679,681,682,685,686,184],{},[408,673,410],{}," Collecting attributes and WKB in two lists, then converting all geometry in one ",[181,676,677],{},"shapely.from_wkb"," call, is much faster than creating a shapely object per feature inside the loop. ",[181,680,476],{}," entries become missing geometries in the frame. Passing the CRS as WKT preserves custom and compound systems that have no EPSG code; for standard systems ",[181,683,684],{},"layer.crs().authid()"," is shorter and equally correct. Joined and virtual fields are included automatically because they are part of ",[181,687,688],{},"layer.fields()",[14,690,691],{},[29,692,695,698,701,704,711,714,719,724,728,732,737,743,746,749,752,756,759,763,766,769],{"viewBox":693,"role":32,"ariaLabel":694,"xmlns":34},"0 0 760 240","Geometry crossing from QgsGeometry to shapely as WKB bytes in one vectorised call, compared with slower text and per-feature routes",[36,696,697],{},"Why WKB is the bridge",[40,699,700],{},"Geometry passes from QGIS to shapely as WKB bytes: QgsGeometry.asWkb produces them, shapely.from_wkb reads a whole list at once in vectorised C code. Converting through WKT text would be slower, larger and lose precision; building shapely objects feature by feature in Python would be slower still. Z and M values survive the WKB route.",[44,702],{"x":46,"y":46,"width":47,"height":703,"fill":49},"240",[51,705,706],{},[54,707,709],{"id":708,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"gpdWkbArrow",[63,710],{"d":65,"fill":66},[68,712,713],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Binary in, binary out, one call",[44,715],{"x":104,"y":716,"width":717,"height":718,"rx":58,"fill":107,"stroke":108,"style":84},"60","200","120",[68,720,723],{"x":721,"y":722,"style":88,"fill":108,"textAnchor":74},"124","97.78","QgsGeometry",[68,725,727],{"x":721,"y":726,"style":117,"fill":66,"textAnchor":74},"123.78","asWkb()",[68,729,731],{"x":721,"y":730,"style":122,"fill":83,"textAnchor":74},"149.78","bytes per feature",[91,733],{"x1":734,"y1":718,"x2":735,"y2":718,"stroke":66,"style":736},"224","266","stroke-width:1.8;marker-end:url(#gpdWkbArrow)",[44,738],{"x":739,"y":716,"width":740,"height":718,"rx":58,"fill":741,"stroke":742,"style":84},"270","220","#fdf2e2","#b45309",[68,744,745],{"x":70,"y":722,"style":88,"fill":742,"textAnchor":74},"list of bytes",[68,747,748],{"x":70,"y":726,"style":122,"fill":83,"textAnchor":74},"Z and M kept",[68,750,751],{"x":70,"y":730,"style":122,"fill":83,"textAnchor":74},"no precision loss",[91,753],{"x1":754,"y1":718,"x2":755,"y2":718,"stroke":66,"style":736},"490","532",[44,757],{"x":758,"y":716,"width":717,"height":718,"rx":58,"fill":127,"stroke":128,"style":84},"536",[68,760,762],{"x":761,"y":722,"style":88,"fill":128,"textAnchor":74},"636","shapely",[68,764,765],{"x":761,"y":726,"style":117,"fill":66,"textAnchor":74},"from_wkb(list)",[68,767,768],{"x":761,"y":730,"style":122,"fill":83,"textAnchor":74},"one vectorised call",[68,770,772],{"x":70,"y":771,"style":122,"fill":83,"textAnchor":74},"216","avoid: WKT text (slow, lossy) · per-feature shapely loops (slowest)",[161,774,776],{"id":775},"read-only-what-you-need","Read only what you need",[14,778,779],{},"The feature route reads every attribute and every geometry unless told otherwise. For wide tables or analyses that need a few columns, a trimmed request speeds conversion and reduces memory.",[193,781,783],{"className":195,"code":782,"language":197,"meta":198,"style":198},"from qgis.core import QgsFeatureRequest\n\nreq = (QgsFeatureRequest()\n       .setSubsetOfAttributes([\"building_id\", \"height_m\", \"use\"], layer.fields())\n       .setFilterExpression('\"use\" IN (\\'residential\\', \\'mixed\\')'))\nsmall = layer_to_gdf(layer, req)\nsmall = small[[\"building_id\", \"height_m\", \"use\", \"geometry\"]]\nprint(small.memory_usage(deep=True).sum() \u002F 1e6, \"MB\")\n",[181,784,785,795,799,809,831,861,871,898],{"__ignoreMap":198},[202,786,787,789,791,793],{"class":91,"line":204},[202,788,224],{"class":207},[202,790,227],{"class":211},[202,792,208],{"class":207},[202,794,490],{"class":211},[202,796,797],{"class":91,"line":221},[202,798,239],{"emptyLinePlaceholder":238},[202,800,801,804,806],{"class":91,"line":235},[202,802,803],{"class":211},"req ",[202,805,248],{"class":207},[202,807,808],{"class":211}," (QgsFeatureRequest()\n",[202,810,811,814,817,820,823,825,828],{"class":91,"line":242},[202,812,813],{"class":211},"       .setSubsetOfAttributes([",[202,815,816],{"class":254},"\"building_id\"",[202,818,819],{"class":211},", ",[202,821,822],{"class":254},"\"height_m\"",[202,824,819],{"class":211},[202,826,827],{"class":254},"\"use\"",[202,829,830],{"class":211},"], layer.fields())\n",[202,832,833,836,839,842,845,847,849,851,854,856,859],{"class":91,"line":267},[202,834,835],{"class":211},"       .setFilterExpression(",[202,837,838],{"class":254},"'\"use\" IN (",[202,840,841],{"class":261},"\\'",[202,843,844],{"class":254},"residential",[202,846,841],{"class":261},[202,848,819],{"class":254},[202,850,841],{"class":261},[202,852,853],{"class":254},"mixed",[202,855,841],{"class":261},[202,857,858],{"class":254},")'",[202,860,349],{"class":211},[202,862,863,866,868],{"class":91,"line":278},[202,864,865],{"class":211},"small ",[202,867,248],{"class":207},[202,869,870],{"class":211}," layer_to_gdf(layer, req)\n",[202,872,873,875,877,880,882,884,886,888,890,892,895],{"class":91,"line":291},[202,874,865],{"class":211},[202,876,248],{"class":207},[202,878,879],{"class":211}," small[[",[202,881,816],{"class":254},[202,883,819],{"class":211},[202,885,822],{"class":254},[202,887,819],{"class":211},[202,889,827],{"class":254},[202,891,819],{"class":211},[202,893,894],{"class":254},"\"geometry\"",[202,896,897],{"class":211},"]]\n",[202,899,900,902,905,908,910,913,916,919,922,924,927],{"class":91,"line":296},[202,901,281],{"class":261},[202,903,904],{"class":211},"(small.memory_usage(",[202,906,907],{"class":337},"deep",[202,909,248],{"class":207},[202,911,912],{"class":261},"True",[202,914,915],{"class":211},").sum() ",[202,917,918],{"class":207},"\u002F",[202,920,921],{"class":261}," 1e6",[202,923,819],{"class":211},[202,925,926],{"class":254},"\"MB\"",[202,928,570],{"class":211},[14,930,931,933,934,937,938,184],{},[408,932,410],{}," With an attribute subset, the provider leaves other columns as NULL, so they still appear in the frame — selecting the wanted columns afterwards drops them. The expression filter is pushed to the provider where possible, so only matching features are read. For attribute-only analysis, add the ",[181,935,936],{},"NoGeometry"," flag and build a plain pandas DataFrame instead, as in ",[21,939,941],{"href":940},"\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fanalyse-attribute-table-with-pandas-pyqgis\u002F","analysing an attribute table with pandas",[161,943,945],{"id":944},"clean-up-nulls-and-dates","Clean up NULLs and dates",[14,947,948,949,952,953,956,957,960],{},"QGIS returns empty attributes as ",[181,950,951],{},"NULL"," QVariants on QGIS 3 and dates as ",[181,954,955],{},"QDate"," or ",[181,958,959],{},"QDateTime",". Pandas does not understand either, so columns can end up with object dtype holding Qt objects.",[193,962,964],{"className":195,"code":963,"language":197,"meta":198,"style":198},"from qgis.PyQt.QtCore import QDate, QDateTime\n\ndef py(v):\n    if v is None or (hasattr(v, \"isNull\") and v.isNull()):\n        return None\n    if isinstance(v, QDateTime):\n        return v.toPyDateTime()\n    if isinstance(v, QDate):\n        return v.toPyDate()\n    return v\n\ndef layer_to_gdf_clean(layer, request=None):\n    gdf = layer_to_gdf(layer, request)\n    for col in gdf.columns.drop(\"geometry\"):\n        if gdf[col].dtype == object:\n            gdf[col] = gdf[col].map(py)\n    return gdf.convert_dtypes()\n\ngdf = layer_to_gdf_clean(layer)\nprint(gdf.dtypes)\n",[181,965,966,978,982,992,1028,1036,1046,1053,1062,1069,1076,1080,1095,1104,1120,1136,1146,1153,1158,1168],{"__ignoreMap":198},[202,967,968,970,973,975],{"class":91,"line":204},[202,969,224],{"class":207},[202,971,972],{"class":211}," qgis.PyQt.QtCore ",[202,974,208],{"class":207},[202,976,977],{"class":211}," QDate, QDateTime\n",[202,979,980],{"class":91,"line":221},[202,981,239],{"emptyLinePlaceholder":238},[202,983,984,986,989],{"class":91,"line":235},[202,985,464],{"class":207},[202,987,988],{"class":467}," py",[202,990,991],{"class":211},"(v):\n",[202,993,994,996,999,1002,1004,1007,1010,1013,1016,1019,1022,1025],{"class":91,"line":242},[202,995,355],{"class":207},[202,997,998],{"class":211}," v ",[202,1000,1001],{"class":207},"is",[202,1003,567],{"class":261},[202,1005,1006],{"class":207}," or",[202,1008,1009],{"class":211}," (",[202,1011,1012],{"class":261},"hasattr",[202,1014,1015],{"class":211},"(v, ",[202,1017,1018],{"class":254},"\"isNull\"",[202,1020,1021],{"class":211},") ",[202,1023,1024],{"class":207},"and",[202,1026,1027],{"class":211}," v.isNull()):\n",[202,1029,1030,1033],{"class":91,"line":267},[202,1031,1032],{"class":207},"        return",[202,1034,1035],{"class":261}," None\n",[202,1037,1038,1040,1043],{"class":91,"line":278},[202,1039,355],{"class":207},[202,1041,1042],{"class":261}," isinstance",[202,1044,1045],{"class":211},"(v, QDateTime):\n",[202,1047,1048,1050],{"class":91,"line":291},[202,1049,1032],{"class":207},[202,1051,1052],{"class":211}," v.toPyDateTime()\n",[202,1054,1055,1057,1059],{"class":91,"line":296},[202,1056,355],{"class":207},[202,1058,1042],{"class":261},[202,1060,1061],{"class":211},"(v, QDate):\n",[202,1063,1064,1066],{"class":91,"line":320},[202,1065,1032],{"class":207},[202,1067,1068],{"class":211}," v.toPyDate()\n",[202,1070,1071,1073],{"class":91,"line":352},[202,1072,626],{"class":207},[202,1074,1075],{"class":211}," v\n",[202,1077,1078],{"class":91,"line":367},[202,1079,239],{"emptyLinePlaceholder":238},[202,1081,1082,1084,1087,1089,1091,1093],{"class":91,"line":387},[202,1083,464],{"class":207},[202,1085,1086],{"class":467}," layer_to_gdf_clean",[202,1088,471],{"class":211},[202,1090,248],{"class":207},[202,1092,476],{"class":261},[202,1094,364],{"class":211},[202,1096,1097,1099,1101],{"class":91,"line":591},[202,1098,323],{"class":211},[202,1100,248],{"class":207},[202,1102,1103],{"class":211}," layer_to_gdf(layer, request)\n",[202,1105,1106,1108,1111,1113,1116,1118],{"class":91,"line":623},[202,1107,531],{"class":207},[202,1109,1110],{"class":211}," col ",[202,1112,537],{"class":207},[202,1114,1115],{"class":211}," gdf.columns.drop(",[202,1117,894],{"class":254},[202,1119,364],{"class":211},[202,1121,1122,1125,1128,1130,1133],{"class":91,"line":648},[202,1123,1124],{"class":207},"        if",[202,1126,1127],{"class":211}," gdf[col].dtype ",[202,1129,305],{"class":207},[202,1131,1132],{"class":261}," object",[202,1134,1135],{"class":211},":\n",[202,1137,1138,1141,1143],{"class":91,"line":653},[202,1139,1140],{"class":211},"            gdf[col] ",[202,1142,248],{"class":207},[202,1144,1145],{"class":211}," gdf[col].map(py)\n",[202,1147,1148,1150],{"class":91,"line":664},[202,1149,626],{"class":207},[202,1151,1152],{"class":211}," gdf.convert_dtypes()\n",[202,1154,1156],{"class":91,"line":1155},18,[202,1157,239],{"emptyLinePlaceholder":238},[202,1159,1161,1163,1165],{"class":91,"line":1160},19,[202,1162,656],{"class":211},[202,1164,248],{"class":207},[202,1166,1167],{"class":211}," layer_to_gdf_clean(layer)\n",[202,1169,1171,1173],{"class":91,"line":1170},20,[202,1172,281],{"class":261},[202,1174,1175],{"class":211},"(gdf.dtypes)\n",[14,1177,1178,1180,1181,1184,1185,1188,1189,1192,1193,184],{},[408,1179,410],{}," Mapping only object-dtype columns keeps numeric columns untouched and fast. After conversion, ",[181,1182,1183],{},"convert_dtypes()"," lets pandas choose nullable types — ",[181,1186,1187],{},"Int64"," for integer columns with missing values instead of falling back to float, ",[181,1190,1191],{},"string"," for text, proper datetime types for dates converted to Python objects. The NULL rules behind this are explained in ",[21,1194,1196],{"href":1195},"\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fhandle-null-values-and-qvariant-pyqgis\u002F","handling NULL values and QVariant",[161,1198,1200],{"id":1199},"keep-the-crs-right","Keep the CRS right",[14,1202,1203],{},"A GeoDataFrame without a CRS silently breaks every later reprojection and overlay. Always carry the layer's CRS across, and check it after conversion.",[14,1205,1206],{},[29,1207,1210,1213,1216,1219,1226,1229,1232,1236,1241,1246,1248,1251,1255,1259,1263,1266,1270,1273,1276],{"viewBox":1208,"role":32,"ariaLabel":1209,"xmlns":34},"0 0 760 230","The layer CRS passes to GeoPandas as an authority id or WKT and becomes a pyproj CRS, with an unknown CRS treated as an error",[36,1211,1212],{},"CRS from QGIS to pyproj",[40,1214,1215],{},"QGIS describes the layer CRS as a QgsCoordinateReferenceSystem. Passing its authority id such as EPSG:25832 or its WKT2 string to GeoPandas creates the equivalent pyproj CRS. Both libraries use PROJ underneath, so the definitions agree. A layer with an unknown CRS produces a frame with crs None, which should be treated as an error.",[44,1217],{"x":46,"y":46,"width":47,"height":1218,"fill":49},"230",[51,1220,1221],{},[54,1222,1224],{"id":1223,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"gpdCrsArrow",[63,1225],{"d":65,"fill":66},[68,1227,1228],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Same PROJ underneath, different wrappers",[44,1230],{"x":104,"y":716,"width":740,"height":1231,"rx":58,"fill":107,"stroke":108,"style":84},"110",[68,1233,1235],{"x":105,"y":1234,"style":157,"fill":108,"textAnchor":74},"105.6","QgsCoordinateReferenceSystem",[68,1237,1240],{"x":105,"y":1238,"style":1239,"fill":66,"textAnchor":74},"131.6","text-anchor:middle;font-size:9.5px;font-family:monospace","authid() \u002F toWkt()",[91,1242],{"x1":149,"y1":1243,"x2":1244,"y2":1243,"stroke":66,"style":1245},"115","286","stroke-width:1.8;marker-end:url(#gpdCrsArrow)",[44,1247],{"x":78,"y":716,"width":80,"height":1231,"rx":58,"fill":741,"stroke":742,"style":84},[68,1249,1191],{"x":70,"y":1250,"style":88,"fill":742,"textAnchor":74},"92.78",[68,1252,1254],{"x":70,"y":1253,"style":117,"fill":66,"textAnchor":74},"118.78","EPSG:25832",[68,1256,1258],{"x":70,"y":1257,"style":122,"fill":83,"textAnchor":74},"144.78","or WKT2",[91,1260],{"x1":1261,"y1":1243,"x2":1262,"y2":1243,"stroke":66,"style":1245},"470","512",[44,1264],{"x":1265,"y":716,"width":740,"height":1231,"rx":58,"fill":127,"stroke":128,"style":84},"516",[68,1267,1269],{"x":1268,"y":1250,"style":88,"fill":128,"textAnchor":74},"626","pyproj CRS",[68,1271,1272],{"x":1268,"y":1253,"style":117,"fill":66,"textAnchor":74},"gdf.crs",[68,1274,1275],{"x":1268,"y":1257,"style":122,"fill":83,"textAnchor":74},"to_epsg()",[68,1277,1279],{"x":70,"y":1278,"style":122,"fill":83,"textAnchor":74},"204","an invalid layer CRS gives crs None — stop there",[193,1281,1283],{"className":195,"code":1282,"language":197,"meta":198,"style":198},"crs = layer.crs()\nif not crs.isValid():\n    raise ValueError(f\"{layer.name()} has no CRS; set it before converting\")\ngdf = layer_to_gdf(layer)\ngdf = gdf.set_crs(crs.authid() if crs.authid() else crs.toWkt(), allow_override=True)\nassert gdf.crs is not None\nprint(gdf.crs.name, gdf.crs.axis_info[0].unit_name)\n",[181,1284,1285,1295,1304,1334,1342,1370,1384],{"__ignoreMap":198},[202,1286,1287,1290,1292],{"class":91,"line":204},[202,1288,1289],{"class":211},"crs ",[202,1291,248],{"class":207},[202,1293,1294],{"class":211}," layer.crs()\n",[202,1296,1297,1299,1301],{"class":91,"line":221},[202,1298,299],{"class":207},[202,1300,314],{"class":207},[202,1302,1303],{"class":211}," crs.isValid():\n",[202,1305,1306,1309,1312,1314,1317,1320,1323,1326,1329,1332],{"class":91,"line":235},[202,1307,1308],{"class":207},"    raise",[202,1310,1311],{"class":261}," ValueError",[202,1313,373],{"class":211},[202,1315,1316],{"class":207},"f",[202,1318,1319],{"class":254},"\"",[202,1321,1322],{"class":261},"{",[202,1324,1325],{"class":211},"layer.name()",[202,1327,1328],{"class":261},"}",[202,1330,1331],{"class":254}," has no CRS; set it before converting\"",[202,1333,570],{"class":211},[202,1335,1336,1338,1340],{"class":91,"line":242},[202,1337,656],{"class":211},[202,1339,248],{"class":207},[202,1341,661],{"class":211},[202,1343,1344,1346,1348,1351,1353,1356,1358,1361,1364,1366,1368],{"class":91,"line":267},[202,1345,656],{"class":211},[202,1347,248],{"class":207},[202,1349,1350],{"class":211}," gdf.set_crs(crs.authid() ",[202,1352,299],{"class":207},[202,1354,1355],{"class":211}," crs.authid() ",[202,1357,564],{"class":207},[202,1359,1360],{"class":211}," crs.toWkt(), ",[202,1362,1363],{"class":337},"allow_override",[202,1365,248],{"class":207},[202,1367,912],{"class":261},[202,1369,570],{"class":211},[202,1371,1372,1375,1378,1380,1382],{"class":91,"line":278},[202,1373,1374],{"class":207},"assert",[202,1376,1377],{"class":211}," gdf.crs ",[202,1379,1001],{"class":207},[202,1381,314],{"class":207},[202,1383,1035],{"class":261},[202,1385,1386,1388,1391,1393],{"class":91,"line":291},[202,1387,281],{"class":261},[202,1389,1390],{"class":211},"(gdf.crs.name, gdf.crs.axis_info[",[202,1392,46],{"class":261},[202,1394,1395],{"class":211},"].unit_name)\n",[14,1397,1398,1400,1401,1404,1405,1409],{},[408,1399,410],{}," Checking validity first catches layers loaded from files without a ",[181,1402,1403],{},".prj"," or with an unrecognised definition; fixing those is covered in ",[21,1406,1408],{"href":1407},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fhandling-missing-crs-in-pyqgis\u002F","handling missing CRS",". The unit name confirms whether distances in GeoPandas will be metres or degrees. Both libraries delegate to PROJ, so transformations done in either agree, provided both use the same PROJ data files — on most installations they do.",[161,1411,1413],{"id":1412},"convert-every-layer-in-a-project","Convert every layer in a project",[14,1415,1416],{},"Analyses that combine several layers — buildings, parcels, flood zones — often start by pulling each of them into a frame. A small loop over the project's vector layers, choosing the route per layer, does it in one go and reports what it did.",[193,1418,1420],{"className":195,"code":1419,"language":197,"meta":198,"style":198},"from qgis.core import QgsVectorLayer\n\nframes = {}\nfor lyr in QgsProject.instance().mapLayers().values():\n    if not isinstance(lyr, QgsVectorLayer) or not lyr.isSpatial():\n        continue\n    src = QgsProviderRegistry.instance().decodeUri(lyr.providerType(), lyr.source())\n    direct = (lyr.providerType() == \"ogr\" and not lyr.isModified()\n              and not src.get(\"subset\") and lyr.fields().count() == lyr.dataProvider().fields().count())\n    frames[lyr.name()] = (gpd.read_file(src[\"path\"], layer=src.get(\"layerName\"))\n                          if direct else layer_to_gdf_clean(lyr))\n    print(f\"{lyr.name():\u003C24} {'read_file' if direct else 'features':\u003C9} {len(frames[lyr.name()]):>8} rows\")\n",[181,1421,1422,1433,1437,1447,1459,1477,1482,1492,1513,1537,1562,1575],{"__ignoreMap":198},[202,1423,1424,1426,1428,1430],{"class":91,"line":204},[202,1425,224],{"class":207},[202,1427,227],{"class":211},[202,1429,208],{"class":207},[202,1431,1432],{"class":211}," QgsVectorLayer\n",[202,1434,1435],{"class":91,"line":221},[202,1436,239],{"emptyLinePlaceholder":238},[202,1438,1439,1442,1444],{"class":91,"line":235},[202,1440,1441],{"class":211},"frames ",[202,1443,248],{"class":207},[202,1445,1446],{"class":211}," {}\n",[202,1448,1449,1451,1454,1456],{"class":91,"line":242},[202,1450,602],{"class":207},[202,1452,1453],{"class":211}," lyr ",[202,1455,537],{"class":207},[202,1457,1458],{"class":211}," QgsProject.instance().mapLayers().values():\n",[202,1460,1461,1463,1465,1467,1470,1472,1474],{"class":91,"line":267},[202,1462,355],{"class":207},[202,1464,314],{"class":207},[202,1466,1042],{"class":261},[202,1468,1469],{"class":211},"(lyr, QgsVectorLayer) ",[202,1471,503],{"class":207},[202,1473,314],{"class":207},[202,1475,1476],{"class":211}," lyr.isSpatial():\n",[202,1478,1479],{"class":91,"line":278},[202,1480,1481],{"class":207},"        continue\n",[202,1483,1484,1487,1489],{"class":91,"line":291},[202,1485,1486],{"class":211},"    src ",[202,1488,248],{"class":207},[202,1490,1491],{"class":211}," QgsProviderRegistry.instance().decodeUri(lyr.providerType(), lyr.source())\n",[202,1493,1494,1497,1499,1502,1504,1506,1508,1510],{"class":91,"line":296},[202,1495,1496],{"class":211},"    direct ",[202,1498,248],{"class":207},[202,1500,1501],{"class":211}," (lyr.providerType() ",[202,1503,305],{"class":207},[202,1505,308],{"class":254},[202,1507,311],{"class":207},[202,1509,314],{"class":207},[202,1511,1512],{"class":211}," lyr.isModified()\n",[202,1514,1515,1518,1520,1523,1525,1527,1529,1532,1534],{"class":91,"line":320},[202,1516,1517],{"class":207},"              and",[202,1519,314],{"class":207},[202,1521,1522],{"class":211}," src.get(",[202,1524,361],{"class":254},[202,1526,1021],{"class":211},[202,1528,1024],{"class":207},[202,1530,1531],{"class":211}," lyr.fields().count() ",[202,1533,305],{"class":207},[202,1535,1536],{"class":211}," lyr.dataProvider().fields().count())\n",[202,1538,1539,1542,1544,1547,1549,1551,1553,1555,1558,1560],{"class":91,"line":352},[202,1540,1541],{"class":211},"    frames[lyr.name()] ",[202,1543,248],{"class":207},[202,1545,1546],{"class":211}," (gpd.read_file(src[",[202,1548,331],{"class":254},[202,1550,334],{"class":211},[202,1552,338],{"class":337},[202,1554,248],{"class":207},[202,1556,1557],{"class":211},"src.get(",[202,1559,346],{"class":254},[202,1561,349],{"class":211},[202,1563,1564,1567,1570,1572],{"class":91,"line":367},[202,1565,1566],{"class":207},"                          if",[202,1568,1569],{"class":211}," direct ",[202,1571,564],{"class":207},[202,1573,1574],{"class":211}," layer_to_gdf_clean(lyr))\n",[202,1576,1577,1579,1581,1583,1585,1587,1590,1593,1595,1598,1601,1604,1606,1608,1611,1614,1616,1619,1622,1625,1627,1630],{"class":91,"line":387},[202,1578,390],{"class":261},[202,1580,373],{"class":211},[202,1582,1316],{"class":207},[202,1584,1319],{"class":254},[202,1586,1322],{"class":261},[202,1588,1589],{"class":211},"lyr.name()",[202,1591,1592],{"class":207},":\u003C24",[202,1594,1328],{"class":261},[202,1596,1597],{"class":261}," {",[202,1599,1600],{"class":254},"'read_file'",[202,1602,1603],{"class":207}," if",[202,1605,1569],{"class":211},[202,1607,564],{"class":207},[202,1609,1610],{"class":254}," 'features'",[202,1612,1613],{"class":207},":\u003C9",[202,1615,1328],{"class":261},[202,1617,1618],{"class":261}," {len",[202,1620,1621],{"class":211},"(frames[lyr.name()])",[202,1623,1624],{"class":207},":>8",[202,1626,1328],{"class":261},[202,1628,1629],{"class":254}," rows\"",[202,1631,570],{"class":211},[14,1633,1634,1636,1637,1640,1641,1644],{},[408,1635,410],{}," The direct route is taken only when it is guaranteed to match what QGIS shows: an OGR source, no pending edits, no subset filter, and no joined or expression fields — detected by comparing the layer's field count with the provider's. Everything else goes through the feature route. Keying the dictionary by layer name keeps later code readable (",[181,1638,1639],{},"frames[\"buildings\"]","), though names are not guaranteed unique in a project; key by ",[181,1642,1643],{},"lyr.id()"," if yours repeat.",[161,1646,1648],{"id":1647},"decide-where-the-analysis-should-run","Decide where the analysis should run",[14,1650,1651,1652,1656],{},"Converting to GeoPandas is worthwhile when the next steps benefit from it: group-bys and merges on attributes, vectorised arithmetic across columns, statistical models, or libraries that expect a frame. For standard geoprocessing — buffers, overlays, dissolves on large layers — QGIS's Processing algorithms are just as fast or faster, keep data in QGIS's own types, and appear in the Processing history. A good rule is to stay in QGIS until a step genuinely needs pandas, convert once, do the pandas work, and ",[21,1653,1655],{"href":1654},"\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fload-geodataframe-into-qgis-pyqgis\u002F","bring the result back"," — rather than shuttling data back and forth between every step.",[161,1658,1660],{"id":1659},"measure-and-choose","Measure and choose",[14,1662,1663],{},"Which route is faster depends on the layer size and source. A quick timing on your own data settles it.",[193,1665,1667],{"className":195,"code":1666,"language":197,"meta":198,"style":198},"import time\n\nt = time.perf_counter()\na = gpd.read_file(parts[\"path\"], layer=parts.get(\"layerName\"))\nt1 = time.perf_counter() - t\nt = time.perf_counter()\nb = layer_to_gdf(layer)\nt2 = time.perf_counter() - t\nprint(f\"read_file {t1:.2f} s · feature route {t2:.2f} s · {len(a)} rows\")\n",[181,1668,1669,1676,1680,1690,1713,1729,1737,1746,1759],{"__ignoreMap":198},[202,1670,1671,1673],{"class":91,"line":204},[202,1672,208],{"class":207},[202,1674,1675],{"class":211}," time\n",[202,1677,1678],{"class":91,"line":221},[202,1679,239],{"emptyLinePlaceholder":238},[202,1681,1682,1685,1687],{"class":91,"line":235},[202,1683,1684],{"class":211},"t ",[202,1686,248],{"class":207},[202,1688,1689],{"class":211}," time.perf_counter()\n",[202,1691,1692,1695,1697,1699,1701,1703,1705,1707,1709,1711],{"class":91,"line":242},[202,1693,1694],{"class":211},"a ",[202,1696,248],{"class":207},[202,1698,328],{"class":211},[202,1700,331],{"class":254},[202,1702,334],{"class":211},[202,1704,338],{"class":337},[202,1706,248],{"class":207},[202,1708,343],{"class":211},[202,1710,346],{"class":254},[202,1712,349],{"class":211},[202,1714,1715,1718,1720,1723,1726],{"class":91,"line":267},[202,1716,1717],{"class":211},"t1 ",[202,1719,248],{"class":207},[202,1721,1722],{"class":211}," time.perf_counter() ",[202,1724,1725],{"class":207},"-",[202,1727,1728],{"class":211}," t\n",[202,1730,1731,1733,1735],{"class":91,"line":278},[202,1732,1684],{"class":211},[202,1734,248],{"class":207},[202,1736,1689],{"class":211},[202,1738,1739,1742,1744],{"class":91,"line":291},[202,1740,1741],{"class":211},"b ",[202,1743,248],{"class":207},[202,1745,661],{"class":211},[202,1747,1748,1751,1753,1755,1757],{"class":91,"line":296},[202,1749,1750],{"class":211},"t2 ",[202,1752,248],{"class":207},[202,1754,1722],{"class":211},[202,1756,1725],{"class":207},[202,1758,1728],{"class":211},[202,1760,1761,1763,1765,1767,1770,1772,1775,1778,1780,1783,1785,1788,1790,1792,1795,1798,1801,1803,1805],{"class":91,"line":320},[202,1762,281],{"class":261},[202,1764,373],{"class":211},[202,1766,1316],{"class":207},[202,1768,1769],{"class":254},"\"read_file ",[202,1771,1322],{"class":261},[202,1773,1774],{"class":211},"t1",[202,1776,1777],{"class":207},":.2f",[202,1779,1328],{"class":261},[202,1781,1782],{"class":254}," s · feature route ",[202,1784,1322],{"class":261},[202,1786,1787],{"class":211},"t2",[202,1789,1777],{"class":207},[202,1791,1328],{"class":261},[202,1793,1794],{"class":254}," s · ",[202,1796,1797],{"class":261},"{len",[202,1799,1800],{"class":211},"(a)",[202,1802,1328],{"class":261},[202,1804,1629],{"class":254},[202,1806,570],{"class":211},[14,1808,1809,1811,1812,1815,1816,1818,1819,1822,1823,956,1826,1829],{},[408,1810,410],{}," For file-backed layers of more than a few thousand features, ",[181,1813,1814],{},"read_file"," usually wins by a wide margin because the whole read happens in compiled code. The feature route's cost grows with column count and geometry complexity; it remains the only option for data that exists only in QGIS. For very large datasets where even ",[181,1817,1814],{}," is slow, read with ",[181,1820,1821],{},"use_arrow=True"," (pyogrio) or filter with a ",[181,1824,1825],{},"bbox",[181,1827,1828],{},"where"," argument at read time.",[161,1831,1833],{"id":1832},"qgis-version-compatibility","QGIS version compatibility",[14,1835,1836,1837,1840,1841,1844,1845,1847],{},"The conversion code is plain PyQGIS plus GeoPandas and works on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. Shapely 2's vectorised ",[181,1838,1839],{},"from_wkb"," is required; on shapely 1.8, use ",[181,1842,1843],{},"shapely.wkb.loads"," per feature. On QGIS 4, NULL values arrive as ",[181,1846,476],{}," and the cleanup function leaves them as is.",[161,1849,1851],{"id":1850},"troubleshooting","Troubleshooting",[166,1853,1854,1862,1872,1881],{},[169,1855,1856,1861],{},[408,1857,1858,184],{},[181,1859,1860],{},"ModuleNotFoundError: geopandas"," It is installed in a different Python; install into QGIS's interpreter.",[169,1863,1864,1871],{},[408,1865,1866,1867,1870],{},"Columns of ",[181,1868,1869],{},"QVariant"," objects."," NULL values were not converted; use the cleanup function.",[169,1873,1874,1877,1878,1880],{},[408,1875,1876],{},"Row count differs from QGIS."," The layer has a subset filter or unsaved edits that ",[181,1879,1814],{}," cannot see.",[169,1882,1883,1886],{},[408,1884,1885],{},"The frame has no CRS."," The layer CRS was invalid; fix it in QGIS before converting.",[161,1888,1890],{"id":1889},"conclusion","Conclusion",[14,1892,1893,1894,1897],{},"Read the layer's source with ",[181,1895,1896],{},"geopandas.read_file"," when it is a plain, unedited file or table; otherwise iterate features with a trimmed request, convert geometry through WKB in one vectorised call, clean up NULLs and Qt dates, and always carry the CRS across.",[161,1899,1901],{"id":1900},"frequently-asked-questions","Frequently Asked Questions",[14,1903,1904,1907,1908,184],{},[408,1905,1906],{},"Does editing the GeoDataFrame change the QGIS layer?","\nNo. The frame is a copy. Write results back as described in ",[21,1909,1910],{"href":1654},"loading a GeoDataFrame into QGIS",[14,1912,1913,1916,1917,1920],{},[408,1914,1915],{},"Can I convert a PostGIS layer with read_file?","\nUse ",[181,1918,1919],{},"gpd.read_postgis"," with an SQL query and a database connection; the layer's URI gives you the connection details.",[14,1922,1923,1926],{},[408,1924,1925],{},"Is there a built-in QGIS function for this?","\nNot in core. Small helper functions like the ones above are the standard approach.",[14,1928,1929,1932],{},[408,1930,1931],{},"What about very large layers?","\nFilter at the source, read only needed columns, and consider Dask-GeoPandas or processing in chunks.",[161,1934,1936],{"id":1935},"related","Related",[166,1938,1939,1944,1949,1955,1961],{},[169,1940,1941,1943],{},[21,1942,24],{"href":23}," — the guide this recipe belongs to",[169,1945,1946],{},[21,1947,1948],{"href":1654},"Load a GeoDataFrame into QGIS",[169,1950,1951],{},[21,1952,1954],{"href":1953},"\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fconvert-between-qgsgeometry-and-shapely\u002F","Convert Between QgsGeometry and Shapely",[169,1956,1957],{},[21,1958,1960],{"href":1959},"\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fread-feature-attributes-and-geometry-pyqgis\u002F","Read Feature Attributes and Geometry in PyQGIS",[169,1962,1963],{},[21,1964,1965],{"href":174},"Install Python Packages into QGIS",[1967,1968,1969],"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 pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":198,"searchDepth":221,"depth":221,"links":1971},[1972,1973,1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985],{"id":163,"depth":221,"text":164},{"id":187,"depth":221,"text":188},{"id":427,"depth":221,"text":428},{"id":775,"depth":221,"text":776},{"id":944,"depth":221,"text":945},{"id":1199,"depth":221,"text":1200},{"id":1412,"depth":221,"text":1413},{"id":1647,"depth":221,"text":1648},{"id":1659,"depth":221,"text":1660},{"id":1832,"depth":221,"text":1833},{"id":1850,"depth":221,"text":1851},{"id":1889,"depth":221,"text":1890},{"id":1900,"depth":221,"text":1901},{"id":1935,"depth":221,"text":1936},"Move features from a QgsVectorLayer into a GeoPandas GeoDataFrame — reading the source file directly when you can, converting feature by feature through WKB when you cannot, keeping the CRS, handling NULLs and dates, and choosing the fastest route for large layers.","md",{"slug":1989,"type":1990,"breadcrumb":1991,"datePublished":1992,"dateModified":1992},"convert-layer-to-geopandas-geodataframe-pyqgis","article","Convert a Layer to a GeoDataFrame","2026-10-02","\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fconvert-layer-to-geopandas-geodataframe-pyqgis",{"title":5,"description":1986},"spatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fconvert-layer-to-geopandas-geodataframe-pyqgis\u002Findex","jh3cMW3NvBcFKOOc7C659eEz9waEXXj4zkrfqEE-hDg",1790966264357]