Add a Scale Bar and North Arrow to a Layout in PyQGIS

A map without a scale bar is a picture. Adding one from Python is straightforward, but two things catch people out: a scale bar that is not linked to a map item shows a meaningless default scale rather than the map's, and a north arrow is not a special item type at all — it is a picture item pointing at an SVG, which is why searching the API for QgsLayoutItemNorthArrow finds nothing.

This page is a focused recipe within Map Canvas Control and Image Export in PyQGIS, and it builds on the layout automation in Automated Map Layout Generation. It covers building a layout with a map item, adding and linking a scale bar, adding a rotation-aware north arrow, and positioning both reliably.

How the decorations attach to the map itemA layout page holds a map item occupying most of it. A scale bar sits in the lower left and a north arrow in the lower right. Dashed arrows from both point back at the map item, indicating that the scale bar reads its scale from the map and the north arrow reads its rotation, rather than either being independent of it.Both decorations read from the map item — link them or they lieA4 landscape pageQgsLayoutItemMap1 kmNscale barsetLinkedMap()north arrowa picture item

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • A project with at least one styled layer.
  • Familiarity with the layout API — Automated Map Layout Generation covers creating layouts and exporting them.

Build a layout with a map item

The scale bar needs something to link to, so the map item comes first.

from qgis.core import (
    QgsLayout,
    QgsLayoutItemMap,
    QgsLayoutPoint,
    QgsLayoutSize,
    QgsProject,
    QgsUnitTypes,
)

project = QgsProject.instance()
layer = project.mapLayersByName("parcels")[0]

layout = QgsLayout(project)
layout.initializeDefaults()
layout.setName("Parcels A4")

map_item = QgsLayoutItemMap(layout)
map_item.attemptMove(QgsLayoutPoint(10, 10, QgsUnitTypes.LayoutMillimeters))
map_item.attemptResize(QgsLayoutSize(190, 110, QgsUnitTypes.LayoutMillimeters))

extent = layer.extent()
extent.scale(1.1)
map_item.setExtent(extent)
map_item.setLayers([layer])
layout.addLayoutItem(map_item)

Breakdown: initializeDefaults() creates a single A4 page, without which the layout has no page at all and every item lands nowhere. attemptMove() and attemptResize() are the correct positioning calls — they respect item constraints and reference points, which plain coordinate assignment does not. Positions and sizes carry an explicit unit, so millimetres and page coordinates cannot be confused. setLayers() on the map item pins the content; without it the item follows the project's layer tree and changes whenever a user ticks a box.

setLinkedMap() is the whole trick. Everything else is presentation.

from qgis.core import QgsLayoutItemScaleBar, QgsLayoutPoint, QgsUnitTypes

bar = QgsLayoutItemScaleBar(layout)
bar.setStyle("Single Box")
bar.setLinkedMap(map_item)          # must come before applyDefaultSize()
bar.setUnits(QgsUnitTypes.DistanceKilometers)
bar.setUnitsPerSegment(1)
bar.setNumberOfSegments(4)
bar.setNumberOfSegmentsLeft(0)
bar.setUnitLabel("km")
bar.applyDefaultSize()
bar.attemptMove(QgsLayoutPoint(12, 124, QgsUnitTypes.LayoutMillimeters))
layout.addLayoutItem(bar)

Breakdown: setLinkedMap() connects the bar to the map item so it reads the real scale; a bar added without it shows a placeholder that has no relationship to the map and is worse than no bar at all. It must be called before applyDefaultSize(), which sizes the bar from the linked map's scale — the other order produces a bar of an arbitrary width. setNumberOfSegmentsLeft(0) removes the subdivided segment to the left of zero, which many cartographers prefer omitted. Style names are the same strings as the GUI dropdown: "Single Box", "Double Box", "Line Ticks Up", "Numeric".

For a map series where the scale varies per page, let the bar size itself rather than fixing its width:

bar.setSegmentSizeMode(QgsScaleBarSettings.SegmentSizeFitWidth)
bar.setMinimumBarWidth(30)
bar.setMaximumBarWidth(70)

Breakdown: SegmentSizeFitWidth tells the bar to choose a round segment distance that fits the width range you allow, rather than holding a fixed distance per segment and growing or shrinking on the page. That is what keeps an atlas readable when one page is at 1:5 000 and the next at 1:25 000 — see Automating Atlas Map Series.

Add a north arrow

There is no north-arrow class. A north arrow is a QgsLayoutItemPicture pointing at one of the SVGs QGIS ships, with its rotation synchronised to the map.

Why the arrow must follow the map rotationThree map frames at rotations of zero, thirty and negative forty-five degrees. In each, the north arrow is drawn rotated by the opposite amount so that it continues to point at true north on the page rather than staying fixed to the frame.setSyncWithMap() keeps the arrow honest when the map rotatesmap rotation 0°arrow at 0°map rotation 30°arrow at −30°map rotation −45°arrow at +45°an arrow fixed to the frame points north only by coincidence

from qgis.core import (
    QgsApplication,
    QgsLayoutItemPicture,
    QgsLayoutPoint,
    QgsLayoutSize,
    QgsUnitTypes,
)
from pathlib import Path

svg_root = Path(QgsApplication.pkgDataPath()) / "svg" / "arrows"
arrow_svg = svg_root / "NorthArrow_02.svg"

arrow = QgsLayoutItemPicture(layout)
arrow.setPicturePath(str(arrow_svg))
arrow.setLinkedMap(map_item)
arrow.setNorthMode(QgsLayoutItemPicture.GridNorth)
arrow.attemptResize(QgsLayoutSize(16, 16, QgsUnitTypes.LayoutMillimeters))
arrow.attemptMove(QgsLayoutPoint(182, 122, QgsUnitTypes.LayoutMillimeters))
layout.addLayoutItem(arrow)

