[{"data":1,"prerenderedAt":1934},["ShallowReactive",2],{"doc:\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fhandle-null-values-and-qvariant-pyqgis":3},{"id":4,"title":5,"body":6,"description":1923,"extension":1924,"meta":1925,"navigation":275,"path":1930,"seo":1931,"stem":1932,"__hash__":1933},"docs\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fhandle-null-values-and-qvariant-pyqgis\u002Findex.md","Handle NULL Values and QVariant in PyQGIS",{"type":7,"value":8,"toc":1909},"minimark",[9,13,48,60,217,222,232,236,239,514,556,575,579,606,672,932,967,971,981,1125,1151,1155,1158,1211,1312,1325,1329,1332,1460,1485,1489,1492,1689,1709,1713,1741,1745,1801,1805,1817,1821,1841,1855,1861,1871,1875,1905],[10,11,5],"h1",{"id":12},"handle-null-values-and-qvariant-in-pyqgis",[14,15,16,17,21,22,25,26,29,30,32,33,35,36,39,40,43,44,47],"p",{},"Sooner or later every PyQGIS script meets an attribute that is empty, and on QGIS 3 that empty value is not Python's ",[18,19,20],"code",{},"None",". It is ",[18,23,24],{},"NULL"," — a Qt ",[18,27,28],{},"QVariant"," that prints as ",[18,31,24],{},", is falsy, compares unequal to ",[18,34,20],{},", and makes arithmetic raise a ",[18,37,38],{},"TypeError",". Dates arrive as ",[18,41,42],{},"QDate",", timestamps as ",[18,45,46],{},"QDateTime",", and some numeric types as Qt values too. Code written as if attributes were plain Python values works on clean test data and fails on the first real table.",[14,49,50,51,56,57,59],{},"This recipe belongs to ",[52,53,55],"a",{"href":54},"\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002F","Features, Geometries & Memory Layers",". It explains where ",[18,58,24],{}," comes from, how to test for it on both QGIS 3 and QGIS 4, how to convert Qt types into Python ones, and how to write a NULL back to a field.",[14,61,62],{},[63,64,69,73,77,84,101,110,119,125,130,134,138,145,153,157,162,166,172,176,184,189,192,196,201,206,209,213],"svg",{"viewBox":65,"role":66,"ariaLabel":67,"xmlns":68},"0 0 760 280","img","A missing value in the data source becomes a null QVariant in the provider, which reaches Python as NULL on QGIS 3 and as None on QGIS 4","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[70,71,72],"title",{},"Where an empty value comes from",[74,75,76],"desc",{},"A database or file stores a missing value, such as SQL NULL in GeoPackage or PostGIS, an empty cell in a CSV read with type detection, or a blank shapefile field. The data provider turns it into a QVariant with no value. On QGIS 3 the Python bindings hand that to your code as the NULL object, which is falsy and not equal to None. On QGIS 4 the bindings convert it to Python None.",[78,79],"rect",{"x":80,"y":80,"width":81,"height":82,"fill":83},"0","760","280","#f6f3ea",[85,86,87],"defs",{},[88,89,96],"marker",{"id":90,"viewBox":91,"refX":92,"refY":93,"markerWidth":94,"markerHeight":94,"orient":95},"nullOriginArrow","0 0 10 10","8","5","7","auto-start-reverse",[97,98],"path",{"d":99,"fill":100},"M0 0 L10 5 L0 10 z","#2f3b35",[102,103,109],"text",{"x":104,"y":105,"style":106,"fill":107,"textAnchor":108},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","One missing value, two Python faces",[78,111],{"x":112,"y":113,"width":114,"height":115,"rx":92,"fill":116,"stroke":117,"style":118},"24","56","210","150","#fffdf7","#59645f","stroke-width:2",[102,120,124],{"x":121,"y":122,"style":123,"fill":107,"textAnchor":108},"129","104.855","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","data source",[102,126,129],{"x":121,"y":127,"style":128,"fill":100,"textAnchor":108},"124.805","text-anchor:middle;font-size:10.5px;font-family:sans-serif","SQL NULL",[102,131,133],{"x":121,"y":132,"style":128,"fill":100,"textAnchor":108},"144.755","empty CSV cell",[102,135,137],{"x":121,"y":136,"style":128,"fill":100,"textAnchor":108},"164.705","blank DBF field",[139,140],"line",{"x1":141,"y1":142,"x2":143,"y2":142,"stroke":100,"style":144},"234","131","276","stroke-width:1.8;marker-end:url(#nullOriginArrow)",[78,146],{"x":147,"y":148,"width":149,"height":150,"rx":92,"fill":151,"stroke":152,"style":118},"284","76","192","110","#eef7f4","#0f766e",[102,154,156],{"x":104,"y":155,"style":123,"fill":152,"textAnchor":108},"114.83","provider",[102,158,161],{"x":104,"y":159,"style":160,"fill":100,"textAnchor":108},"134.78","text-anchor:middle;font-size:10.0px;font-family:monospace","QVariant()",[102,163,165],{"x":104,"y":164,"style":128,"fill":117,"textAnchor":108},"154.73","no value",[139,167],{"x1":168,"y1":169,"x2":170,"y2":171,"stroke":100,"style":144},"476","116","520","86",[139,173],{"x1":168,"y1":174,"x2":170,"y2":175,"stroke":100,"style":144},"146","176",[78,177],{"x":178,"y":179,"width":180,"height":181,"rx":92,"fill":182,"stroke":183,"style":118},"528","52","208","72","#fdf2e2","#b45309",[102,185,188],{"x":186,"y":187,"style":123,"fill":183,"textAnchor":108},"632","71.83","QGIS 3",[102,190,24],{"x":186,"y":191,"style":160,"fill":100,"textAnchor":108},"91.78",[102,193,195],{"x":186,"y":194,"style":128,"fill":117,"textAnchor":108},"111.73","falsy, ≠ None",[78,197],{"x":178,"y":198,"width":180,"height":181,"rx":92,"fill":199,"stroke":200,"style":118},"140","#e8efe6","#15803d",[102,202,205],{"x":186,"y":203,"style":123,"fill":204,"textAnchor":108},"159.83","#166534","QGIS 4",[102,207,20],{"x":186,"y":208,"style":160,"fill":100,"textAnchor":108},"179.78",[102,210,212],{"x":186,"y":211,"style":128,"fill":117,"textAnchor":108},"199.73","plain Python",[102,214,216],{"x":104,"y":215,"style":128,"fill":117,"textAnchor":108},"250","code that must run on both has to accept either",[218,219,221],"h2",{"id":220},"prerequisites","Prerequisites",[223,224,225,229],"ul",{},[226,227,228],"li",{},"QGIS 3.34 LTR, 3.40 LTR, or the QGIS 4 series.",[226,230,231],{},"A layer with some empty attributes. A GeoPackage with nullable columns is the most realistic test; shapefiles behave differently, as explained below.",[218,233,235],{"id":234},"test-for-null-reliably","Test for NULL reliably",[14,237,238],{},"The tests that work depend on the QGIS version. A helper that accepts both forms removes the question from the rest of the code.",[240,241,246],"pre",{"className":242,"code":243,"language":244,"meta":245,"style":245},"language-python shiki shiki-themes github-dark","from qgis.core import NULL, QgsProject\n\nlayer = QgsProject.instance().mapLayersByName(\"inspections\")[0]\n\ndef is_null(value):\n    \"\"\"True for QGIS 3 NULL QVariants and for QGIS 4 None.\"\"\"\n    if value is None:\n        return True\n    is_null_method = getattr(value, \"isNull\", None)\n    return bool(is_null_method and is_null_method())\n\nempty, filled = 0, 0\nfor f in layer.getFeatures():\n    if is_null(f[\"inspected_on\"]):\n        empty += 1\n    else:\n        filled += 1\nprint(f\"{empty} never inspected, {filled} inspected\")\n","python","",[18,247,248,270,277,301,306,319,325,343,352,377,395,400,416,431,445,457,465,475],{"__ignoreMap":245},[249,250,252,256,260,263,267],"span",{"class":139,"line":251},1,[249,253,255],{"class":254},"snl16","from",[249,257,259],{"class":258},"s95oV"," qgis.core ",[249,261,262],{"class":254},"import",[249,264,266],{"class":265},"sDLfK"," NULL",[249,268,269],{"class":258},", QgsProject\n",[249,271,273],{"class":139,"line":272},2,[249,274,276],{"emptyLinePlaceholder":275},true,"\n",[249,278,280,283,286,289,293,296,298],{"class":139,"line":279},3,[249,281,282],{"class":258},"layer ",[249,284,285],{"class":254},"=",[249,287,288],{"class":258}," QgsProject.instance().mapLayersByName(",[249,290,292],{"class":291},"sU2Wk","\"inspections\"",[249,294,295],{"class":258},")[",[249,297,80],{"class":265},[249,299,300],{"class":258},"]\n",[249,302,304],{"class":139,"line":303},4,[249,305,276],{"emptyLinePlaceholder":275},[249,307,309,312,316],{"class":139,"line":308},5,[249,310,311],{"class":254},"def",[249,313,315],{"class":314},"svObZ"," is_null",[249,317,318],{"class":258},"(value):\n",[249,320,322],{"class":139,"line":321},6,[249,323,324],{"class":291},"    \"\"\"True for QGIS 3 NULL QVariants and for QGIS 4 None.\"\"\"\n",[249,326,328,331,334,337,340],{"class":139,"line":327},7,[249,329,330],{"class":254},"    if",[249,332,333],{"class":258}," value ",[249,335,336],{"class":254},"is",[249,338,339],{"class":265}," None",[249,341,342],{"class":258},":\n",[249,344,346,349],{"class":139,"line":345},8,[249,347,348],{"class":254},"        return",[249,350,351],{"class":265}," True\n",[249,353,355,358,360,363,366,369,372,374],{"class":139,"line":354},9,[249,356,357],{"class":258},"    is_null_method ",[249,359,285],{"class":254},[249,361,362],{"class":265}," getattr",[249,364,365],{"class":258},"(value, ",[249,367,368],{"class":291},"\"isNull\"",[249,370,371],{"class":258},", ",[249,373,20],{"class":265},[249,375,376],{"class":258},")\n",[249,378,380,383,386,389,392],{"class":139,"line":379},10,[249,381,382],{"class":254},"    return",[249,384,385],{"class":265}," bool",[249,387,388],{"class":258},"(is_null_method ",[249,390,391],{"class":254},"and",[249,393,394],{"class":258}," is_null_method())\n",[249,396,398],{"class":139,"line":397},11,[249,399,276],{"emptyLinePlaceholder":275},[249,401,403,406,408,411,413],{"class":139,"line":402},12,[249,404,405],{"class":258},"empty, filled ",[249,407,285],{"class":254},[249,409,410],{"class":265}," 0",[249,412,371],{"class":258},[249,414,415],{"class":265},"0\n",[249,417,419,422,425,428],{"class":139,"line":418},13,[249,420,421],{"class":254},"for",[249,423,424],{"class":258}," f ",[249,426,427],{"class":254},"in",[249,429,430],{"class":258}," layer.getFeatures():\n",[249,432,434,436,439,442],{"class":139,"line":433},14,[249,435,330],{"class":254},[249,437,438],{"class":258}," is_null(f[",[249,440,441],{"class":291},"\"inspected_on\"",[249,443,444],{"class":258},"]):\n",[249,446,448,451,454],{"class":139,"line":447},15,[249,449,450],{"class":258},"        empty ",[249,452,453],{"class":254},"+=",[249,455,456],{"class":265}," 1\n",[249,458,460,463],{"class":139,"line":459},16,[249,461,462],{"class":254},"    else",[249,464,342],{"class":258},[249,466,468,471,473],{"class":139,"line":467},17,[249,469,470],{"class":258},"        filled ",[249,472,453],{"class":254},[249,474,456],{"class":265},[249,476,478,481,484,487,490,493,496,499,502,504,507,509,512],{"class":139,"line":477},18,[249,479,480],{"class":265},"print",[249,482,483],{"class":258},"(",[249,485,486],{"class":254},"f",[249,488,489],{"class":291},"\"",[249,491,492],{"class":265},"{",[249,494,495],{"class":258},"empty",[249,497,498],{"class":265},"}",[249,500,501],{"class":291}," never inspected, ",[249,503,492],{"class":265},[249,505,506],{"class":258},"filled",[249,508,498],{"class":265},[249,510,511],{"class":291}," inspected\"",[249,513,376],{"class":258},[14,515,516,520,521,524,525,527,528,531,532,535,536,539,540,542,543,545,546,549,550,552,553,555],{},[517,518,519],"strong",{},"Breakdown:"," On QGIS 3, ",[18,522,523],{},"f[\"inspected_on\"]"," for an empty value is a ",[18,526,28],{}," whose ",[18,529,530],{},"isNull()"," returns True; ",[18,533,534],{},"value == NULL"," also works there. ",[18,537,538],{},"value is None"," does not. On QGIS 4, the bindings return ",[18,541,20],{}," and there is no ",[18,544,24],{}," QVariant to compare against. Checking for an ",[18,547,548],{},"isNull"," method also catches empty ",[18,551,42],{}," and ",[18,554,46],{}," objects, which some providers return for blank date fields instead of a null variant. Putting the logic in one function means one place to change when the minimum supported version moves.",[14,557,558,559,562,563,371,565,371,568,552,571,574],{},"Avoid ",[18,560,561],{},"if not value:"," as a NULL test. It is true for NULL, but also for ",[18,564,80],{},[18,566,567],{},"0.0",[18,569,570],{},"\"\"",[18,572,573],{},"False"," — so a pipe with zero recorded leaks, or a flag field that is legitimately false, would be counted as missing.",[218,576,578],{"id":577},"convert-qt-types-into-python-values","Convert Qt types into Python values",[14,580,581,582,371,585,371,588,371,591,594,595,371,597,371,600,552,602,605],{},"Non-null values usually arrive as native Python types — ",[18,583,584],{},"int",[18,586,587],{},"float",[18,589,590],{},"str",[18,592,593],{},"bool",". Dates, times and some binary values do not: they are ",[18,596,42],{},[18,598,599],{},"QTime",[18,601,46],{},[18,603,604],{},"QByteArray",". Each has a conversion method.",[14,607,608],{},[63,609,612,615,618,621,624,628,633,637,641,645,649,652,656,659,662,665,668],{"viewBox":610,"role":66,"ariaLabel":611,"xmlns":68},"0 0 760 260","Field types and the Python types they arrive as, with the conversion methods for QDate, QDateTime, QTime and QByteArray",[70,613,614],{},"Field type to Python type",[74,616,617],{},"Integer and integer64 fields arrive as Python int, double as float, string as str and boolean as bool. Date fields arrive as QDate and convert with toPyDate; datetime fields arrive as QDateTime and convert with toPyDateTime; time fields arrive as QTime and convert with toPyTime; binary fields arrive as QByteArray and convert with bytes or data. A NULL in any of them needs the is_null check before conversion.",[78,619],{"x":80,"y":80,"width":81,"height":620,"fill":83},"260",[102,622,623],{"x":104,"y":105,"style":106,"fill":107,"textAnchor":108},"Most values are plain Python — dates are not",[78,625],{"x":112,"y":113,"width":626,"height":627,"rx":92,"fill":199,"stroke":200,"style":118},"340","160",[102,629,632],{"x":630,"y":631,"style":123,"fill":204,"textAnchor":108},"194","99.88","already Python",[102,634,636],{"x":630,"y":635,"style":128,"fill":100,"textAnchor":108},"119.83","integer, int8 → int",[102,638,640],{"x":630,"y":639,"style":128,"fill":100,"textAnchor":108},"139.78","double → float",[102,642,644],{"x":630,"y":643,"style":128,"fill":100,"textAnchor":108},"159.73","string → str",[102,646,648],{"x":630,"y":647,"style":128,"fill":100,"textAnchor":108},"179.68","boolean → bool",[78,650],{"x":651,"y":113,"width":626,"height":627,"rx":92,"fill":182,"stroke":183,"style":118},"396",[102,653,655],{"x":654,"y":631,"style":123,"fill":183,"textAnchor":108},"566","Qt objects",[102,657,658],{"x":654,"y":635,"style":160,"fill":100,"textAnchor":108},"QDate.toPyDate()",[102,660,661],{"x":654,"y":639,"style":160,"fill":100,"textAnchor":108},"QDateTime.toPyDateTime()",[102,663,664],{"x":654,"y":643,"style":160,"fill":100,"textAnchor":108},"QTime.toPyTime()",[102,666,667],{"x":654,"y":647,"style":160,"fill":100,"textAnchor":108},"bytes(QByteArray)",[102,669,671],{"x":104,"y":670,"style":128,"fill":117,"textAnchor":108},"244","check is_null() first — a NULL date has no toPyDate()",[240,673,675],{"className":242,"code":674,"language":244,"meta":245,"style":245},"from datetime import date, datetime\nfrom qgis.PyQt.QtCore import QDate, QDateTime, QTime, QByteArray\n\ndef to_python(value):\n    if is_null(value):\n        return None\n    if isinstance(value, QDateTime):\n        return value.toPyDateTime()\n    if isinstance(value, QDate):\n        return value.toPyDate()\n    if isinstance(value, QTime):\n        return value.toPyTime()\n    if isinstance(value, QByteArray):\n        return bytes(value)\n    return value\n\nnames = layer.fields().names()\nrows = [{n: to_python(v) for n, v in zip(names, f.attributes())}\n        for f in layer.getFeatures()]\noverdue = [r for r in rows if r[\"inspected_on\"] and r[\"inspected_on\"] \u003C date(2025, 1, 1)]\nprint(len(overdue), \"inspections older than 2025\")\n",[18,676,677,689,701,705,714,721,728,738,745,754,761,770,777,786,796,803,807,817,840,853,914],{"__ignoreMap":245},[249,678,679,681,684,686],{"class":139,"line":251},[249,680,255],{"class":254},[249,682,683],{"class":258}," datetime ",[249,685,262],{"class":254},[249,687,688],{"class":258}," date, datetime\n",[249,690,691,693,696,698],{"class":139,"line":272},[249,692,255],{"class":254},[249,694,695],{"class":258}," qgis.PyQt.QtCore ",[249,697,262],{"class":254},[249,699,700],{"class":258}," QDate, QDateTime, QTime, QByteArray\n",[249,702,703],{"class":139,"line":279},[249,704,276],{"emptyLinePlaceholder":275},[249,706,707,709,712],{"class":139,"line":303},[249,708,311],{"class":254},[249,710,711],{"class":314}," to_python",[249,713,318],{"class":258},[249,715,716,718],{"class":139,"line":308},[249,717,330],{"class":254},[249,719,720],{"class":258}," is_null(value):\n",[249,722,723,725],{"class":139,"line":321},[249,724,348],{"class":254},[249,726,727],{"class":265}," None\n",[249,729,730,732,735],{"class":139,"line":327},[249,731,330],{"class":254},[249,733,734],{"class":265}," isinstance",[249,736,737],{"class":258},"(value, QDateTime):\n",[249,739,740,742],{"class":139,"line":345},[249,741,348],{"class":254},[249,743,744],{"class":258}," value.toPyDateTime()\n",[249,746,747,749,751],{"class":139,"line":354},[249,748,330],{"class":254},[249,750,734],{"class":265},[249,752,753],{"class":258},"(value, QDate):\n",[249,755,756,758],{"class":139,"line":379},[249,757,348],{"class":254},[249,759,760],{"class":258}," value.toPyDate()\n",[249,762,763,765,767],{"class":139,"line":397},[249,764,330],{"class":254},[249,766,734],{"class":265},[249,768,769],{"class":258},"(value, QTime):\n",[249,771,772,774],{"class":139,"line":402},[249,773,348],{"class":254},[249,775,776],{"class":258}," value.toPyTime()\n",[249,778,779,781,783],{"class":139,"line":418},[249,780,330],{"class":254},[249,782,734],{"class":265},[249,784,785],{"class":258},"(value, QByteArray):\n",[249,787,788,790,793],{"class":139,"line":433},[249,789,348],{"class":254},[249,791,792],{"class":265}," bytes",[249,794,795],{"class":258},"(value)\n",[249,797,798,800],{"class":139,"line":447},[249,799,382],{"class":254},[249,801,802],{"class":258}," value\n",[249,804,805],{"class":139,"line":459},[249,806,276],{"emptyLinePlaceholder":275},[249,808,809,812,814],{"class":139,"line":467},[249,810,811],{"class":258},"names ",[249,813,285],{"class":254},[249,815,816],{"class":258}," layer.fields().names()\n",[249,818,819,822,824,827,829,832,834,837],{"class":139,"line":477},[249,820,821],{"class":258},"rows ",[249,823,285],{"class":254},[249,825,826],{"class":258}," [{n: to_python(v) ",[249,828,421],{"class":254},[249,830,831],{"class":258}," n, v ",[249,833,427],{"class":254},[249,835,836],{"class":265}," zip",[249,838,839],{"class":258},"(names, f.attributes())}\n",[249,841,843,846,848,850],{"class":139,"line":842},19,[249,844,845],{"class":254},"        for",[249,847,424],{"class":258},[249,849,427],{"class":254},[249,851,852],{"class":258}," layer.getFeatures()]\n",[249,854,856,859,861,864,866,869,871,874,877,880,882,885,887,889,891,893,896,899,902,904,907,909,911],{"class":139,"line":855},20,[249,857,858],{"class":258},"overdue ",[249,860,285],{"class":254},[249,862,863],{"class":258}," [r ",[249,865,421],{"class":254},[249,867,868],{"class":258}," r ",[249,870,427],{"class":254},[249,872,873],{"class":258}," rows ",[249,875,876],{"class":254},"if",[249,878,879],{"class":258}," r[",[249,881,441],{"class":291},[249,883,884],{"class":258},"] ",[249,886,391],{"class":254},[249,888,879],{"class":258},[249,890,441],{"class":291},[249,892,884],{"class":258},[249,894,895],{"class":254},"\u003C",[249,897,898],{"class":258}," date(",[249,900,901],{"class":265},"2025",[249,903,371],{"class":258},[249,905,906],{"class":265},"1",[249,908,371],{"class":258},[249,910,906],{"class":265},[249,912,913],{"class":258},")]\n",[249,915,917,919,921,924,927,930],{"class":139,"line":916},21,[249,918,480],{"class":265},[249,920,483],{"class":258},[249,922,923],{"class":265},"len",[249,925,926],{"class":258},"(overdue), ",[249,928,929],{"class":291},"\"inspections older than 2025\"",[249,931,376],{"class":258},[14,933,934,936,937,940,941,944,945,947,948,951,952,954,955,957,958,961,962,966],{},[517,935,519],{}," Converting once at the boundary — when rows leave QGIS for Python logic, JSON, pandas or a report — means the rest of the program can use ordinary comparisons and ",[18,938,939],{},"datetime"," arithmetic. ",[18,942,943],{},"toPyDateTime()"," returns a naive ",[18,946,939],{}," when the QGIS value has no time zone, and an aware one when it does; PostGIS ",[18,949,950],{},"timestamptz"," columns arrive as UTC-aware values. Checking ",[18,953,46],{}," before ",[18,956,42],{}," is not required for correctness here, but keeps the order of the most specific type first, which matters if you extend the function with subclasses. The same ",[18,959,960],{},"to_python"," function is the right preprocessing step before building a DataFrame, as in ",[52,963,965],{"href":964},"\u002Fspatial-data-processing-automation\u002Fpyqgis-and-the-python-data-stack\u002Fanalyse-attribute-table-with-pandas-pyqgis\u002F","analysing an attribute table with pandas",".",[218,968,970],{"id":969},"write-null-back-to-a-field","Write NULL back to a field",[14,972,973,974,976,977,980],{},"Clearing a value is where many scripts go wrong in the other direction: assigning ",[18,975,20],{}," on QGIS 3, or the string ",[18,978,979],{},"\"NULL\"",", which some providers store literally.",[240,982,984],{"className":242,"code":983,"language":244,"meta":245,"style":245},"from qgis.core import edit, NULL\n\nfield_idx = layer.fields().indexOf(\"inspected_on\")\ntry:\n    null_value = NULL                       # QGIS 3\nexcept NameError:\n    null_value = None                       # QGIS 4\n\nwith edit(layer):\n    for f in layer.getFeatures('\"status\" = \\'cancelled\\''):\n        layer.changeAttributeValue(f.id(), field_idx, null_value)\n\n# via the provider, no edit session\nlayer.dataProvider().changeAttributeValues({17: {field_idx: null_value}})\n",[18,985,986,1000,1004,1018,1025,1038,1048,1059,1063,1071,1100,1105,1109,1114],{"__ignoreMap":245},[249,987,988,990,992,994,997],{"class":139,"line":251},[249,989,255],{"class":254},[249,991,259],{"class":258},[249,993,262],{"class":254},[249,995,996],{"class":258}," edit, ",[249,998,999],{"class":265},"NULL\n",[249,1001,1002],{"class":139,"line":272},[249,1003,276],{"emptyLinePlaceholder":275},[249,1005,1006,1009,1011,1014,1016],{"class":139,"line":279},[249,1007,1008],{"class":258},"field_idx ",[249,1010,285],{"class":254},[249,1012,1013],{"class":258}," layer.fields().indexOf(",[249,1015,441],{"class":291},[249,1017,376],{"class":258},[249,1019,1020,1023],{"class":139,"line":303},[249,1021,1022],{"class":254},"try",[249,1024,342],{"class":258},[249,1026,1027,1030,1032,1034],{"class":139,"line":308},[249,1028,1029],{"class":258},"    null_value ",[249,1031,285],{"class":254},[249,1033,266],{"class":265},[249,1035,1037],{"class":1036},"sjoCn","                       # QGIS 3\n",[249,1039,1040,1043,1046],{"class":139,"line":321},[249,1041,1042],{"class":254},"except",[249,1044,1045],{"class":265}," NameError",[249,1047,342],{"class":258},[249,1049,1050,1052,1054,1056],{"class":139,"line":327},[249,1051,1029],{"class":258},[249,1053,285],{"class":254},[249,1055,339],{"class":265},[249,1057,1058],{"class":1036},"                       # QGIS 4\n",[249,1060,1061],{"class":139,"line":345},[249,1062,276],{"emptyLinePlaceholder":275},[249,1064,1065,1068],{"class":139,"line":354},[249,1066,1067],{"class":254},"with",[249,1069,1070],{"class":258}," edit(layer):\n",[249,1072,1073,1076,1078,1080,1083,1086,1089,1092,1094,1097],{"class":139,"line":379},[249,1074,1075],{"class":254},"    for",[249,1077,424],{"class":258},[249,1079,427],{"class":254},[249,1081,1082],{"class":258}," layer.getFeatures(",[249,1084,1085],{"class":291},"'\"status\" = ",[249,1087,1088],{"class":265},"\\'",[249,1090,1091],{"class":291},"cancelled",[249,1093,1088],{"class":265},[249,1095,1096],{"class":291},"'",[249,1098,1099],{"class":258},"):\n",[249,1101,1102],{"class":139,"line":397},[249,1103,1104],{"class":258},"        layer.changeAttributeValue(f.id(), field_idx, null_value)\n",[249,1106,1107],{"class":139,"line":402},[249,1108,276],{"emptyLinePlaceholder":275},[249,1110,1111],{"class":139,"line":418},[249,1112,1113],{"class":1036},"# via the provider, no edit session\n",[249,1115,1116,1119,1122],{"class":139,"line":433},[249,1117,1118],{"class":258},"layer.dataProvider().changeAttributeValues({",[249,1120,1121],{"class":265},"17",[249,1123,1124],{"class":258},": {field_idx: null_value}})\n",[14,1126,1127,520,1129,1131,1132,1134,1135,1137,1138,1140,1141,1143,1144,1146,1147,1150],{},[517,1128,519],{},[18,1130,24],{}," is what the provider recognises as \"no value\" for every field type. Assigning Python ",[18,1133,20],{}," works for many providers on recent 3.x releases, but older releases and some providers convert it to an empty string or zero, so the explicit constant is the safe choice. On Qt6-based QGIS 4 builds, PyQt6 has no Python-visible ",[18,1136,28],{},", so ",[18,1139,20],{}," is the null value; the ",[18,1142,1022],{}," block picks whichever form the running version provides. Never write the string ",[18,1145,979],{},": a text field stores those four characters, and a later ",[18,1148,1149],{},"is_null"," test reports a value.",[218,1152,1154],{"id":1153},"shapefiles-have-no-true-null","Shapefiles have no true NULL",[14,1156,1157],{},"The dBASE files behind shapefiles cannot store NULL for most types. A blank numeric field is read as NULL by recent GDAL versions, but a blank text field may be an empty string and a date field may be NULL or a zero date depending on how the file was written.",[14,1159,1160],{},[63,1161,1164,1167,1170,1173,1176,1178,1182,1185,1188,1191,1195,1197,1200,1202,1205,1208],{"viewBox":1162,"role":66,"ariaLabel":1163,"xmlns":68},"0 0 760 222","An empty value round-tripped through GeoPackage or PostGIS stays NULL for every type, while a shapefile may return an empty string for text fields",[70,1165,1166],{},"Same empty value, different formats",[74,1168,1169],{},"An empty value written to three formats and read back. GeoPackage and PostGIS return NULL for every field type. A shapefile returns NULL for blank numeric fields, but an empty text field may come back as an empty string, and dates vary with the writer. Treat empty strings as missing when reading shapefiles.",[78,1171],{"x":80,"y":80,"width":81,"height":1172,"fill":83},"222",[102,1174,1175],{"x":104,"y":105,"style":106,"fill":107,"textAnchor":108},"Round trip of an empty value",[78,1177],{"x":112,"y":113,"width":626,"height":115,"rx":92,"fill":199,"stroke":200,"style":118},[102,1179,1181],{"x":630,"y":1180,"style":123,"fill":204,"textAnchor":108},"94.88","GeoPackage · PostGIS",[102,1183,1184],{"x":630,"y":155,"style":128,"fill":100,"textAnchor":108},"number → NULL",[102,1186,1187],{"x":630,"y":159,"style":128,"fill":100,"textAnchor":108},"text → NULL",[102,1189,1190],{"x":630,"y":164,"style":128,"fill":100,"textAnchor":108},"date → NULL",[102,1192,1194],{"x":630,"y":1193,"style":128,"fill":117,"textAnchor":108},"174.68","true SQL NULL",[78,1196],{"x":651,"y":113,"width":626,"height":115,"rx":92,"fill":182,"stroke":183,"style":118},[102,1198,1199],{"x":654,"y":1180,"style":123,"fill":183,"textAnchor":108},"Shapefile (DBF)",[102,1201,1184],{"x":654,"y":155,"style":128,"fill":100,"textAnchor":108},[102,1203,1204],{"x":654,"y":159,"style":128,"fill":100,"textAnchor":108},"text → \"\" or NULL",[102,1206,1207],{"x":654,"y":164,"style":128,"fill":100,"textAnchor":108},"date → depends on writer",[102,1209,1210],{"x":654,"y":1193,"style":128,"fill":117,"textAnchor":108},"normalise on read",[240,1212,1214],{"className":242,"code":1213,"language":244,"meta":245,"style":245},"def is_missing(value):\n    \"\"\"NULL, None, or an empty\u002Fblank string — for shapefile and CSV sources.\"\"\"\n    return is_null(value) or (isinstance(value, str) and not value.strip())\n\nmissing_names = sum(1 for f in layer.getFeatures() if is_missing(f[\"owner_name\"]))\nprint(missing_names, \"features without an owner name\")\n",[18,1215,1216,1225,1230,1261,1265,1300],{"__ignoreMap":245},[249,1217,1218,1220,1223],{"class":139,"line":251},[249,1219,311],{"class":254},[249,1221,1222],{"class":314}," is_missing",[249,1224,318],{"class":258},[249,1226,1227],{"class":139,"line":272},[249,1228,1229],{"class":291},"    \"\"\"NULL, None, or an empty\u002Fblank string — for shapefile and CSV sources.\"\"\"\n",[249,1231,1232,1234,1237,1240,1243,1246,1248,1250,1253,1255,1258],{"class":139,"line":279},[249,1233,382],{"class":254},[249,1235,1236],{"class":258}," is_null(value) ",[249,1238,1239],{"class":254},"or",[249,1241,1242],{"class":258}," (",[249,1244,1245],{"class":265},"isinstance",[249,1247,365],{"class":258},[249,1249,590],{"class":265},[249,1251,1252],{"class":258},") ",[249,1254,391],{"class":254},[249,1256,1257],{"class":254}," not",[249,1259,1260],{"class":258}," value.strip())\n",[249,1262,1263],{"class":139,"line":303},[249,1264,276],{"emptyLinePlaceholder":275},[249,1266,1267,1270,1272,1275,1277,1279,1282,1284,1286,1289,1291,1294,1297],{"class":139,"line":308},[249,1268,1269],{"class":258},"missing_names ",[249,1271,285],{"class":254},[249,1273,1274],{"class":265}," sum",[249,1276,483],{"class":258},[249,1278,906],{"class":265},[249,1280,1281],{"class":254}," for",[249,1283,424],{"class":258},[249,1285,427],{"class":254},[249,1287,1288],{"class":258}," layer.getFeatures() ",[249,1290,876],{"class":254},[249,1292,1293],{"class":258}," is_missing(f[",[249,1295,1296],{"class":291},"\"owner_name\"",[249,1298,1299],{"class":258},"]))\n",[249,1301,1302,1304,1307,1310],{"class":139,"line":321},[249,1303,480],{"class":265},[249,1305,1306],{"class":258},"(missing_names, ",[249,1308,1309],{"class":291},"\"features without an owner name\"",[249,1311,376],{"class":258},[14,1313,1314,1316,1317,1319,1320,1324],{},[517,1315,519],{}," For sources without a reliable NULL — shapefiles and delimited text files — treating blank strings as missing is usually what the data means. Keep this separate from ",[18,1318,1149],{}," so that a GeoPackage, where an empty string and NULL genuinely differ, is not flattened by accident. Converting shapefiles to GeoPackage early in a workflow, as in ",[52,1321,1323],{"href":1322},"\u002Fspatial-data-processing-automation\u002Fpostgis-and-database-workflows\u002Fwrite-vector-layer-to-geopackage-pyqgis\u002F","writing a vector layer to GeoPackage",", removes the ambiguity for everything downstream.",[218,1326,1328],{"id":1327},"null-inside-expressions","NULL inside expressions",[14,1330,1331],{},"Expressions have their own NULL semantics, which follow SQL rather than Python: any comparison with NULL is NULL, and a filter that evaluates to NULL excludes the feature.",[240,1333,1335],{"className":242,"code":1334,"language":244,"meta":245,"style":245},"from qgis.core import QgsFeatureRequest\n\n# misses NULLs: NULL \u003C> 'closed' is NULL, not true\nopen_a = len(list(layer.getFeatures('\"status\" \u003C> \\'closed\\'')))\n# includes NULLs explicitly\nopen_b = len(list(layer.getFeatures('\"status\" IS NULL OR \"status\" \u003C> \\'closed\\'')))\nprint(open_a, \"vs\", open_b)\n\n# replace NULL with a default inside the expression\nreq = QgsFeatureRequest().setFilterExpression('coalesce(\"priority\", 3) \u003C= 2')\n",[18,1336,1337,1348,1352,1357,1390,1395,1423,1436,1440,1445],{"__ignoreMap":245},[249,1338,1339,1341,1343,1345],{"class":139,"line":251},[249,1340,255],{"class":254},[249,1342,259],{"class":258},[249,1344,262],{"class":254},[249,1346,1347],{"class":258}," QgsFeatureRequest\n",[249,1349,1350],{"class":139,"line":272},[249,1351,276],{"emptyLinePlaceholder":275},[249,1353,1354],{"class":139,"line":279},[249,1355,1356],{"class":1036},"# misses NULLs: NULL \u003C> 'closed' is NULL, not true\n",[249,1358,1359,1362,1364,1367,1369,1372,1375,1378,1380,1383,1385,1387],{"class":139,"line":303},[249,1360,1361],{"class":258},"open_a ",[249,1363,285],{"class":254},[249,1365,1366],{"class":265}," len",[249,1368,483],{"class":258},[249,1370,1371],{"class":265},"list",[249,1373,1374],{"class":258},"(layer.getFeatures(",[249,1376,1377],{"class":291},"'\"status\" \u003C> ",[249,1379,1088],{"class":265},[249,1381,1382],{"class":291},"closed",[249,1384,1088],{"class":265},[249,1386,1096],{"class":291},[249,1388,1389],{"class":258},")))\n",[249,1391,1392],{"class":139,"line":308},[249,1393,1394],{"class":1036},"# includes NULLs explicitly\n",[249,1396,1397,1400,1402,1404,1406,1408,1410,1413,1415,1417,1419,1421],{"class":139,"line":321},[249,1398,1399],{"class":258},"open_b ",[249,1401,285],{"class":254},[249,1403,1366],{"class":265},[249,1405,483],{"class":258},[249,1407,1371],{"class":265},[249,1409,1374],{"class":258},[249,1411,1412],{"class":291},"'\"status\" IS NULL OR \"status\" \u003C> ",[249,1414,1088],{"class":265},[249,1416,1382],{"class":291},[249,1418,1088],{"class":265},[249,1420,1096],{"class":291},[249,1422,1389],{"class":258},[249,1424,1425,1427,1430,1433],{"class":139,"line":327},[249,1426,480],{"class":265},[249,1428,1429],{"class":258},"(open_a, ",[249,1431,1432],{"class":291},"\"vs\"",[249,1434,1435],{"class":258},", open_b)\n",[249,1437,1438],{"class":139,"line":345},[249,1439,276],{"emptyLinePlaceholder":275},[249,1441,1442],{"class":139,"line":354},[249,1443,1444],{"class":1036},"# replace NULL with a default inside the expression\n",[249,1446,1447,1450,1452,1455,1458],{"class":139,"line":379},[249,1448,1449],{"class":258},"req ",[249,1451,285],{"class":254},[249,1453,1454],{"class":258}," QgsFeatureRequest().setFilterExpression(",[249,1456,1457],{"class":291},"'coalesce(\"priority\", 3) \u003C= 2'",[249,1459,376],{"class":258},[14,1461,1462,1464,1465,552,1468,1471,1472,1475,1476,1479,1480,1484],{},[517,1463,519],{}," The two counts differ by exactly the number of features with a NULL status — the most common silent error in expression filters. ",[18,1466,1467],{},"IS NULL",[18,1469,1470],{},"IS NOT NULL"," are the only correct tests inside an expression; ",[18,1473,1474],{},"= NULL"," never matches. ",[18,1477,1478],{},"coalesce"," substitutes a default and is the expression equivalent of the Python helper. ",[52,1481,1483],{"href":1482},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fevaluate-qgis-expression-in-pyqgis\u002F","Evaluating QGIS expressions in PyQGIS"," covers the evaluation side; the NULL rules are the same everywhere expressions are used, including labels and data-defined styling.",[218,1486,1488],{"id":1487},"null-in-statistics-and-aggregates","NULL in statistics and aggregates",[14,1490,1491],{},"Summaries are where NULL handling decides the answer rather than causing a crash. An average computed in Python over values that include NULL either fails or, if the NULLs were turned into zeros, comes out too low. QGIS's own aggregate functions skip NULLs, which is usually right — but you should know it is happening and how many values were skipped.",[240,1493,1495],{"className":242,"code":1494,"language":244,"meta":245,"style":245},"from qgis.core import QgsAggregateCalculator\n\nvalues = [f[\"flow_lps\"] for f in layer.getFeatures()]\npresent = [v for v in values if not is_null(v)]\nprint(f\"{len(values) - len(present)} of {len(values)} readings missing\")\nprint(\"python mean:\", sum(present) \u002F len(present) if present else None)\n\nmean, ok = layer.aggregate(QgsAggregateCalculator.Mean, \"flow_lps\")\nmissing, _ = layer.aggregate(QgsAggregateCalculator.CountMissing, \"flow_lps\")\nprint(\"aggregate mean:\", mean, \"| missing:\", missing)\n",[18,1496,1497,1508,1512,1535,1562,1603,1639,1643,1657,1671],{"__ignoreMap":245},[249,1498,1499,1501,1503,1505],{"class":139,"line":251},[249,1500,255],{"class":254},[249,1502,259],{"class":258},[249,1504,262],{"class":254},[249,1506,1507],{"class":258}," QgsAggregateCalculator\n",[249,1509,1510],{"class":139,"line":272},[249,1511,276],{"emptyLinePlaceholder":275},[249,1513,1514,1517,1519,1522,1525,1527,1529,1531,1533],{"class":139,"line":279},[249,1515,1516],{"class":258},"values ",[249,1518,285],{"class":254},[249,1520,1521],{"class":258}," [f[",[249,1523,1524],{"class":291},"\"flow_lps\"",[249,1526,884],{"class":258},[249,1528,421],{"class":254},[249,1530,424],{"class":258},[249,1532,427],{"class":254},[249,1534,852],{"class":258},[249,1536,1537,1540,1542,1545,1547,1550,1552,1555,1557,1559],{"class":139,"line":303},[249,1538,1539],{"class":258},"present ",[249,1541,285],{"class":254},[249,1543,1544],{"class":258}," [v ",[249,1546,421],{"class":254},[249,1548,1549],{"class":258}," v ",[249,1551,427],{"class":254},[249,1553,1554],{"class":258}," values ",[249,1556,876],{"class":254},[249,1558,1257],{"class":254},[249,1560,1561],{"class":258}," is_null(v)]\n",[249,1563,1564,1566,1568,1570,1572,1575,1578,1581,1583,1586,1588,1591,1593,1596,1598,1601],{"class":139,"line":308},[249,1565,480],{"class":265},[249,1567,483],{"class":258},[249,1569,486],{"class":254},[249,1571,489],{"class":291},[249,1573,1574],{"class":265},"{len",[249,1576,1577],{"class":258},"(values) ",[249,1579,1580],{"class":254},"-",[249,1582,1366],{"class":265},[249,1584,1585],{"class":258},"(present)",[249,1587,498],{"class":265},[249,1589,1590],{"class":291}," of ",[249,1592,1574],{"class":265},[249,1594,1595],{"class":258},"(values)",[249,1597,498],{"class":265},[249,1599,1600],{"class":291}," readings missing\"",[249,1602,376],{"class":258},[249,1604,1605,1607,1609,1612,1614,1617,1620,1623,1625,1627,1629,1632,1635,1637],{"class":139,"line":321},[249,1606,480],{"class":265},[249,1608,483],{"class":258},[249,1610,1611],{"class":291},"\"python mean:\"",[249,1613,371],{"class":258},[249,1615,1616],{"class":265},"sum",[249,1618,1619],{"class":258},"(present) ",[249,1621,1622],{"class":254},"\u002F",[249,1624,1366],{"class":265},[249,1626,1619],{"class":258},[249,1628,876],{"class":254},[249,1630,1631],{"class":258}," present ",[249,1633,1634],{"class":254},"else",[249,1636,339],{"class":265},[249,1638,376],{"class":258},[249,1640,1641],{"class":139,"line":327},[249,1642,276],{"emptyLinePlaceholder":275},[249,1644,1645,1648,1650,1653,1655],{"class":139,"line":345},[249,1646,1647],{"class":258},"mean, ok ",[249,1649,285],{"class":254},[249,1651,1652],{"class":258}," layer.aggregate(QgsAggregateCalculator.Mean, ",[249,1654,1524],{"class":291},[249,1656,376],{"class":258},[249,1658,1659,1662,1664,1667,1669],{"class":139,"line":354},[249,1660,1661],{"class":258},"missing, _ ",[249,1663,285],{"class":254},[249,1665,1666],{"class":258}," layer.aggregate(QgsAggregateCalculator.CountMissing, ",[249,1668,1524],{"class":291},[249,1670,376],{"class":258},[249,1672,1673,1675,1677,1680,1683,1686],{"class":139,"line":379},[249,1674,480],{"class":265},[249,1676,483],{"class":258},[249,1678,1679],{"class":291},"\"aggregate mean:\"",[249,1681,1682],{"class":258},", mean, ",[249,1684,1685],{"class":291},"\"| missing:\"",[249,1687,1688],{"class":258},", missing)\n",[14,1690,1691,1693,1694,1697,1698,1701,1702,1705,1706,1708],{},[517,1692,519],{}," Reporting the number of missing values next to every statistic is cheap and prevents the most common misreading of a summary: a mean over 40 readings presented as if it described all 400 pipes. ",[18,1695,1696],{},"layer.aggregate"," runs inside the provider where it can — a SQL ",[18,1699,1700],{},"AVG"," on PostGIS — and ignores NULLs in the same way SQL does. ",[18,1703,1704],{},"CountMissing"," counts NULLs and, for text fields, empty strings, which suits the shapefile case above. If a NULL should count as zero for a particular statistic — no recorded leaks meaning zero leaks — make that substitution explicitly with ",[18,1707,1478],{}," or a Python default, and say so in the output, rather than letting it happen as a side effect of a falsy test.",[218,1710,1712],{"id":1711},"qgis-version-compatibility","QGIS version compatibility",[14,1714,1715,1716,1718,1719,1722,1723,1725,1726,1728,1729,1731,1732,1622,1734,1736,1737,1740],{},"QGIS 3.x returns NULL attributes as ",[18,1717,28],{}," objects exported as ",[18,1720,1721],{},"qgis.core.NULL",". QGIS 4, built on Qt6 and PyQt6, returns ",[18,1724,20],{}," for empty attributes because PyQt6 converts null variants automatically; do not rely on a ",[18,1727,24],{}," constant existing there. The ",[18,1730,1149],{}," helper and the ",[18,1733,24],{},[18,1735,20],{}," fallback shown above run unchanged on both. Field type constants also change: QGIS 4 uses ",[18,1738,1739],{},"QMetaType.Type"," values, which 3.38 and later accept too.",[218,1742,1744],{"id":1743},"troubleshooting","Troubleshooting",[223,1746,1747,1758,1770,1786],{},[226,1748,1749,1754,1755,1757],{},[517,1750,1751,966],{},[18,1752,1753],{},"TypeError: '\u003C' not supported between instances of 'QVariant' and 'int'"," A NULL reached a comparison; filter with ",[18,1756,1149],{}," first.",[226,1759,1760,1763,1764,1766,1767,966],{},[517,1761,1762],{},"The text \"NULL\" appears in the attribute table."," A script wrote the string; repair with an update setting the field to the ",[18,1765,24],{}," constant where it equals ",[18,1768,1769],{},"'NULL'",[226,1771,1772,1777,1778,1781,1782,1785],{},[517,1773,1774],{},[18,1775,1776],{},"AttributeError: 'QDate' object has no attribute 'year'"," in older code. Convert with ",[18,1779,1780],{},"toPyDate()"," or use ",[18,1783,1784],{},"QDate.year()"," with parentheses.",[226,1787,1788,1791,1792,1794,1795,1798,1799,1757],{},[517,1789,1790],{},"JSON export fails with \"not serializable\"."," A ",[18,1793,42],{}," or NULL reached ",[18,1796,1797],{},"json.dumps","; run values through ",[18,1800,960],{},[218,1802,1804],{"id":1803},"conclusion","Conclusion",[14,1806,1807,1808,1810,1811,1813,1814,1816],{},"Empty attributes are ",[18,1809,24],{}," on QGIS 3 and ",[18,1812,20],{}," on QGIS 4, so test with a helper that accepts both, convert Qt date and binary types at the boundary to Python, write NULL with the constant rather than a string, treat blank strings as missing for shapefile and CSV sources, and use ",[18,1815,1467],{}," inside expressions.",[218,1818,1820],{"id":1819},"frequently-asked-questions","Frequently Asked Questions",[14,1822,1823,1830,1832,1833,1835,1836,1838,1839,966],{},[517,1824,1825,1826,1829],{},"Why does ",[18,1827,1828],{},"print(value)"," show NULL if it is not None?",[18,1831,24],{}," is a ",[18,1834,28],{}," with a custom string representation. It prints as ",[18,1837,24],{}," but is a different object from ",[18,1840,20],{},[14,1842,1843,1850,1851,1854],{},[517,1844,1845,1846,1849],{},"Is ",[18,1847,1848],{},"bool(NULL)"," always False?","\nYes, which is why ",[18,1852,1853],{},"if value:"," treats NULL as missing — along with every other falsy value.",[14,1856,1857,1860],{},[517,1858,1859],{},"Can I make QGIS 3 return None instead?","\nNot globally. Convert at the boundary with a helper, which also prepares the code for QGIS 4.",[14,1862,1863,1866,1867,1870],{},[517,1864,1865],{},"How do I count NULLs per field quickly?","\nUse ",[18,1868,1869],{},"layer.aggregate(QgsAggregateCalculator.CountMissing, \"field\")",", which runs in the provider.",[218,1872,1874],{"id":1873},"related","Related",[223,1876,1877,1882,1888,1894,1899],{},[226,1878,1879,1881],{},[52,1880,55],{"href":54}," — the guide this recipe belongs to",[226,1883,1884],{},[52,1885,1887],{"href":1886},"\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fread-feature-attributes-and-geometry-pyqgis\u002F","Read Feature Attributes and Geometry in PyQGIS",[226,1889,1890],{},[52,1891,1893],{"href":1892},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fupdate-attribute-values-pyqgis\u002F","Update Attribute Values in PyQGIS",[226,1895,1896],{},[52,1897,1898],{"href":1482},"Evaluate a QGIS Expression in PyQGIS",[226,1900,1901],{},[52,1902,1904],{"href":1903},"\u002Fpyqgis-fundamentals-environment-setup\u002Fqgis-api-architecture\u002Fqgis-python-version-compatibility-guide\u002F","QGIS Python Version Compatibility Guide",[1906,1907,1908],"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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":245,"searchDepth":272,"depth":272,"links":1910},[1911,1912,1913,1914,1915,1916,1917,1918,1919,1920,1921,1922],{"id":220,"depth":272,"text":221},{"id":234,"depth":272,"text":235},{"id":577,"depth":272,"text":578},{"id":969,"depth":272,"text":970},{"id":1153,"depth":272,"text":1154},{"id":1327,"depth":272,"text":1328},{"id":1487,"depth":272,"text":1488},{"id":1711,"depth":272,"text":1712},{"id":1743,"depth":272,"text":1744},{"id":1803,"depth":272,"text":1804},{"id":1819,"depth":272,"text":1820},{"id":1873,"depth":272,"text":1874},"Why empty attributes in PyQGIS are NULL rather than None, how to test for them reliably on QGIS 3 and QGIS 4, how dates and numbers arrive as Qt types, and how to write NULL back to a field without storing the string \"NULL\".","md",{"slug":1926,"type":1927,"breadcrumb":1928,"datePublished":1929,"dateModified":1929},"handle-null-values-and-qvariant-pyqgis","article","Handle NULL Values and QVariant","2026-10-02","\u002Fpyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fhandle-null-values-and-qvariant-pyqgis",{"title":5,"description":1923},"pyqgis-fundamentals-environment-setup\u002Ffeatures-geometries-and-memory-layers\u002Fhandle-null-values-and-qvariant-pyqgis\u002Findex","JwIF8m0Kn7kjRcayRVeGIA5pmOOTsgo_H4mM-GHcwNE",1790966252948]