[{"data":1,"prerenderedAt":1618},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fset-default-values-and-field-constraints-pyqgis":3},{"id":4,"title":5,"body":6,"description":1607,"extension":1608,"meta":1609,"navigation":259,"path":1614,"seo":1615,"stem":1616,"__hash__":1617},"docs\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fset-default-values-and-field-constraints-pyqgis\u002Findex.md","Set Default Values and Field Constraints in PyQGIS",{"type":7,"value":8,"toc":1595},"minimark",[9,13,26,35,188,193,215,219,227,483,507,601,608,612,627,744,756,760,771,999,1019,1081,1084,1177,1193,1197,1212,1398,1425,1429,1455,1459,1504,1508,1517,1521,1530,1538,1551,1557,1561,1591],[10,11,5],"h1",{"id":12},"set-default-values-and-field-constraints-in-pyqgis",[14,15,16,17,21,22,25],"p",{},"Editor widgets control ",[18,19,20],"em",{},"how"," a value is entered. Defaults and constraints control ",[18,23,24],{},"what"," ends up stored. A default fills a field before the user sees the form — the date, the name of whoever is logged in, a unique identifier, the area of the polygon just drawn. A constraint refuses to let a feature be saved unless a rule holds: the field is not empty, the value is unique, an expression evaluates true. Together they turn a layer from a place where data is typed into a place where data is checked.",[14,27,28,29,34],{},"This recipe belongs to ",[30,31,33],"a",{"href":32},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002F","Attribute Forms & Layer Actions in PyQGIS",". It sets expression defaults, including ones that update on every edit, adds the three constraint types with hard and soft strength, and shows how to apply the same rules to features that a script creates.",[14,36,37],{},[38,39,44,48,52,59,76,85,93,99,103,107,115,120,125,129,134,138,144,150,154,158,162,165,169,172,179,184],"svg",{"viewBox":40,"role":41,"ariaLabel":42,"xmlns":43},"0 0 760 300","img","Where defaults and constraints act in the life of a new feature: defaults fill values when the feature is created, the user edits the form, constraints are checked while editing and again on save, and hard failures block the commit while soft failures only warn","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[45,46,47],"title",{},"Defaults first, constraints last",[49,50,51],"desc",{},"A timeline for one new feature. At creation, default expressions such as now, user account name and uuid fill fields. During editing, the form shows constraint status live with green and red indicators. On save, hard constraints that fail block the feature from being accepted, while soft constraints that fail show a warning but allow saving. Defaults marked apply on update are recalculated each time the feature changes.",[53,54],"rect",{"x":55,"y":55,"width":56,"height":57,"fill":58},"0","760","300","#f6f3ea",[60,61,62],"defs",{},[63,64,71],"marker",{"id":65,"viewBox":66,"refX":67,"refY":68,"markerWidth":69,"markerHeight":69,"orient":70},"dvLifeArrow","0 0 10 10","8","5","7","auto-start-reverse",[72,73],"path",{"d":74,"fill":75},"M0 0 L10 5 L0 10 z","#2f3b35",[77,78,84],"text",{"x":79,"y":80,"style":81,"fill":82,"textAnchor":83},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","The life of one new feature",[86,87],"line",{"x1":88,"y1":89,"x2":90,"y2":89,"stroke":91,"style":92},"40","150","720","#9aa39d","stroke-width:3;marker-end:url(#dvLifeArrow)",[94,95],"circle",{"cx":96,"cy":89,"r":97,"fill":98},"130","10","#0f766e",[94,100],{"cx":101,"cy":89,"r":97,"fill":102},"370","#2563eb",[94,104],{"cx":105,"cy":89,"r":97,"fill":106},"610","#b45309",[53,108],{"x":109,"y":110,"width":111,"height":112,"rx":97,"fill":113,"stroke":98,"style":114},"36","50","188","80","#eef7f4","stroke-width:2",[77,116,119],{"x":96,"y":117,"style":118,"fill":98,"textAnchor":83},"74","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","created: defaults",[77,121,124],{"x":96,"y":122,"style":123,"fill":75,"textAnchor":83},"96","text-anchor:middle;font-size:9.5px;font-family:monospace","now()",[77,126,128],{"x":96,"y":127,"style":123,"fill":75,"textAnchor":83},"114","@user_account_name",[53,130],{"x":131,"y":132,"width":111,"height":112,"rx":97,"fill":133,"stroke":102,"style":114},"276","170","#eff3ff",[77,135,137],{"x":101,"y":136,"style":118,"fill":102,"textAnchor":83},"194","editing: live checks",[94,139],{"cx":140,"cy":141,"r":142,"fill":143},"316","220","6","#15803d",[77,145,149],{"x":146,"y":147,"style":148,"fill":75},"330","224","font-size:9.5px;font-family:sans-serif","species ok",[94,151],{"cx":140,"cy":152,"r":142,"fill":153},"238","#b91c1c",[77,155,157],{"x":146,"y":156,"style":148,"fill":75},"242","height empty",[53,159],{"x":160,"y":110,"width":111,"height":112,"rx":97,"fill":161,"stroke":106,"style":114},"516","#fdf2e2",[77,163,164],{"x":105,"y":117,"style":118,"fill":106,"textAnchor":83},"save: enforce",[77,166,168],{"x":105,"y":122,"style":167,"fill":153,"textAnchor":83},"text-anchor:middle;font-size:9.5px;font-family:sans-serif","hard failure → blocked",[77,170,171],{"x":105,"y":127,"style":167,"fill":106,"textAnchor":83},"soft failure → warning",[53,173],{"x":160,"y":174,"width":111,"height":175,"rx":97,"fill":176,"stroke":177,"style":178},"178","64","#fffdf7","#59645f","stroke-width:1.8",[77,180,183],{"x":105,"y":181,"style":182,"fill":75,"textAnchor":83},"202","text-anchor:middle;font-size:10px;font-family:sans-serif","applyOnUpdate defaults",[77,185,187],{"x":105,"y":186,"style":182,"fill":75,"textAnchor":83},"222","recalculate on every change",[189,190,192],"h2",{"id":191},"prerequisites","Prerequisites",[194,195,196,204,207],"ul",{},[197,198,199,203],"li",{},[200,201,202],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series.",[197,205,206],{},"An editable vector layer. Constraints are stored per layer in the project or its style; database-level constraints are read from the provider automatically.",[197,208,209,210,214],{},"Familiarity with ",[30,211,213],{"href":212},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fevaluate-qgis-expression-in-pyqgis\u002F","QGIS expressions",", which drive both defaults and expression constraints.",[189,216,218],{"id":217},"expression-defaults","Expression defaults",[14,220,221,222,226],{},"A default is a ",[223,224,225],"code",{},"QgsDefaultValue",": an expression, plus a flag saying whether it is recalculated whenever the feature changes. It is set per field index.",[228,229,234],"pre",{"className":230,"code":231,"language":232,"meta":233,"style":233},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsDefaultValue\n\ninspections = QgsProject.instance().mapLayersByName(\"inspections\")[0]\n\ndef set_default(layer, field_name, expression, apply_on_update=False):\n    idx = layer.fields().indexOf(field_name)\n    if idx \u003C 0:\n        raise KeyError(field_name)\n    layer.setDefaultValueDefinition(idx, QgsDefaultValue(expression, apply_on_update))\n\nset_default(inspections, \"inspection_uid\", \"uuid('WithoutBraces')\")\nset_default(inspections, \"created_at\", \"now()\")\nset_default(inspections, \"created_by\", \"@user_account_name\")\nset_default(inspections, \"modified_at\", \"now()\", apply_on_update=True)\nset_default(inspections, \"modified_by\", \"@user_full_name\", apply_on_update=True)\nset_default(inspections, \"area_m2\", \"round($area, 1)\", apply_on_update=True)\n","python","",[223,235,236,254,261,286,291,312,323,341,353,359,364,382,397,412,437,460],{"__ignoreMap":233},[237,238,240,244,248,251],"span",{"class":86,"line":239},1,[237,241,243],{"class":242},"snl16","from",[237,245,247],{"class":246},"s95oV"," qgis.core ",[237,249,250],{"class":242},"import",[237,252,253],{"class":246}," QgsProject, QgsDefaultValue\n",[237,255,257],{"class":86,"line":256},2,[237,258,260],{"emptyLinePlaceholder":259},true,"\n",[237,262,264,267,270,273,277,280,283],{"class":86,"line":263},3,[237,265,266],{"class":246},"inspections ",[237,268,269],{"class":242},"=",[237,271,272],{"class":246}," QgsProject.instance().mapLayersByName(",[237,274,276],{"class":275},"sU2Wk","\"inspections\"",[237,278,279],{"class":246},")[",[237,281,55],{"class":282},"sDLfK",[237,284,285],{"class":246},"]\n",[237,287,289],{"class":86,"line":288},4,[237,290,260],{"emptyLinePlaceholder":259},[237,292,294,297,301,304,306,309],{"class":86,"line":293},5,[237,295,296],{"class":242},"def",[237,298,300],{"class":299},"svObZ"," set_default",[237,302,303],{"class":246},"(layer, field_name, expression, apply_on_update",[237,305,269],{"class":242},[237,307,308],{"class":282},"False",[237,310,311],{"class":246},"):\n",[237,313,315,318,320],{"class":86,"line":314},6,[237,316,317],{"class":246},"    idx ",[237,319,269],{"class":242},[237,321,322],{"class":246}," layer.fields().indexOf(field_name)\n",[237,324,326,329,332,335,338],{"class":86,"line":325},7,[237,327,328],{"class":242},"    if",[237,330,331],{"class":246}," idx ",[237,333,334],{"class":242},"\u003C",[237,336,337],{"class":282}," 0",[237,339,340],{"class":246},":\n",[237,342,344,347,350],{"class":86,"line":343},8,[237,345,346],{"class":242},"        raise",[237,348,349],{"class":282}," KeyError",[237,351,352],{"class":246},"(field_name)\n",[237,354,356],{"class":86,"line":355},9,[237,357,358],{"class":246},"    layer.setDefaultValueDefinition(idx, QgsDefaultValue(expression, apply_on_update))\n",[237,360,362],{"class":86,"line":361},10,[237,363,260],{"emptyLinePlaceholder":259},[237,365,367,370,373,376,379],{"class":86,"line":366},11,[237,368,369],{"class":246},"set_default(inspections, ",[237,371,372],{"class":275},"\"inspection_uid\"",[237,374,375],{"class":246},", ",[237,377,378],{"class":275},"\"uuid('WithoutBraces')\"",[237,380,381],{"class":246},")\n",[237,383,385,387,390,392,395],{"class":86,"line":384},12,[237,386,369],{"class":246},[237,388,389],{"class":275},"\"created_at\"",[237,391,375],{"class":246},[237,393,394],{"class":275},"\"now()\"",[237,396,381],{"class":246},[237,398,400,402,405,407,410],{"class":86,"line":399},13,[237,401,369],{"class":246},[237,403,404],{"class":275},"\"created_by\"",[237,406,375],{"class":246},[237,408,409],{"class":275},"\"@user_account_name\"",[237,411,381],{"class":246},[237,413,415,417,420,422,424,426,430,432,435],{"class":86,"line":414},14,[237,416,369],{"class":246},[237,418,419],{"class":275},"\"modified_at\"",[237,421,375],{"class":246},[237,423,394],{"class":275},[237,425,375],{"class":246},[237,427,429],{"class":428},"s9osk","apply_on_update",[237,431,269],{"class":242},[237,433,434],{"class":282},"True",[237,436,381],{"class":246},[237,438,440,442,445,447,450,452,454,456,458],{"class":86,"line":439},15,[237,441,369],{"class":246},[237,443,444],{"class":275},"\"modified_by\"",[237,446,375],{"class":246},[237,448,449],{"class":275},"\"@user_full_name\"",[237,451,375],{"class":246},[237,453,429],{"class":428},[237,455,269],{"class":242},[237,457,434],{"class":282},[237,459,381],{"class":246},[237,461,463,465,468,470,473,475,477,479,481],{"class":86,"line":462},16,[237,464,369],{"class":246},[237,466,467],{"class":275},"\"area_m2\"",[237,469,375],{"class":246},[237,471,472],{"class":275},"\"round($area, 1)\"",[237,474,375],{"class":246},[237,476,429],{"class":428},[237,478,269],{"class":242},[237,480,434],{"class":282},[237,482,381],{"class":246},[14,484,485,488,489,491,492,495,496,498,499,502,503,506],{},[200,486,487],{},"Breakdown:"," Without ",[223,490,429],{},", a default is evaluated once, when the feature is created, and never again — right for creation timestamps and identifiers. With it, the expression runs every time the feature is edited, which is how audit fields and derived values like area stay current. ",[223,493,494],{},"$area"," respects the project's ellipsoid and distance units, so set those deliberately before relying on a stored area. ",[223,497,128],{}," is the operating system login and ",[223,500,501],{},"@user_full_name"," the display name; both are evaluated on the editing machine, which is exactly what an audit trail needs. ",[223,504,505],{},"uuid('WithoutBraces')"," produces identifiers that stay unique across offline editors merging data later, which sequential numbers cannot promise.",[14,508,509],{},[38,510,513,516,519,522,526,531,536,540,544,549,554,556,560,564,566,568,572,574,576,580,582,585,589,591,593,596,599],{"viewBox":511,"role":41,"ariaLabel":512,"xmlns":43},"0 0 760 268","Choosing a default expression for common audit and identifier fields, and whether each should apply on update",[45,514,515],{},"Common defaults and when they recalculate",[49,517,518],{},"A table of six fields. inspection_uid uses uuid and is set once. created_at uses now and is set once. created_by uses user_account_name and is set once. modified_at uses now and applies on update. modified_by uses user_full_name and applies on update. area_m2 uses rounded area and applies on update so it follows geometry edits.",[53,520],{"x":55,"y":55,"width":56,"height":521,"fill":58},"268",[77,523,525],{"x":79,"y":524,"style":81,"fill":82,"textAnchor":83},"26","Set once, or keep current?",[53,527],{"x":528,"y":88,"width":529,"height":530,"rx":97,"fill":176,"stroke":177,"style":114},"24","712","210",[77,532,535],{"x":89,"y":533,"style":534,"fill":82,"textAnchor":83},"66","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","field",[77,537,539],{"x":538,"y":533,"style":534,"fill":82,"textAnchor":83},"390","expression",[77,541,543],{"x":542,"y":533,"style":534,"fill":82,"textAnchor":83},"630","applyOnUpdate",[86,545],{"x1":88,"y1":546,"x2":90,"y2":546,"stroke":547,"style":548},"76","#d9d3c4","stroke-width:1.5",[77,550,553],{"x":89,"y":551,"style":552,"fill":75,"textAnchor":83},"100","text-anchor:middle;font-size:10px;font-family:monospace","inspection_uid",[77,555,505],{"x":538,"y":551,"style":552,"fill":75,"textAnchor":83},[77,557,559],{"x":542,"y":551,"style":558,"fill":98,"textAnchor":83},"text-anchor:middle;font-size:10.5px;font-family:sans-serif","once",[77,561,563],{"x":89,"y":562,"style":552,"fill":75,"textAnchor":83},"128","created_at",[77,565,124],{"x":538,"y":562,"style":552,"fill":75,"textAnchor":83},[77,567,559],{"x":542,"y":562,"style":558,"fill":98,"textAnchor":83},[77,569,571],{"x":89,"y":570,"style":552,"fill":75,"textAnchor":83},"156","created_by",[77,573,128],{"x":538,"y":570,"style":552,"fill":75,"textAnchor":83},[77,575,559],{"x":542,"y":570,"style":558,"fill":98,"textAnchor":83},[77,577,579],{"x":89,"y":578,"style":552,"fill":75,"textAnchor":83},"184","modified_at",[77,581,124],{"x":538,"y":578,"style":552,"fill":75,"textAnchor":83},[77,583,584],{"x":542,"y":578,"style":534,"fill":106,"textAnchor":83},"every edit",[77,586,588],{"x":89,"y":587,"style":552,"fill":75,"textAnchor":83},"212","modified_by",[77,590,501],{"x":538,"y":587,"style":552,"fill":75,"textAnchor":83},[77,592,584],{"x":542,"y":587,"style":534,"fill":106,"textAnchor":83},[77,594,595],{"x":89,"y":152,"style":552,"fill":75,"textAnchor":83},"area_m2",[77,597,598],{"x":538,"y":152,"style":552,"fill":75,"textAnchor":83},"round($area, 1)",[77,600,584],{"x":542,"y":152,"style":534,"fill":106,"textAnchor":83},[14,602,603,604,607],{},"A tempting default for a sequential id is ",[223,605,606],{},"maximum(\"inspection_no\") + 1",". It works for one editor and breaks for two: both compute the same maximum before either commits. If the numbers must be sequential, let the database assign them with a sequence and read them back after the commit; if they only need to be unique, use a UUID.",[189,609,611],{"id":610},"database-defaults","Database defaults",[14,613,614,615,618,619,622,623,626],{},"Layers backed by PostGIS or GeoPackage may already have defaults defined in the table — a ",[223,616,617],{},"serial"," key, ",[223,620,621],{},"DEFAULT now()",". QGIS reads those as provider defaults and shows the SQL clause in the form, such as ",[223,624,625],{},"nextval('inspections_id_seq')",", until the feature is committed.",[228,628,630],{"className":230,"code":629,"language":232,"meta":233,"style":233},"from qgis.core import Qgis\n\nprovider = inspections.dataProvider()\nfor idx, field in enumerate(inspections.fields()):\n    clause = provider.defaultValueClause(idx)\n    if clause:\n        print(f\"{field.name():\u003C16} provider default: {clause}\")\n\nQgsProject.instance().setFlag(Qgis.ProjectFlag.EvaluateDefaultValuesOnProviderSide, True)\n",[223,631,632,643,647,657,674,684,691,731,735],{"__ignoreMap":233},[237,633,634,636,638,640],{"class":86,"line":239},[237,635,243],{"class":242},[237,637,247],{"class":246},[237,639,250],{"class":242},[237,641,642],{"class":246}," Qgis\n",[237,644,645],{"class":86,"line":256},[237,646,260],{"emptyLinePlaceholder":259},[237,648,649,652,654],{"class":86,"line":263},[237,650,651],{"class":246},"provider ",[237,653,269],{"class":242},[237,655,656],{"class":246}," inspections.dataProvider()\n",[237,658,659,662,665,668,671],{"class":86,"line":288},[237,660,661],{"class":242},"for",[237,663,664],{"class":246}," idx, field ",[237,666,667],{"class":242},"in",[237,669,670],{"class":282}," enumerate",[237,672,673],{"class":246},"(inspections.fields()):\n",[237,675,676,679,681],{"class":86,"line":293},[237,677,678],{"class":246},"    clause ",[237,680,269],{"class":242},[237,682,683],{"class":246}," provider.defaultValueClause(idx)\n",[237,685,686,688],{"class":86,"line":314},[237,687,328],{"class":242},[237,689,690],{"class":246}," clause:\n",[237,692,693,696,699,702,705,708,711,714,717,720,722,725,727,729],{"class":86,"line":325},[237,694,695],{"class":282},"        print",[237,697,698],{"class":246},"(",[237,700,701],{"class":242},"f",[237,703,704],{"class":275},"\"",[237,706,707],{"class":282},"{",[237,709,710],{"class":246},"field.name()",[237,712,713],{"class":242},":\u003C16",[237,715,716],{"class":282},"}",[237,718,719],{"class":275}," provider default: ",[237,721,707],{"class":282},[237,723,724],{"class":246},"clause",[237,726,716],{"class":282},[237,728,704],{"class":275},[237,730,381],{"class":246},[237,732,733],{"class":86,"line":343},[237,734,260],{"emptyLinePlaceholder":259},[237,736,737,740,742],{"class":86,"line":355},[237,738,739],{"class":246},"QgsProject.instance().setFlag(Qgis.ProjectFlag.EvaluateDefaultValuesOnProviderSide, ",[237,741,434],{"class":282},[237,743,381],{"class":246},[14,745,746,748,749,752,753,755],{},[200,747,487],{}," ",[223,750,751],{},"defaultValueClause"," returns the database's own default expression, which the provider will apply on insert. Setting the project flag asks QGIS to evaluate those defaults immediately when the feature is created, so users see the actual next id rather than the clause text — at the cost of a round trip to the database per new feature, and of consuming a sequence value even if the user cancels. A layer-level ",[223,754,225],{}," on the same field overrides the database default in the form, so avoid setting both on key fields.",[189,757,759],{"id":758},"constraints-not-null-unique-and-expressions","Constraints: not null, unique and expressions",[14,761,762,763,766,767,770],{},"Constraints are rules checked in the form and when edits are committed. Each has a strength: ",[18,764,765],{},"hard"," constraints block the feature from being accepted, ",[18,768,769],{},"soft"," constraints show a warning but let the user continue.",[228,772,774],{"className":230,"code":773,"language":232,"meta":233,"style":233},"from qgis.core import QgsFieldConstraints\n\nHARD = QgsFieldConstraints.ConstraintStrength.ConstraintStrengthHard\nSOFT = QgsFieldConstraints.ConstraintStrength.ConstraintStrengthSoft\n\ndef idx(name):\n    return inspections.fields().indexOf(name)\n\ninspections.setFieldConstraint(\n    idx(\"asset_id\"), QgsFieldConstraints.Constraint.ConstraintNotNull, HARD)\ninspections.setFieldConstraint(\n    idx(\"inspection_uid\"), QgsFieldConstraints.Constraint.ConstraintUnique, HARD)\n\ninspections.setConstraintExpression(\n    idx(\"condition\"),\n    '\"condition\" BETWEEN 1 AND 5',\n    \"Condition must be scored 1 (worst) to 5 (best)\",\n)\ninspections.setFieldConstraint(\n    idx(\"condition\"), QgsFieldConstraints.Constraint.ConstraintExpression, HARD)\n\ninspections.setConstraintExpression(\n    idx(\"notes\"),\n    '\"condition\" > 2 OR length(coalesce(\"notes\", \\'\\')) >= 20',\n    \"Poor or worse condition needs a note of at least 20 characters\",\n)\ninspections.setFieldConstraint(\n    idx(\"notes\"), QgsFieldConstraints.Constraint.ConstraintExpression, SOFT)\n",[223,775,776,787,791,802,812,816,826,834,838,843,858,862,875,879,884,894,902,910,915,920,934,939,944,954,968,976,981,986],{"__ignoreMap":233},[237,777,778,780,782,784],{"class":86,"line":239},[237,779,243],{"class":242},[237,781,247],{"class":246},[237,783,250],{"class":242},[237,785,786],{"class":246}," QgsFieldConstraints\n",[237,788,789],{"class":86,"line":256},[237,790,260],{"emptyLinePlaceholder":259},[237,792,793,796,799],{"class":86,"line":263},[237,794,795],{"class":282},"HARD",[237,797,798],{"class":242}," =",[237,800,801],{"class":246}," QgsFieldConstraints.ConstraintStrength.ConstraintStrengthHard\n",[237,803,804,807,809],{"class":86,"line":288},[237,805,806],{"class":282},"SOFT",[237,808,798],{"class":242},[237,810,811],{"class":246}," QgsFieldConstraints.ConstraintStrength.ConstraintStrengthSoft\n",[237,813,814],{"class":86,"line":293},[237,815,260],{"emptyLinePlaceholder":259},[237,817,818,820,823],{"class":86,"line":314},[237,819,296],{"class":242},[237,821,822],{"class":299}," idx",[237,824,825],{"class":246},"(name):\n",[237,827,828,831],{"class":86,"line":325},[237,829,830],{"class":242},"    return",[237,832,833],{"class":246}," inspections.fields().indexOf(name)\n",[237,835,836],{"class":86,"line":343},[237,837,260],{"emptyLinePlaceholder":259},[237,839,840],{"class":86,"line":355},[237,841,842],{"class":246},"inspections.setFieldConstraint(\n",[237,844,845,848,851,854,856],{"class":86,"line":361},[237,846,847],{"class":246},"    idx(",[237,849,850],{"class":275},"\"asset_id\"",[237,852,853],{"class":246},"), QgsFieldConstraints.Constraint.ConstraintNotNull, ",[237,855,795],{"class":282},[237,857,381],{"class":246},[237,859,860],{"class":86,"line":366},[237,861,842],{"class":246},[237,863,864,866,868,871,873],{"class":86,"line":384},[237,865,847],{"class":246},[237,867,372],{"class":275},[237,869,870],{"class":246},"), QgsFieldConstraints.Constraint.ConstraintUnique, ",[237,872,795],{"class":282},[237,874,381],{"class":246},[237,876,877],{"class":86,"line":399},[237,878,260],{"emptyLinePlaceholder":259},[237,880,881],{"class":86,"line":414},[237,882,883],{"class":246},"inspections.setConstraintExpression(\n",[237,885,886,888,891],{"class":86,"line":439},[237,887,847],{"class":246},[237,889,890],{"class":275},"\"condition\"",[237,892,893],{"class":246},"),\n",[237,895,896,899],{"class":86,"line":462},[237,897,898],{"class":275},"    '\"condition\" BETWEEN 1 AND 5'",[237,900,901],{"class":246},",\n",[237,903,905,908],{"class":86,"line":904},17,[237,906,907],{"class":275},"    \"Condition must be scored 1 (worst) to 5 (best)\"",[237,909,901],{"class":246},[237,911,913],{"class":86,"line":912},18,[237,914,381],{"class":246},[237,916,918],{"class":86,"line":917},19,[237,919,842],{"class":246},[237,921,923,925,927,930,932],{"class":86,"line":922},20,[237,924,847],{"class":246},[237,926,890],{"class":275},[237,928,929],{"class":246},"), QgsFieldConstraints.Constraint.ConstraintExpression, ",[237,931,795],{"class":282},[237,933,381],{"class":246},[237,935,937],{"class":86,"line":936},21,[237,938,260],{"emptyLinePlaceholder":259},[237,940,942],{"class":86,"line":941},22,[237,943,883],{"class":246},[237,945,947,949,952],{"class":86,"line":946},23,[237,948,847],{"class":246},[237,950,951],{"class":275},"\"notes\"",[237,953,893],{"class":246},[237,955,957,960,963,966],{"class":86,"line":956},24,[237,958,959],{"class":275},"    '\"condition\" > 2 OR length(coalesce(\"notes\", ",[237,961,962],{"class":282},"\\'\\'",[237,964,965],{"class":275},")) >= 20'",[237,967,901],{"class":246},[237,969,971,974],{"class":86,"line":970},25,[237,972,973],{"class":275},"    \"Poor or worse condition needs a note of at least 20 characters\"",[237,975,901],{"class":246},[237,977,979],{"class":86,"line":978},26,[237,980,381],{"class":246},[237,982,984],{"class":86,"line":983},27,[237,985,842],{"class":246},[237,987,989,991,993,995,997],{"class":86,"line":988},28,[237,990,847],{"class":246},[237,992,951],{"class":275},[237,994,929],{"class":246},[237,996,806],{"class":282},[237,998,381],{"class":246},[14,1000,1001,748,1003,1006,1007,1010,1011,1014,1015,1018],{},[200,1002,487],{},[223,1004,1005],{},"setConstraintExpression"," stores the rule and the message users see; ",[223,1008,1009],{},"setFieldConstraint"," with ",[223,1012,1013],{},"ConstraintExpression"," sets its strength. Expression constraints can reference other fields, which is what makes the conditional rule on ",[223,1016,1017],{},"notes"," possible — the note is only demanded when the condition is poor. Making that one soft is a judgement call: it nudges field crews without stopping them saving an urgent record when they are standing in the rain. Unique constraints are checked against the whole layer, including uncommitted edits, which can be slow on very large tables; a unique index in the database is the stronger and faster guarantee where one is available.",[14,1020,1021],{},[38,1022,1025,1028,1031,1034,1037,1042,1048,1052,1056,1059,1062,1065,1069,1072,1075,1078],{"viewBox":1023,"role":41,"ariaLabel":1024,"xmlns":43},"0 0 760 250","Constraint origin and strength: provider constraints come from the database and are always hard, layer constraints are set in QGIS and can be hard or soft, and only provider constraints also protect writes from outside QGIS",[45,1026,1027],{},"Where a constraint comes from decides what it protects",[49,1029,1030],{},"Two columns. Provider constraints such as NOT NULL and UNIQUE defined in PostGIS or GeoPackage are read by QGIS automatically, are always hard, and also reject writes made by other software. Layer constraints are defined in QGIS, stored in the project or style, can be hard or soft, can use any expression, and only apply to edits made through QGIS.",[53,1032],{"x":55,"y":55,"width":56,"height":1033,"fill":58},"250",[77,1035,1036],{"x":79,"y":524,"style":81,"fill":82,"textAnchor":83},"Two origins, different reach",[53,1038],{"x":528,"y":1039,"width":1040,"height":578,"rx":97,"fill":133,"stroke":102,"style":1041},"46","344","stroke-width:2.5",[77,1043,1047],{"x":1044,"y":1045,"style":1046,"fill":102,"textAnchor":83},"196","72","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","provider constraint",[77,1049,1051],{"x":1044,"y":1050,"style":558,"fill":75,"textAnchor":83},"102","defined in the database",[77,1053,1055],{"x":1044,"y":1054,"style":558,"fill":75,"textAnchor":83},"126","always hard",[77,1057,1058],{"x":1044,"y":89,"style":558,"fill":75,"textAnchor":83},"NOT NULL, UNIQUE, CHECK",[77,1060,1061],{"x":1044,"y":1044,"style":534,"fill":143,"textAnchor":83},"protects every client",[53,1063],{"x":1064,"y":1039,"width":1040,"height":578,"rx":97,"fill":113,"stroke":98,"style":1041},"392",[77,1066,1068],{"x":1067,"y":1045,"style":1046,"fill":98,"textAnchor":83},"564","layer constraint",[77,1070,1071],{"x":1067,"y":1050,"style":558,"fill":75,"textAnchor":83},"stored in project or style",[77,1073,1074],{"x":1067,"y":1054,"style":558,"fill":75,"textAnchor":83},"hard or soft",[77,1076,1077],{"x":1067,"y":89,"style":558,"fill":75,"textAnchor":83},"any QGIS expression",[77,1079,1080],{"x":1067,"y":1044,"style":534,"fill":106,"textAnchor":83},"only edits made in QGIS",[14,1082,1083],{},"To see what a field is subject to, including rules that came from the database, read its constraints object:",[228,1085,1087],{"className":230,"code":1086,"language":232,"meta":233,"style":233},"field = inspections.fields().field(\"asset_id\")\nconstraints = field.constraints()\nfor c in (QgsFieldConstraints.Constraint.ConstraintNotNull,\n          QgsFieldConstraints.Constraint.ConstraintUnique,\n          QgsFieldConstraints.Constraint.ConstraintExpression):\n    if constraints.constraints() & c:\n        origin = constraints.constraintOrigin(c)\n        print(c, \"origin:\", origin, \"strength:\", constraints.constraintStrength(c))\n",[223,1088,1089,1103,1113,1125,1130,1135,1148,1158],{"__ignoreMap":233},[237,1090,1091,1094,1096,1099,1101],{"class":86,"line":239},[237,1092,1093],{"class":246},"field ",[237,1095,269],{"class":242},[237,1097,1098],{"class":246}," inspections.fields().field(",[237,1100,850],{"class":275},[237,1102,381],{"class":246},[237,1104,1105,1108,1110],{"class":86,"line":256},[237,1106,1107],{"class":246},"constraints ",[237,1109,269],{"class":242},[237,1111,1112],{"class":246}," field.constraints()\n",[237,1114,1115,1117,1120,1122],{"class":86,"line":263},[237,1116,661],{"class":242},[237,1118,1119],{"class":246}," c ",[237,1121,667],{"class":242},[237,1123,1124],{"class":246}," (QgsFieldConstraints.Constraint.ConstraintNotNull,\n",[237,1126,1127],{"class":86,"line":288},[237,1128,1129],{"class":246},"          QgsFieldConstraints.Constraint.ConstraintUnique,\n",[237,1131,1132],{"class":86,"line":293},[237,1133,1134],{"class":246},"          QgsFieldConstraints.Constraint.ConstraintExpression):\n",[237,1136,1137,1139,1142,1145],{"class":86,"line":314},[237,1138,328],{"class":242},[237,1140,1141],{"class":246}," constraints.constraints() ",[237,1143,1144],{"class":242},"&",[237,1146,1147],{"class":246}," c:\n",[237,1149,1150,1153,1155],{"class":86,"line":325},[237,1151,1152],{"class":246},"        origin ",[237,1154,269],{"class":242},[237,1156,1157],{"class":246}," constraints.constraintOrigin(c)\n",[237,1159,1160,1162,1165,1168,1171,1174],{"class":86,"line":343},[237,1161,695],{"class":282},[237,1163,1164],{"class":246},"(c, ",[237,1166,1167],{"class":275},"\"origin:\"",[237,1169,1170],{"class":246},", origin, ",[237,1172,1173],{"class":275},"\"strength:\"",[237,1175,1176],{"class":246},", constraints.constraintStrength(c))\n",[14,1178,1179,748,1181,1184,1185,1188,1189,1192],{},[200,1180,487],{},[223,1182,1183],{},"constraintOrigin"," distinguishes ",[223,1186,1187],{},"ConstraintOriginProvider"," — the database said so — from ",[223,1190,1191],{},"ConstraintOriginLayer"," — somebody set it in QGIS. Provider constraints cannot be removed from QGIS, only from the database schema. When a project must also be safe against edits from other tools, move the important rules into the database and let QGIS pick them up.",[189,1194,1196],{"id":1195},"validate-features-created-by-scripts","Validate features created by scripts",[14,1198,1199,1200,1203,1204,1207,1208,1211],{},"Defaults and constraints are applied by the form. A script that builds a ",[223,1201,1202],{},"QgsFeature"," and calls ",[223,1205,1206],{},"addFeature"," sees neither — unless it asks. ",[223,1209,1210],{},"QgsVectorLayerUtils"," creates features with defaults evaluated and validates attributes against every constraint.",[228,1213,1215],{"className":230,"code":1214,"language":232,"meta":233,"style":233},"from qgis.core import QgsGeometry, QgsPointXY, QgsVectorLayerUtils, edit\n\ngeom = QgsGeometry.fromPointXY(QgsPointXY(429812.4, 433120.7))\nattrs = {idx(\"asset_id\"): \"TR-10432\", idx(\"condition\"): 2}\nfeature = QgsVectorLayerUtils.createFeature(\n    inspections, geom, attrs, inspections.createExpressionContext())\n\nproblems = []\nfor i in range(inspections.fields().count()):\n    ok, errors = QgsVectorLayerUtils.validateAttribute(inspections, feature, i)\n    if not ok:\n        problems.append((inspections.fields()[i].name(), errors))\n\nif problems:\n    print(\"rejected:\", problems)\nelse:\n    with edit(inspections):\n        inspections.addFeature(feature)\n",[223,1216,1217,1228,1232,1253,1284,1294,1299,1303,1313,1328,1338,1348,1353,1357,1365,1378,1385,1393],{"__ignoreMap":233},[237,1218,1219,1221,1223,1225],{"class":86,"line":239},[237,1220,243],{"class":242},[237,1222,247],{"class":246},[237,1224,250],{"class":242},[237,1226,1227],{"class":246}," QgsGeometry, QgsPointXY, QgsVectorLayerUtils, edit\n",[237,1229,1230],{"class":86,"line":256},[237,1231,260],{"emptyLinePlaceholder":259},[237,1233,1234,1237,1239,1242,1245,1247,1250],{"class":86,"line":263},[237,1235,1236],{"class":246},"geom ",[237,1238,269],{"class":242},[237,1240,1241],{"class":246}," QgsGeometry.fromPointXY(QgsPointXY(",[237,1243,1244],{"class":282},"429812.4",[237,1246,375],{"class":246},[237,1248,1249],{"class":282},"433120.7",[237,1251,1252],{"class":246},"))\n",[237,1254,1255,1258,1260,1263,1265,1268,1271,1274,1276,1278,1281],{"class":86,"line":288},[237,1256,1257],{"class":246},"attrs ",[237,1259,269],{"class":242},[237,1261,1262],{"class":246}," {idx(",[237,1264,850],{"class":275},[237,1266,1267],{"class":246},"): ",[237,1269,1270],{"class":275},"\"TR-10432\"",[237,1272,1273],{"class":246},", idx(",[237,1275,890],{"class":275},[237,1277,1267],{"class":246},[237,1279,1280],{"class":282},"2",[237,1282,1283],{"class":246},"}\n",[237,1285,1286,1289,1291],{"class":86,"line":293},[237,1287,1288],{"class":246},"feature ",[237,1290,269],{"class":242},[237,1292,1293],{"class":246}," QgsVectorLayerUtils.createFeature(\n",[237,1295,1296],{"class":86,"line":314},[237,1297,1298],{"class":246},"    inspections, geom, attrs, inspections.createExpressionContext())\n",[237,1300,1301],{"class":86,"line":325},[237,1302,260],{"emptyLinePlaceholder":259},[237,1304,1305,1308,1310],{"class":86,"line":343},[237,1306,1307],{"class":246},"problems ",[237,1309,269],{"class":242},[237,1311,1312],{"class":246}," []\n",[237,1314,1315,1317,1320,1322,1325],{"class":86,"line":355},[237,1316,661],{"class":242},[237,1318,1319],{"class":246}," i ",[237,1321,667],{"class":242},[237,1323,1324],{"class":282}," range",[237,1326,1327],{"class":246},"(inspections.fields().count()):\n",[237,1329,1330,1333,1335],{"class":86,"line":361},[237,1331,1332],{"class":246},"    ok, errors ",[237,1334,269],{"class":242},[237,1336,1337],{"class":246}," QgsVectorLayerUtils.validateAttribute(inspections, feature, i)\n",[237,1339,1340,1342,1345],{"class":86,"line":366},[237,1341,328],{"class":242},[237,1343,1344],{"class":242}," not",[237,1346,1347],{"class":246}," ok:\n",[237,1349,1350],{"class":86,"line":384},[237,1351,1352],{"class":246},"        problems.append((inspections.fields()[i].name(), errors))\n",[237,1354,1355],{"class":86,"line":399},[237,1356,260],{"emptyLinePlaceholder":259},[237,1358,1359,1362],{"class":86,"line":414},[237,1360,1361],{"class":242},"if",[237,1363,1364],{"class":246}," problems:\n",[237,1366,1367,1370,1372,1375],{"class":86,"line":439},[237,1368,1369],{"class":282},"    print",[237,1371,698],{"class":246},[237,1373,1374],{"class":275},"\"rejected:\"",[237,1376,1377],{"class":246},", problems)\n",[237,1379,1380,1383],{"class":86,"line":462},[237,1381,1382],{"class":242},"else",[237,1384,340],{"class":246},[237,1386,1387,1390],{"class":86,"line":904},[237,1388,1389],{"class":242},"    with",[237,1391,1392],{"class":246}," edit(inspections):\n",[237,1394,1395],{"class":86,"line":912},[237,1396,1397],{"class":246},"        inspections.addFeature(feature)\n",[14,1399,1400,748,1402,1405,1406,1409,1410,1412,1413,1415,1416,1419,1420,1424],{},[200,1401,487],{},[223,1403,1404],{},"createFeature"," fills every field that has a default — UUID, timestamps, user — and then applies the attributes you pass, so explicit values win. ",[223,1407,1408],{},"validateAttribute"," checks all constraints on one field, hard and soft, and returns the messages defined with the expressions, which makes the rejection log readable. In this example the soft rule on ",[223,1411,1017],{}," fails because condition 2 has no note; deciding whether a script should honour soft rules as strictly as hard ones is a policy choice, and passing a strength argument to ",[223,1414,1408],{}," lets you check hard constraints only. The ",[223,1417,1418],{},"edit"," context manager commits on success and rolls back on an exception, as covered in ",[30,1421,1423],{"href":1422},"\u002Fspatial-data-processing-automation\u002Fvector-data-manipulation\u002Fedit-features-with-transactions-pyqgis\u002F","editing features with transactions",".",[189,1426,1428],{"id":1427},"qgis-version-compatibility","QGIS version compatibility",[14,1430,1431,1010,1433,1435,1436,1439,1440,1443,1444,1447,1448,1451,1452,1454],{},[223,1432,225],{},[223,1434,543],{}," has existed since 3.2. The scoped enums ",[223,1437,1438],{},"QgsFieldConstraints.Constraint.ConstraintNotNull"," and ",[223,1441,1442],{},"QgsFieldConstraints.ConstraintStrength.ConstraintStrengthHard"," work on all 3.x releases and are required on the QGIS 4 series. ",[223,1445,1446],{},"Qgis.ProjectFlag.EvaluateDefaultValuesOnProviderSide"," replaced ",[223,1449,1450],{},"setEvaluateDefaultValues"," in 3.26. ",[223,1453,505],{}," accepts its format argument from 3.18; older releases always return braces.",[189,1456,1458],{"id":1457},"troubleshooting","Troubleshooting",[194,1460,1461,1469,1479,1488,1498],{},[197,1462,1463,1466,1467,1424],{},[200,1464,1465],{},"A default never appears."," The field is filled by a provider default that overrides it, or the feature was created by a script without ",[223,1468,1404],{},[197,1470,1471,748,1476,1478],{},[200,1472,1473,1475],{},[223,1474,579],{}," does not change.",[223,1477,543],{}," was left False.",[197,1480,1481,1484,1485,1487],{},[200,1482,1483],{},"Users can save despite a failing rule."," The constraint is soft, or only the expression was set without ",[223,1486,1009],{}," for its strength.",[197,1489,1490,1497],{},[200,1491,1492,1493,1496],{},"The form shows ",[223,1494,1495],{},"nextval(…)"," as text."," That is the provider default clause; enable provider-side evaluation to see the value.",[197,1499,1500,1503],{},[200,1501,1502],{},"Unique checks make saving slow."," The layer is large; enforce uniqueness in the database instead.",[189,1505,1507],{"id":1506},"conclusion","Conclusion",[14,1509,1510,1511,1513,1514,1516],{},"Use expression defaults for identifiers, audit fields and derived values, with ",[223,1512,543],{}," for anything that must follow later edits. Add not-null, unique and expression constraints with hard strength for rules that must hold and soft strength for guidance. Push the non-negotiable rules into the database, and route script-created features through ",[223,1515,1210],{}," so they meet the same standard as features entered by hand.",[189,1518,1520],{"id":1519},"frequently-asked-questions","Frequently Asked Questions",[14,1522,1523,1526,1527,1529],{},[200,1524,1525],{},"Can a default reference other fields of the same feature?","\nYes. Field references in a default expression read the current values, which is why ",[223,1528,543],{}," defaults can derive one field from others.",[14,1531,1532,1535,1536,1424],{},[200,1533,1534],{},"Do constraints check existing features?","\nOnly when those features are edited. To audit a whole layer, loop over it with ",[223,1537,1408],{},[14,1539,1540,1543,1544,1439,1547,1550],{},[200,1541,1542],{},"Are defaults and constraints saved in QML styles?","\nYes, under the ",[223,1545,1546],{},"Fields",[223,1548,1549],{},"Forms"," style categories, so they travel with a style saved to the data source.",[14,1552,1553,1556],{},[200,1554,1555],{},"Can I show the constraint message in another language?","\nThe message is stored as text; set it from your plugin using its translation function when you configure the layer.",[189,1558,1560],{"id":1559},"related","Related",[194,1562,1563,1568,1574,1580,1585],{},[197,1564,1565,1567],{},[30,1566,33],{"href":32}," — the guide this recipe belongs to",[197,1569,1570],{},[30,1571,1573],{"href":1572},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fconfigure-editor-widgets-pyqgis\u002F","Configure Editor Widgets in PyQGIS",[197,1575,1576],{},[30,1577,1579],{"href":1578},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fadd-python-logic-to-attribute-form-pyqgis\u002F","Add Python Logic to an Attribute Form in PyQGIS",[197,1581,1582],{},[30,1583,1584],{"href":1422},"Edit Features with Transactions in PyQGIS",[197,1586,1587],{},[30,1588,1590],{"href":1589},"\u002Fspatial-data-processing-automation\u002Fattribute-tables-and-field-management\u002Fupdate-attribute-values-pyqgis\u002F","Update Attribute Values in Bulk with PyQGIS",[1592,1593,1594],"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 .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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);}",{"title":233,"searchDepth":256,"depth":256,"links":1596},[1597,1598,1599,1600,1601,1602,1603,1604,1605,1606],{"id":191,"depth":256,"text":192},{"id":217,"depth":256,"text":218},{"id":610,"depth":256,"text":611},{"id":758,"depth":256,"text":759},{"id":1195,"depth":256,"text":1196},{"id":1427,"depth":256,"text":1428},{"id":1457,"depth":256,"text":1458},{"id":1506,"depth":256,"text":1507},{"id":1519,"depth":256,"text":1520},{"id":1559,"depth":256,"text":1560},"Fill fields automatically with QgsDefaultValue expressions — timestamps, editor names, UUIDs, calculated areas — and enforce not-null, unique and expression constraints with hard or soft strength, then validate features created by scripts.","md",{"slug":1610,"type":1611,"breadcrumb":1612,"datePublished":1613,"dateModified":1613},"set-default-values-and-field-constraints-pyqgis","article","Default Values and Constraints","2026-09-17","\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fset-default-values-and-field-constraints-pyqgis",{"title":5,"description":1607},"qgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fset-default-values-and-field-constraints-pyqgis\u002Findex","gGb7jP_SBtG6LV_ZFMAZa0CxNDqHRRWqOXo8gmMeAJM",1789632907437]