Breakdown: QgsApplication.pkgDataPath() locates the QGIS installation's shared data so the SVG path works on any machine — hard-coding /usr/share/qgis/svg/... breaks the moment the script moves to Windows. setLinkedMap() plus setNorthMode() is what makes the arrow rotate to compensate when the map item is rotated; GridNorth follows the CRS grid, TrueNorth follows the meridian, and the two diverge noticeably at high latitudes. Without the link the arrow is decoration that happens to point up.

Register and export

A layout has to be added to the project's layout manager before it can be found by name or exported by the GUI.

from qgis.core import QgsLayoutExporter

manager = project.layoutManager()
existing = manager.layoutByName(layout.name())
if existing:
    manager.removeLayout(existing)
manager.addLayout(layout)

exporter = QgsLayoutExporter(layout)
settings = QgsLayoutExporter.PdfExportSettings()
settings.dpi = 300
result = exporter.exportToPdf("/data/exports/parcels.pdf", settings)
if result != QgsLayoutExporter.Success:
    raise RuntimeError(f"export failed with code {result}")

Breakdown: Removing an existing layout of the same name first makes the script re-runnable; adding a duplicate name leaves two layouts and the GUI shows both. exportToPdf() returns a status code rather than raising, so comparing it against Success is the only way to catch a locked output file. For multi-page output and atlas driving, see Exporting Multiple QGIS Layouts to PDF.

Pick a scale bar style that suits the map

The style is not decoration — each one communicates a different level of precision, and a mismatch undermines the map's credibility.

Four scale bar styles and what each impliesSingle box draws alternating filled and empty segments and reads as a general-purpose bar. Double box stacks two rows of alternating segments, implying finer subdivision. Line ticks up draws a plain line with upward ticks, which suits technical drawings. Numeric prints the scale as a ratio, which suits fixed-scale output where a graphic bar would be redundant.The style tells the reader how precise the map claims to beSingle Box4 kmgeneral purposethe safe defaultDouble Box4 kmimplies fine detaillarge-scale mapsLine Ticks Up4 kmtechnical drawingsminimal inkNumeric1 : 25 000fixed-scale output onlymeaningless if resized

The numeric style deserves a warning: a printed ratio is only true at the size the map was exported at. Resize the PDF, or view it on a screen at any zoom, and the stated scale becomes a lie — which is precisely why a graphic bar is the safer choice for anything that might be reproduced at a different size.

QGIS version compatibility

The examples target QGIS 3.34 LTR (Python 3.12).

QGIS versionPythonNotes
3.28 LTR3.9Identical API. Scale bar style names unchanged.
3.34 LTR3.12Baseline for this page.
3.40 / 3.443.12Qgis.DistanceUnit replaces QgsUnitTypes.DistanceKilometers; the old spelling still resolves.

QgsLayoutItemScaleBar, QgsLayoutItemPicture and the layout item positioning calls are unchanged across 3.x.

Troubleshooting

  • The scale bar shows a nonsensical distance. setLinkedMap() was not called, so the bar has no map to read a scale from.
  • The bar is a strange width. applyDefaultSize() ran before setLinkedMap(). Link first, then size.
  • The north arrow is missing. The SVG path is wrong. Build it from QgsApplication.pkgDataPath() rather than hard-coding an absolute path.
  • The arrow does not rotate with the map. setLinkedMap() and setNorthMode() were not both set on the picture item.
  • Items appear off the page. initializeDefaults() was skipped, so the layout has no page. Add it before positioning anything.
  • Two layouts with the same name appear. The script was re-run without removing the previous layout from the manager.

Conclusion

A scale bar and a north arrow are both linked items: the bar reads its scale from the map item and the arrow reads its rotation. Call setLinkedMap() on both — before applyDefaultSize() on the bar — build SVG paths from QgsApplication.pkgDataPath(), and remove any same-named layout before adding yours so the script stays re-runnable.

Frequently Asked Questions

Why does my scale bar show the wrong distance? It is not linked to a map item, so it has no scale to read and falls back to a default. Call bar.setLinkedMap(map_item) before doing anything else with it.

Is there a north arrow layout item? No. A north arrow is a QgsLayoutItemPicture pointing at one of the arrow SVGs QGIS ships. Linking it to the map and setting a north mode is what makes it rotate correctly.

What is the difference between grid north and true north? Grid north follows the CRS grid's vertical axis; true north follows the meridian towards the pole. They coincide along a projection's central meridian and diverge increasingly away from it, particularly at high latitudes.

How do I keep the scale bar readable across an atlas? Use SegmentSizeFitWidth with a minimum and maximum bar width. The bar then chooses a round segment distance that fits the allowed width instead of growing or shrinking as the page scale changes.

Should a small-scale map have a north arrow at all? Often not. On a continental or world map the meridians converge visibly, so a single arrow is only correct at one point on the page and misleading everywhere else. A graticule communicates orientation honestly at those scales; reserve the arrow for maps small enough that north is effectively constant across the frame.

Can I put the scale bar inside the map frame? Yes — layout items are positioned independently, so placing the bar's coordinates within the map item's rectangle overlays it. Add a semi-opaque background to the bar so it stays legible over whatever the map draws beneath it.

Can the scale bar units follow the project? Yes — leaving the units unset lets the bar adopt the linked map's units, which keeps a template correct when it is reused on data in a different CRS. Setting them explicitly is better when the output must always read in kilometres regardless of what the data uses.