[{"data":1,"prerenderedAt":1590},["ShallowReactive",2],{"doc:\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-text-annotation-to-map-pyqgis":3},{"id":4,"title":5,"body":6,"description":1579,"extension":1580,"meta":1581,"navigation":262,"path":1586,"seo":1587,"stem":1588,"__hash__":1589},"docs\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-text-annotation-to-map-pyqgis\u002Findex.md","Add a Text Annotation to a Map in PyQGIS",{"type":7,"value":8,"toc":1565},"minimark",[9,13,17,26,162,167,195,199,204,375,401,407,411,414,535,543,641,645,656,813,840,853,857,860,889,902,906,909,1206,1223,1226,1230,1233,1306,1315,1319,1325,1405,1409,1462,1466,1478,1482,1493,1504,1510,1529,1533,1561],[10,11,5],"h1",{"id":12},"add-a-text-annotation-to-a-map-in-pyqgis",[14,15,16],"p",{},"Labels come from data. Annotations do not — they are the source note in the corner, the arrow pointing at the anomaly, the \"provisional, do not circulate\" that has to appear on every export. QGIS keeps them in a separate system from labelling, with its own classes, its own storage and its own way of deciding where a thing sits when the map moves.",[14,18,19,20,25],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002F","Labeling & Annotations in PyQGIS",". It covers creating text annotations from Python, anchoring them to a map coordinate rather than to the screen, using the annotation layer introduced in QGIS 3.16, styling with HTML, and making sure the notes survive a save.",[14,27,28],{},[29,30,35,39,43,50,67,76,86,92,102,108,115,121,128,133,137,141,144,149,154,157],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 304","img","Two annotation anchoring modes: one pinned to a map coordinate so it moves with the terrain when the map pans, and one pinned to the canvas so it stays in the same corner regardless of the view","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Map-anchored and screen-anchored annotations",[40,41,42],"desc",{},"A map anchored annotation is attached to a coordinate, so panning the map carries it along and it can leave the view entirely. A screen anchored annotation is attached to a position on the canvas, so it stays in the same corner whatever the map shows, which is what a source note or a draft stamp needs.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","304","#f6f3ea",[51,52,53],"defs",{},[54,55,62],"marker",{"id":56,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"annArrow","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-weight:bold;font-family:sans-serif","#17211d","middle","The anchor decides what happens when the map moves",[44,77],{"x":78,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":84,"style":85},"26","52","330","216","10","#fffdf7","#2563eb","stroke-width:2.5",[68,87,91],{"x":88,"y":89,"style":90,"fill":84,"textAnchor":74},"191","78","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","anchored to a coordinate",[44,93],{"x":94,"y":95,"width":96,"height":97,"rx":98,"fill":99,"stroke":100,"style":101},"50","92","282","120","4","#e7e2d4","#59645f","stroke-width:1.4",[103,104],"circle",{"cx":105,"cy":106,"r":59,"fill":107},"132","176","#b91c1c",[44,109],{"x":110,"y":111,"width":112,"height":113,"rx":59,"fill":83,"stroke":84,"style":114},"150","118","140","42","stroke-width:1.8",[68,116,120],{"x":117,"y":118,"style":119,"fill":66,"textAnchor":74},"220","144","text-anchor:middle;font-size:10px;font-family:sans-serif","subsidence reported",[122,123],"line",{"x1":110,"y1":124,"x2":125,"y2":126,"stroke":84,"style":127},"152","138","172","stroke-width:1.6;marker-end:url(#annArrow)",[68,129,132],{"x":88,"y":130,"style":131,"fill":66,"textAnchor":74},"238","text-anchor:middle;font-size:10.5px;font-family:sans-serif","pans with the map, can scroll out of view",[44,134],{"x":135,"y":79,"width":80,"height":81,"rx":82,"fill":83,"stroke":136,"style":85},"404","#15803d",[68,138,140],{"x":139,"y":89,"style":90,"fill":136,"textAnchor":74},"569","anchored to the canvas",[44,142],{"x":143,"y":95,"width":96,"height":97,"rx":98,"fill":99,"stroke":100,"style":101},"428",[44,145],{"x":146,"y":126,"width":147,"height":148,"rx":98,"fill":83,"stroke":136,"style":114},"440","180","30",[68,150,153],{"x":151,"y":152,"style":119,"fill":66,"textAnchor":74},"530","192","Source: Ordnance data 2026",[68,155,156],{"x":139,"y":130,"style":131,"fill":66,"textAnchor":74},"stays put whatever the map shows",[68,158,161],{"x":70,"y":159,"style":160,"fill":100,"textAnchor":74},"290","text-anchor:middle;font-size:11px;font-family:sans-serif","both are annotations; only the anchor differs",[163,164,166],"h2",{"id":165},"prerequisites","Prerequisites",[168,169,170,188],"ul",{},[171,172,173,177,178,182,183,187],"li",{},[174,175,176],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer. The annotation ",[179,180,181],"em",{},"layer"," arrived in 3.16; the older ",[184,185,186],"code",{},"QgsTextAnnotation"," still works and is what the canvas annotation tools produce.",[171,189,190,191,194],{},"A project with a map canvas. Most of this needs ",[184,192,193],{},"iface",", so a fully headless script uses the annotation-layer route instead.",[163,196,198],{"id":197},"the-simple-case-a-note-pinned-to-the-canvas","The simple case: a note pinned to the canvas",[14,200,201,203],{},[184,202,186],{}," holds a rich-text document and a placement.",[205,206,211],"pre",{"className":207,"code":208,"language":209,"meta":210,"style":210},"language-python shiki shiki-themes github-dark","from qgis.core import QgsTextAnnotation, QgsProject\nfrom qgis.PyQt.QtGui import QTextDocument, QColor\nfrom qgis.PyQt.QtCore import QPointF, QSizeF\n\nannotation = QgsTextAnnotation()\ndocument = QTextDocument()\ndocument.setHtml(\n    '\u003Cp style=\"font-family:sans-serif;font-size:9pt\">'\n    'Source: Ordnance data 2026\u003Cbr>Provisional — not for circulation\u003C\u002Fp>'\n)\nannotation.setDocument(document)\nannotation.setHasFixedMapPosition(False)\nannotation.setRelativePosition(QPointF(0.02, 0.94))\nannotation.setFrameSizeMm(QSizeF(62, 12))\n\nQgsProject.instance().annotationManager().addAnnotation(annotation)\n","python","",[184,212,213,231,244,257,264,276,287,293,300,306,312,318,330,348,364,369],{"__ignoreMap":210},[214,215,217,221,225,228],"span",{"class":122,"line":216},1,[214,218,220],{"class":219},"snl16","from",[214,222,224],{"class":223},"s95oV"," qgis.core ",[214,226,227],{"class":219},"import",[214,229,230],{"class":223}," QgsTextAnnotation, QgsProject\n",[214,232,234,236,239,241],{"class":122,"line":233},2,[214,235,220],{"class":219},[214,237,238],{"class":223}," qgis.PyQt.QtGui ",[214,240,227],{"class":219},[214,242,243],{"class":223}," QTextDocument, QColor\n",[214,245,247,249,252,254],{"class":122,"line":246},3,[214,248,220],{"class":219},[214,250,251],{"class":223}," qgis.PyQt.QtCore ",[214,253,227],{"class":219},[214,255,256],{"class":223}," QPointF, QSizeF\n",[214,258,260],{"class":122,"line":259},4,[214,261,263],{"emptyLinePlaceholder":262},true,"\n",[214,265,267,270,273],{"class":122,"line":266},5,[214,268,269],{"class":223},"annotation ",[214,271,272],{"class":219},"=",[214,274,275],{"class":223}," QgsTextAnnotation()\n",[214,277,279,282,284],{"class":122,"line":278},6,[214,280,281],{"class":223},"document ",[214,283,272],{"class":219},[214,285,286],{"class":223}," QTextDocument()\n",[214,288,290],{"class":122,"line":289},7,[214,291,292],{"class":223},"document.setHtml(\n",[214,294,296],{"class":122,"line":295},8,[214,297,299],{"class":298},"sU2Wk","    '\u003Cp style=\"font-family:sans-serif;font-size:9pt\">'\n",[214,301,303],{"class":122,"line":302},9,[214,304,305],{"class":298},"    'Source: Ordnance data 2026\u003Cbr>Provisional — not for circulation\u003C\u002Fp>'\n",[214,307,309],{"class":122,"line":308},10,[214,310,311],{"class":223},")\n",[214,313,315],{"class":122,"line":314},11,[214,316,317],{"class":223},"annotation.setDocument(document)\n",[214,319,321,324,328],{"class":122,"line":320},12,[214,322,323],{"class":223},"annotation.setHasFixedMapPosition(",[214,325,327],{"class":326},"sDLfK","False",[214,329,311],{"class":223},[214,331,333,336,339,342,345],{"class":122,"line":332},13,[214,334,335],{"class":223},"annotation.setRelativePosition(QPointF(",[214,337,338],{"class":326},"0.02",[214,340,341],{"class":223},", ",[214,343,344],{"class":326},"0.94",[214,346,347],{"class":223},"))\n",[214,349,351,354,357,359,362],{"class":122,"line":350},14,[214,352,353],{"class":223},"annotation.setFrameSizeMm(QSizeF(",[214,355,356],{"class":326},"62",[214,358,341],{"class":223},[214,360,361],{"class":326},"12",[214,363,347],{"class":223},[214,365,367],{"class":122,"line":366},15,[214,368,263],{"emptyLinePlaceholder":262},[214,370,372],{"class":122,"line":371},16,[214,373,374],{"class":223},"QgsProject.instance().annotationManager().addAnnotation(annotation)\n",[14,376,377,380,381,384,385,388,389,392,393,396,397,400],{},[174,378,379],{},"Breakdown:"," ",[184,382,383],{},"setHasFixedMapPosition(False)"," is what makes this a screen-anchored note; ",[184,386,387],{},"setRelativePosition()"," then takes fractions of the canvas width and height, so ",[184,390,391],{},"(0.02, 0.94)"," is the bottom-left corner and stays there at any zoom. The content is a ",[184,394,395],{},"QTextDocument",", which means HTML — inline styles, line breaks, bold, even a small table. Adding it to the project's ",[184,398,399],{},"annotationManager()"," rather than to the canvas is what makes it part of the project and therefore savable.",[14,402,403,406],{},[184,404,405],{},"setFrameSizeMm()"," sizes the box in millimetres. Getting it wrong clips the text rather than wrapping it, so it is worth measuring once against a realistic string rather than guessing.",[163,408,410],{"id":409},"pinning-a-note-to-a-place","Pinning a note to a place",[14,412,413],{},"For a note that belongs to a location — an anomaly, a site, a measurement — the anchor is a map coordinate.",[205,415,417],{"className":207,"code":416,"language":209,"meta":210,"style":210},"from qgis.core import QgsPointXY\n\nannotation = QgsTextAnnotation()\ndocument = QTextDocument()\ndocument.setHtml('\u003Cp style=\"font-family:sans-serif;font-size:9pt\">Subsidence reported 2025-11\u003C\u002Fp>')\nannotation.setDocument(document)\n\nannotation.setHasFixedMapPosition(True)\nannotation.setMapPosition(QgsPointXY(432150, 189400))\nannotation.setMapPositionCrs(QgsProject.instance().crs())\nannotation.setFrameOffsetFromReferencePointMm(QPointF(8, -14))\nannotation.setFrameSizeMm(QSizeF(46, 10))\n\nQgsProject.instance().annotationManager().addAnnotation(annotation)\n",[184,418,419,430,434,442,450,460,464,468,477,492,497,514,527,531],{"__ignoreMap":210},[214,420,421,423,425,427],{"class":122,"line":216},[214,422,220],{"class":219},[214,424,224],{"class":223},[214,426,227],{"class":219},[214,428,429],{"class":223}," QgsPointXY\n",[214,431,432],{"class":122,"line":233},[214,433,263],{"emptyLinePlaceholder":262},[214,435,436,438,440],{"class":122,"line":246},[214,437,269],{"class":223},[214,439,272],{"class":219},[214,441,275],{"class":223},[214,443,444,446,448],{"class":122,"line":259},[214,445,281],{"class":223},[214,447,272],{"class":219},[214,449,286],{"class":223},[214,451,452,455,458],{"class":122,"line":266},[214,453,454],{"class":223},"document.setHtml(",[214,456,457],{"class":298},"'\u003Cp style=\"font-family:sans-serif;font-size:9pt\">Subsidence reported 2025-11\u003C\u002Fp>'",[214,459,311],{"class":223},[214,461,462],{"class":122,"line":278},[214,463,317],{"class":223},[214,465,466],{"class":122,"line":289},[214,467,263],{"emptyLinePlaceholder":262},[214,469,470,472,475],{"class":122,"line":295},[214,471,323],{"class":223},[214,473,474],{"class":326},"True",[214,476,311],{"class":223},[214,478,479,482,485,487,490],{"class":122,"line":302},[214,480,481],{"class":223},"annotation.setMapPosition(QgsPointXY(",[214,483,484],{"class":326},"432150",[214,486,341],{"class":223},[214,488,489],{"class":326},"189400",[214,491,347],{"class":223},[214,493,494],{"class":122,"line":308},[214,495,496],{"class":223},"annotation.setMapPositionCrs(QgsProject.instance().crs())\n",[214,498,499,502,504,506,509,512],{"class":122,"line":314},[214,500,501],{"class":223},"annotation.setFrameOffsetFromReferencePointMm(QPointF(",[214,503,58],{"class":326},[214,505,341],{"class":223},[214,507,508],{"class":219},"-",[214,510,511],{"class":326},"14",[214,513,347],{"class":223},[214,515,516,518,521,523,525],{"class":122,"line":320},[214,517,353],{"class":223},[214,519,520],{"class":326},"46",[214,522,341],{"class":223},[214,524,82],{"class":326},[214,526,347],{"class":223},[214,528,529],{"class":122,"line":332},[214,530,263],{"emptyLinePlaceholder":262},[214,532,533],{"class":122,"line":350},[214,534,374],{"class":223},[14,536,537,380,539,542],{},[174,538,379],{},[184,540,541],{},"setMapPositionCrs()"," matters and is easy to skip: without it the coordinate is interpreted in the project CRS anyway, but the annotation will not follow correctly if the project is later reprojected. Setting it explicitly makes the annotation transform properly. The frame offset is the leader-line vector in millimetres from the anchor to the box corner — a positive x and negative y put the note up and to the right, with QGIS drawing the connector automatically.",[14,544,545],{},[29,546,549,552,555,558,565,568,571,576,580,587,593,596,602,606,610,615,617,620,627,633,637],{"viewBox":547,"role":32,"ariaLabel":548,"xmlns":34},"0 0 760 292","The parts of a map anchored annotation: the anchor coordinate, the millimetre offset to the frame, the frame size, and the automatically drawn leader line between them",[36,550,551],{},"Anatomy of a map-anchored annotation",[40,553,554],{},"The anchor is a coordinate in a named CRS. The frame offset is a vector in millimetres from that anchor to the annotation box. The frame size fixes the box dimensions, and text longer than the box is clipped rather than wrapped. QGIS draws the connector between anchor and frame.",[44,556],{"x":46,"y":46,"width":47,"height":557,"fill":49},"292",[51,559,560],{},[54,561,563],{"id":562,"viewBox":57,"refX":58,"refY":59,"markerWidth":60,"markerHeight":60,"orient":61},"anaArrow",[63,564],{"d":65,"fill":66},[68,566,567],{"x":70,"y":71,"style":72,"fill":73,"textAnchor":74},"Four settings, and one of them silently clips your text",[103,569],{"cx":147,"cy":570,"r":60,"fill":107},"212",[68,572,575],{"x":147,"y":573,"style":574,"fill":107,"textAnchor":74},"242","text-anchor:middle;font-size:10.5px;font-family:monospace","setMapPosition()",[68,577,579],{"x":147,"y":578,"style":119,"fill":66,"textAnchor":74},"262","a coordinate, in a CRS",[122,581],{"x1":582,"y1":583,"x2":584,"y2":124,"stroke":585,"style":586},"186","206","286","#0f766e","stroke-width:2;stroke-dasharray:5 4;marker-end:url(#anaArrow)",[68,588,592],{"x":589,"y":590,"style":591,"fill":585,"textAnchor":74},"236","196","text-anchor:middle;font-size:10px;font-family:monospace","frameOffset",[68,594,595],{"x":589,"y":570,"style":119,"fill":585,"textAnchor":74},"in millimetres",[44,597],{"x":159,"y":598,"width":599,"height":600,"rx":601,"fill":83,"stroke":84,"style":85},"106","230","60","6",[68,603,605],{"x":604,"y":105,"style":131,"fill":66,"textAnchor":74},"405","Subsidence reported 2025-11",[68,607,609],{"x":604,"y":608,"style":119,"fill":100,"textAnchor":74},"154","a QTextDocument — HTML allowed",[122,611],{"x1":159,"y1":95,"x2":612,"y2":95,"stroke":613,"style":614},"520","#b45309","stroke-width:1.6;marker-end:url(#anaArrow)",[122,616],{"x1":612,"y1":95,"x2":159,"y2":95,"stroke":613,"style":614},[68,618,405],{"x":604,"y":619,"style":591,"fill":613,"textAnchor":74},"82",[44,621],{"x":622,"y":623,"width":624,"height":619,"rx":58,"fill":625,"stroke":613,"style":626},"556","96","184","#fdf2e2","stroke-width:2",[68,628,632],{"x":629,"y":630,"style":631,"fill":613,"textAnchor":74},"648","124","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","text too long?",[68,634,636],{"x":629,"y":635,"style":119,"fill":66,"textAnchor":74},"146","it is clipped,",[68,638,640],{"x":629,"y":639,"style":119,"fill":66,"textAnchor":74},"164","not wrapped",[163,642,644],{"id":643},"the-annotation-layer-for-anything-scripted","The annotation layer, for anything scripted",[14,646,647,648,651,652,655],{},"QGIS 3.16 added ",[184,649,650],{},"QgsAnnotationLayer",", which holds annotation ",[179,653,654],{},"items"," as ordinary map layer content. It is the better route for anything generated, because the items behave like layer content: they respect layer order, opacity, scale ranges and visibility.",[205,657,659],{"className":207,"code":658,"language":209,"meta":210,"style":210},"from qgis.core import (\n    QgsAnnotationLayer, QgsAnnotationPointTextItem, QgsProject,\n    QgsCoordinateTransformContext, QgsTextFormat, QgsPointXY,\n)\nfrom qgis.PyQt.QtGui import QColor\n\nlayer = QgsAnnotationLayer(\n    \"Site notes\", QgsAnnotationLayer.LayerOptions(QgsCoordinateTransformContext())\n)\nlayer.setCrs(QgsProject.instance().crs())\n\nfmt = QgsTextFormat()\nfmt.setSize(9)\nfmt.setColor(QColor(\"#17211d\"))\n\nitem = QgsAnnotationPointTextItem(\"Subsidence reported 2025-11\", QgsPointXY(432150, 189400))\nitem.setFormat(fmt)\nlayer.addItem(item)\n\nQgsProject.instance().addMapLayer(layer)\n",[184,660,661,672,677,682,686,697,701,711,719,723,728,732,742,752,762,766,790,796,802,807],{"__ignoreMap":210},[214,662,663,665,667,669],{"class":122,"line":216},[214,664,220],{"class":219},[214,666,224],{"class":223},[214,668,227],{"class":219},[214,670,671],{"class":223}," (\n",[214,673,674],{"class":122,"line":233},[214,675,676],{"class":223},"    QgsAnnotationLayer, QgsAnnotationPointTextItem, QgsProject,\n",[214,678,679],{"class":122,"line":246},[214,680,681],{"class":223},"    QgsCoordinateTransformContext, QgsTextFormat, QgsPointXY,\n",[214,683,684],{"class":122,"line":259},[214,685,311],{"class":223},[214,687,688,690,692,694],{"class":122,"line":266},[214,689,220],{"class":219},[214,691,238],{"class":223},[214,693,227],{"class":219},[214,695,696],{"class":223}," QColor\n",[214,698,699],{"class":122,"line":278},[214,700,263],{"emptyLinePlaceholder":262},[214,702,703,706,708],{"class":122,"line":289},[214,704,705],{"class":223},"layer ",[214,707,272],{"class":219},[214,709,710],{"class":223}," QgsAnnotationLayer(\n",[214,712,713,716],{"class":122,"line":295},[214,714,715],{"class":298},"    \"Site notes\"",[214,717,718],{"class":223},", QgsAnnotationLayer.LayerOptions(QgsCoordinateTransformContext())\n",[214,720,721],{"class":122,"line":302},[214,722,311],{"class":223},[214,724,725],{"class":122,"line":308},[214,726,727],{"class":223},"layer.setCrs(QgsProject.instance().crs())\n",[214,729,730],{"class":122,"line":314},[214,731,263],{"emptyLinePlaceholder":262},[214,733,734,737,739],{"class":122,"line":320},[214,735,736],{"class":223},"fmt ",[214,738,272],{"class":219},[214,740,741],{"class":223}," QgsTextFormat()\n",[214,743,744,747,750],{"class":122,"line":332},[214,745,746],{"class":223},"fmt.setSize(",[214,748,749],{"class":326},"9",[214,751,311],{"class":223},[214,753,754,757,760],{"class":122,"line":350},[214,755,756],{"class":223},"fmt.setColor(QColor(",[214,758,759],{"class":298},"\"#17211d\"",[214,761,347],{"class":223},[214,763,764],{"class":122,"line":366},[214,765,263],{"emptyLinePlaceholder":262},[214,767,768,771,773,776,779,782,784,786,788],{"class":122,"line":371},[214,769,770],{"class":223},"item ",[214,772,272],{"class":219},[214,774,775],{"class":223}," QgsAnnotationPointTextItem(",[214,777,778],{"class":298},"\"Subsidence reported 2025-11\"",[214,780,781],{"class":223},", QgsPointXY(",[214,783,484],{"class":326},[214,785,341],{"class":223},[214,787,489],{"class":326},[214,789,347],{"class":223},[214,791,793],{"class":122,"line":792},17,[214,794,795],{"class":223},"item.setFormat(fmt)\n",[214,797,799],{"class":122,"line":798},18,[214,800,801],{"class":223},"layer.addItem(item)\n",[214,803,805],{"class":122,"line":804},19,[214,806,263],{"emptyLinePlaceholder":262},[214,808,810],{"class":122,"line":809},20,[214,811,812],{"class":223},"QgsProject.instance().addMapLayer(layer)\n",[14,814,815,817,818,821,822,341,825,828,829,341,832,835,836,839],{},[174,816,379],{}," The layer takes a ",[184,819,820],{},"LayerOptions"," carrying a transform context, which is what lets its items reproject when the project CRS changes. Items come in several kinds — ",[184,823,824],{},"QgsAnnotationPointTextItem",[184,826,827],{},"QgsAnnotationLineTextItem"," for text along a line, ",[184,830,831],{},"QgsAnnotationMarkerItem",[184,833,834],{},"QgsAnnotationPolygonItem"," — and they all take the same ",[184,837,838],{},"QgsTextFormat"," or symbol objects used elsewhere in QGIS, so a house style applies to annotations too.",[14,841,842,843,847,848,852],{},"Because it is a layer, it can be hidden by a ",[21,844,846],{"href":845},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002F","map theme",", given a ",[21,849,851],{"href":850},"\u002Fpyqgis-cartography-visualization\u002Fmap-themes-and-layer-visibility\u002Fset-scale-based-visibility-pyqgis\u002F","scale range"," so notes appear only when zoomed in, and placed above or below other layers deliberately.",[163,854,856],{"id":855},"making-them-persist","Making them persist",[14,858,859],{},"Annotations added to the annotation manager are written into the project file. Items in an annotation layer are written into the project file too, as part of that layer's definition. Neither survives if the project is never saved, and neither is exported with the data.",[205,861,863],{"className":207,"code":862,"language":209,"meta":210,"style":210},"project = QgsProject.instance()\nproject.setDirty(True)\nproject.write()\n",[184,864,865,875,884],{"__ignoreMap":210},[214,866,867,870,872],{"class":122,"line":216},[214,868,869],{"class":223},"project ",[214,871,272],{"class":219},[214,873,874],{"class":223}," QgsProject.instance()\n",[214,876,877,880,882],{"class":122,"line":233},[214,878,879],{"class":223},"project.setDirty(",[214,881,474],{"class":326},[214,883,311],{"class":223},[214,885,886],{"class":122,"line":246},[214,887,888],{"class":223},"project.write()\n",[14,890,891,893,894,897,898,901],{},[174,892,379],{}," In a plugin, ",[184,895,896],{},"setDirty(True)"," alone is the polite move — it prompts the user rather than writing over their file. In a batch script, ",[184,899,900],{},"write()"," with no argument saves back to the loaded path. What neither does is put the note into a GeoPackage or a shapefile: annotations are project furniture, so a colleague who receives only the data receives none of them.",[163,903,905],{"id":904},"a-reusable-stamp-for-exports","A reusable stamp for exports",[14,907,908],{},"The recurring real-world use is a stamp — a status, a date, a reference number — that must appear on every output and must be easy to remove. Building it as a function with a recognisable marker makes both halves reliable.",[205,910,912],{"className":207,"code":911,"language":209,"meta":210,"style":210},"from qgis.core import QgsProject, QgsTextAnnotation\nfrom qgis.PyQt.QtGui import QTextDocument\nfrom qgis.PyQt.QtCore import QPointF, QSizeF\n\nMARKER = \"\\u200b\"          # a zero-width space, invisible but findable\n\n\ndef clear_stamps():\n    manager = QgsProject.instance().annotationManager()\n    for annotation in list(manager.annotations()):\n        document = annotation.document()\n        if document and MARKER in document.toPlainText():\n            manager.removeAnnotation(annotation)\n\n\ndef stamp(text, corner=(0.02, 0.95)):\n    clear_stamps()\n    annotation = QgsTextAnnotation()\n    document = QTextDocument()\n    document.setHtml(\n        f'\u003Cp style=\"font-family:sans-serif;font-size:9pt;color:#b91c1c\">'\n        f'{MARKER}{text}\u003C\u002Fp>'\n    )\n    annotation.setDocument(document)\n    annotation.setHasFixedMapPosition(False)\n    annotation.setRelativePosition(QPointF(*corner))\n    annotation.setFrameSizeMm(QSizeF(70, 10))\n    QgsProject.instance().annotationManager().addAnnotation(annotation)\n    return annotation\n",[184,913,914,925,936,946,950,971,975,979,991,1001,1018,1028,1048,1053,1057,1061,1086,1091,1100,1109,1114,1123,1142,1148,1154,1164,1176,1191,1197],{"__ignoreMap":210},[214,915,916,918,920,922],{"class":122,"line":216},[214,917,220],{"class":219},[214,919,224],{"class":223},[214,921,227],{"class":219},[214,923,924],{"class":223}," QgsProject, QgsTextAnnotation\n",[214,926,927,929,931,933],{"class":122,"line":233},[214,928,220],{"class":219},[214,930,238],{"class":223},[214,932,227],{"class":219},[214,934,935],{"class":223}," QTextDocument\n",[214,937,938,940,942,944],{"class":122,"line":246},[214,939,220],{"class":219},[214,941,251],{"class":223},[214,943,227],{"class":219},[214,945,256],{"class":223},[214,947,948],{"class":122,"line":259},[214,949,263],{"emptyLinePlaceholder":262},[214,951,952,955,958,961,964,967],{"class":122,"line":266},[214,953,954],{"class":326},"MARKER",[214,956,957],{"class":219}," =",[214,959,960],{"class":298}," \"",[214,962,963],{"class":326},"\\u200b",[214,965,966],{"class":298},"\"",[214,968,970],{"class":969},"sjoCn","          # a zero-width space, invisible but findable\n",[214,972,973],{"class":122,"line":278},[214,974,263],{"emptyLinePlaceholder":262},[214,976,977],{"class":122,"line":289},[214,978,263],{"emptyLinePlaceholder":262},[214,980,981,984,988],{"class":122,"line":295},[214,982,983],{"class":219},"def",[214,985,987],{"class":986},"svObZ"," clear_stamps",[214,989,990],{"class":223},"():\n",[214,992,993,996,998],{"class":122,"line":302},[214,994,995],{"class":223},"    manager ",[214,997,272],{"class":219},[214,999,1000],{"class":223}," QgsProject.instance().annotationManager()\n",[214,1002,1003,1006,1009,1012,1015],{"class":122,"line":308},[214,1004,1005],{"class":219},"    for",[214,1007,1008],{"class":223}," annotation ",[214,1010,1011],{"class":219},"in",[214,1013,1014],{"class":326}," list",[214,1016,1017],{"class":223},"(manager.annotations()):\n",[214,1019,1020,1023,1025],{"class":122,"line":314},[214,1021,1022],{"class":223},"        document ",[214,1024,272],{"class":219},[214,1026,1027],{"class":223}," annotation.document()\n",[214,1029,1030,1033,1036,1039,1042,1045],{"class":122,"line":320},[214,1031,1032],{"class":219},"        if",[214,1034,1035],{"class":223}," document ",[214,1037,1038],{"class":219},"and",[214,1040,1041],{"class":326}," MARKER",[214,1043,1044],{"class":219}," in",[214,1046,1047],{"class":223}," document.toPlainText():\n",[214,1049,1050],{"class":122,"line":332},[214,1051,1052],{"class":223},"            manager.removeAnnotation(annotation)\n",[214,1054,1055],{"class":122,"line":350},[214,1056,263],{"emptyLinePlaceholder":262},[214,1058,1059],{"class":122,"line":366},[214,1060,263],{"emptyLinePlaceholder":262},[214,1062,1063,1065,1068,1071,1073,1076,1078,1080,1083],{"class":122,"line":371},[214,1064,983],{"class":219},[214,1066,1067],{"class":986}," stamp",[214,1069,1070],{"class":223},"(text, corner",[214,1072,272],{"class":219},[214,1074,1075],{"class":223},"(",[214,1077,338],{"class":326},[214,1079,341],{"class":223},[214,1081,1082],{"class":326},"0.95",[214,1084,1085],{"class":223},")):\n",[214,1087,1088],{"class":122,"line":792},[214,1089,1090],{"class":223},"    clear_stamps()\n",[214,1092,1093,1096,1098],{"class":122,"line":798},[214,1094,1095],{"class":223},"    annotation ",[214,1097,272],{"class":219},[214,1099,275],{"class":223},[214,1101,1102,1105,1107],{"class":122,"line":804},[214,1103,1104],{"class":223},"    document ",[214,1106,272],{"class":219},[214,1108,286],{"class":223},[214,1110,1111],{"class":122,"line":809},[214,1112,1113],{"class":223},"    document.setHtml(\n",[214,1115,1117,1120],{"class":122,"line":1116},21,[214,1118,1119],{"class":219},"        f",[214,1121,1122],{"class":298},"'\u003Cp style=\"font-family:sans-serif;font-size:9pt;color:#b91c1c\">'\n",[214,1124,1126,1128,1131,1134,1136,1139],{"class":122,"line":1125},22,[214,1127,1119],{"class":219},[214,1129,1130],{"class":298},"'",[214,1132,1133],{"class":326},"{MARKER}{",[214,1135,68],{"class":223},[214,1137,1138],{"class":326},"}",[214,1140,1141],{"class":298},"\u003C\u002Fp>'\n",[214,1143,1145],{"class":122,"line":1144},23,[214,1146,1147],{"class":223},"    )\n",[214,1149,1151],{"class":122,"line":1150},24,[214,1152,1153],{"class":223},"    annotation.setDocument(document)\n",[214,1155,1157,1160,1162],{"class":122,"line":1156},25,[214,1158,1159],{"class":223},"    annotation.setHasFixedMapPosition(",[214,1161,327],{"class":326},[214,1163,311],{"class":223},[214,1165,1167,1170,1173],{"class":122,"line":1166},26,[214,1168,1169],{"class":223},"    annotation.setRelativePosition(QPointF(",[214,1171,1172],{"class":219},"*",[214,1174,1175],{"class":223},"corner))\n",[214,1177,1179,1182,1185,1187,1189],{"class":122,"line":1178},27,[214,1180,1181],{"class":223},"    annotation.setFrameSizeMm(QSizeF(",[214,1183,1184],{"class":326},"70",[214,1186,341],{"class":223},[214,1188,82],{"class":326},[214,1190,347],{"class":223},[214,1192,1194],{"class":122,"line":1193},28,[214,1195,1196],{"class":223},"    QgsProject.instance().annotationManager().addAnnotation(annotation)\n",[214,1198,1200,1203],{"class":122,"line":1199},29,[214,1201,1202],{"class":219},"    return",[214,1204,1205],{"class":223}," annotation\n",[14,1207,1208,1210,1211,1214,1215,1218,1219,1222],{},[174,1209,379],{}," A zero-width space is an unusual but effective marker — invisible to the reader, present in ",[184,1212,1213],{},"toPlainText()",", and vanishingly unlikely to appear in a note somebody wrote by hand. That is what makes ",[184,1216,1217],{},"clear_stamps()"," safe to run against a project full of other people's annotations. Calling it at the top of ",[184,1220,1221],{},"stamp()"," makes the whole function idempotent: run it five times with different text and you get one stamp, not five stacked on top of each other.",[14,1224,1225],{},"The pattern generalises to anything a script owns and a user does not. Where several scripts each want their own set, use a distinct marker string per script and keep them in one shared constants module, so nothing ever clears somebody else's notes.",[163,1227,1229],{"id":1228},"reading-and-clearing-what-is-there","Reading and clearing what is there",[14,1231,1232],{},"Scripts that add annotations should be able to remove their own.",[205,1234,1236],{"className":207,"code":1235,"language":209,"meta":210,"style":210},"manager = QgsProject.instance().annotationManager()\nprint(len(manager.annotations()))\n\nfor annotation in list(manager.annotations()):\n    document = annotation.document()\n    if document and \"Provisional\" in document.toPlainText():\n        manager.removeAnnotation(annotation)\n",[184,1237,1238,1247,1260,1264,1277,1285,1301],{"__ignoreMap":210},[214,1239,1240,1243,1245],{"class":122,"line":216},[214,1241,1242],{"class":223},"manager ",[214,1244,272],{"class":219},[214,1246,1000],{"class":223},[214,1248,1249,1252,1254,1257],{"class":122,"line":233},[214,1250,1251],{"class":326},"print",[214,1253,1075],{"class":223},[214,1255,1256],{"class":326},"len",[214,1258,1259],{"class":223},"(manager.annotations()))\n",[214,1261,1262],{"class":122,"line":246},[214,1263,263],{"emptyLinePlaceholder":262},[214,1265,1266,1269,1271,1273,1275],{"class":122,"line":259},[214,1267,1268],{"class":219},"for",[214,1270,1008],{"class":223},[214,1272,1011],{"class":219},[214,1274,1014],{"class":326},[214,1276,1017],{"class":223},[214,1278,1279,1281,1283],{"class":122,"line":266},[214,1280,1104],{"class":223},[214,1282,272],{"class":219},[214,1284,1027],{"class":223},[214,1286,1287,1290,1292,1294,1297,1299],{"class":122,"line":278},[214,1288,1289],{"class":219},"    if",[214,1291,1035],{"class":223},[214,1293,1038],{"class":219},[214,1295,1296],{"class":298}," \"Provisional\"",[214,1298,1044],{"class":219},[214,1300,1047],{"class":223},[214,1302,1303],{"class":122,"line":289},[214,1304,1305],{"class":223},"        manager.removeAnnotation(annotation)\n",[14,1307,1308,1310,1311,1314],{},[174,1309,379],{}," Iterating over a copy of the list is necessary because removal mutates the underlying collection. Matching on the text content is crude but effective, and it is what makes a \"stamp every export as provisional, then clear the stamps\" workflow safe to run repeatedly. For annotation-layer items the equivalent is ",[184,1312,1313],{},"layer.items()"," keyed by item id, which is cleaner because each item has a stable identifier rather than needing to be recognised by its contents.",[163,1316,1318],{"id":1317},"qgis-version-compatibility","QGIS version compatibility",[14,1320,1321,1322,1324],{},"The examples target ",[174,1323,176],{}," (Python 3.12).",[1326,1327,1328,1344],"table",{},[1329,1330,1331],"thead",{},[1332,1333,1334,1338,1341],"tr",{},[1335,1336,1337],"th",{},"QGIS version",[1335,1339,1340],{},"Python",[1335,1342,1343],{},"Notes",[1345,1346,1347,1361,1374,1384,1395],"tbody",{},[1332,1348,1349,1353,1356],{},[1350,1351,1352],"td",{},"3.16 LTR",[1350,1354,1355],{},"3.7",[1350,1357,1358,1360],{},[184,1359,650],{}," and the item classes introduced alongside the older annotation manager.",[1332,1362,1363,1366,1369],{},[1350,1364,1365],{},"3.22 LTR",[1350,1367,1368],{},"3.9",[1350,1370,1371,1373],{},[184,1372,827],{}," added for text following a line.",[1332,1375,1376,1379,1381],{},[1350,1377,1378],{},"3.28 LTR",[1350,1380,1368],{},[1350,1382,1383],{},"Annotation items gain per-item z-index ordering within a layer.",[1332,1385,1386,1389,1392],{},[1350,1387,1388],{},"3.34 LTR",[1350,1390,1391],{},"3.12",[1350,1393,1394],{},"Baseline for this page.",[1332,1396,1397,1400,1402],{},[1350,1398,1399],{},"3.40+",[1350,1401,1391],{},[1350,1403,1404],{},"Annotation items support callouts linking item to anchor.",[163,1406,1408],{"id":1407},"troubleshooting","Troubleshooting",[168,1410,1411,1419,1427,1433,1450,1456],{},[171,1412,1413,380,1416,1418],{},[174,1414,1415],{},"The note is cut off.",[184,1417,405],{}," is too small; annotations clip rather than wrap.",[171,1420,1421,380,1424,1426],{},[174,1422,1423],{},"The note moved when the project CRS changed.",[184,1425,541],{}," was never set, so the coordinate was never transformable.",[171,1428,1429,1432],{},[174,1430,1431],{},"The note disappeared after reopening."," The project was not saved. Annotations live in the project file only.",[171,1434,1435,1438,1439,1442,1443,1446,1447,1449],{},[174,1436,1437],{},"HTML styling has no effect."," The document was set with ",[184,1440,1441],{},"setPlainText()"," rather than ",[184,1444,1445],{},"setHtml()",", or the style is on an element ",[184,1448,395],{}," does not support.",[171,1451,1452,1455],{},[174,1453,1454],{},"Annotations do not appear in a layout."," They are canvas furniture; the layout has its own label and picture items. Duplicate the note as a layout item, or use an annotation layer, which does render in layouts.",[171,1457,1458,1461],{},[174,1459,1460],{},"The script keeps adding duplicates."," It never removes its previous run's annotations. Tag them recognisably and clear before adding.",[163,1463,1465],{"id":1464},"conclusion","Conclusion",[14,1467,1468,1469,1471,1472,1474,1475,1477],{},"Use ",[184,1470,383],{}," for a note that belongs to the page and ",[184,1473,474],{}," with an explicit CRS for one that belongs to a place. For anything generated by a script, prefer ",[184,1476,650],{}," — the items behave like layer content, render in layouts, and can be cleared by id rather than by guessing at their text.",[163,1479,1481],{"id":1480},"frequently-asked-questions","Frequently Asked Questions",[14,1483,1484,1487,1488,1492],{},[174,1485,1486],{},"What is the difference between an annotation and a label?","\nA label is generated from a feature's attributes by the labelling engine and moves with the data. An annotation is a fixed piece of content you placed. Use labelling for anything per-feature; see ",[21,1489,1491],{"href":1490},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-rule-based-labels-pyqgis\u002F","rule-based labels",".",[14,1494,1495,1498,1499,1503],{},[174,1496,1497],{},"Can annotations be exported to an image?","\nCanvas annotations appear in a ",[21,1500,1502],{"href":1501},"\u002Fpyqgis-cartography-visualization\u002Fmap-canvas-and-image-export\u002Fexport-map-canvas-to-image-pyqgis\u002F","canvas image export"," because they are drawn on the canvas. Annotation-layer items appear in both canvas and layout exports, which is one more reason to prefer them.",[14,1505,1506,1509],{},[174,1507,1508],{},"How do I attach a note to a specific feature?","\nAnnotation items are placed by coordinate, not by feature reference. Read the feature's geometry, take a representative point, and place the item there — regenerating the annotations when the data changes.",[14,1511,1512,1515,1516,1519,1520,1523,1524,1528],{},[174,1513,1514],{},"Can I style the frame?","\nYes — ",[184,1517,1518],{},"setFillSymbol()"," on the annotation controls the box, taking an ordinary ",[184,1521,1522],{},"QgsFillSymbol",", so it can carry a ",[21,1525,1527],{"href":1526},"\u002Fpyqgis-cartography-visualization\u002Fsymbol-layers-and-advanced-symbology\u002Fstack-symbol-layers-pyqgis\u002F","stacked symbol"," with a shadow.",[163,1530,1532],{"id":1531},"related","Related",[168,1534,1535,1540,1546,1551,1556],{},[171,1536,1537,1539],{},[21,1538,24],{"href":23}," — the guide this recipe belongs to",[171,1541,1542],{},[21,1543,1545],{"href":1544},"\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fcontrol-label-placement-and-collisions-pyqgis\u002F","Control Label Placement and Collisions in PyQGIS",[171,1547,1548],{},[21,1549,1550],{"href":1490},"Add Rule-Based Labels in PyQGIS",[171,1552,1553],{},[21,1554,1555],{"href":845},"Map Themes & Layer Visibility in PyQGIS",[171,1557,1558],{},[21,1559,1560],{"href":1501},"Export the Map Canvas to an Image in PyQGIS",[1562,1563,1564],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":210,"searchDepth":233,"depth":233,"links":1566},[1567,1568,1569,1570,1571,1572,1573,1574,1575,1576,1577,1578],{"id":165,"depth":233,"text":166},{"id":197,"depth":233,"text":198},{"id":409,"depth":233,"text":410},{"id":643,"depth":233,"text":644},{"id":855,"depth":233,"text":856},{"id":904,"depth":233,"text":905},{"id":1228,"depth":233,"text":1229},{"id":1317,"depth":233,"text":1318},{"id":1407,"depth":233,"text":1408},{"id":1464,"depth":233,"text":1465},{"id":1480,"depth":233,"text":1481},{"id":1531,"depth":233,"text":1532},"Place fixed notes on a QGIS map from Python — QgsTextAnnotation and the annotation layer, map-anchored versus screen-anchored placement, HTML content, and saving annotations with the project.","md",{"slug":1582,"type":1583,"breadcrumb":1584,"datePublished":1585,"dateModified":1585},"add-text-annotation-to-map-pyqgis","article","Text Annotations","2026-08-27","\u002Fpyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-text-annotation-to-map-pyqgis",{"title":5,"description":1579},"pyqgis-cartography-visualization\u002Flabeling-and-annotations\u002Fadd-text-annotation-to-map-pyqgis\u002Findex","jLvOirKKt-fcOvJUO18E4EOca1dK_TQIKaDq21QH3L8",1787823360558]