Use Marker Line and Interval Symbols in PyQGIS

Many classic map line styles are not lines at all but markers placed along lines: the cross-ties of a railway, the dots of a footpath, the triangles of a cliff edge, the arrows of a one-way street, the symbols at the start and end of a pipe. QGIS builds them with two symbol layers. The marker line places any marker symbol at intervals, at vertices or at specific points along a line. The hashed line draws short line segments across the line at intervals — the ties of a railway, the ticks of a boundary. Stacked with ordinary line layers, they produce the full range of cartographic line styles.

This recipe belongs to Symbol Layers & Advanced Symbology. It places markers at intervals and at specific positions, offsets and rotates them, builds hashed lines, combines both into classic map symbols, and controls behaviour at line ends and corners.

Where markers can goPlacement options for marker lines along a single line: at a fixed interval, for example every 10 millimetres; on every vertex; on the first and last vertex only; on the central point of the line; and on every curve point. Interval markers can be offset along the line so the first one does not sit on the start.Interval, vertices, ends, centreintervalverticesfirst · centre · last

Prerequisites

  • QGIS 3.34 LTR or newer, or the QGIS 4 series.
  • A line layer: railways, paths, administrative boundaries, pipes.

Place markers at a fixed interval

The marker line symbol layer takes a rotation flag, an interval and a marker sub-symbol. Stacked over a thin line, it gives dotted or symbol-studded lines.

from qgis.core import (QgsProject, QgsLineSymbol, QgsMarkerLineSymbolLayer, QgsMarkerSymbol,
                       QgsSingleSymbolRenderer, QgsUnitTypes, Qgis)

paths = QgsProject.instance().mapLayersByName("footpaths")[0]

dots = QgsMarkerLineSymbolLayer(True, 3.0)           # rotate with line, every 3 mm
dots.setPlacements(Qgis.MarkerLinePlacement.Interval)
dots.setIntervalUnit(QgsUnitTypes.RenderMillimeters)
dots.setOffsetAlongLine(1.5)
dots.setSubSymbol(QgsMarkerSymbol.createSimple(
    {"name": "circle", "color": "#b45309", "size": "0.9", "outline_style": "no"}))

symbol = QgsLineSymbol()
symbol.changeSymbolLayer(0, dots)
paths.setRenderer(QgsSingleSymbolRenderer(symbol))
paths.triggerRepaint()

Breakdown: The interval is the distance between markers along the line, here 3 mm on the page, so the pattern looks the same at every scale. setOffsetAlongLine shifts the first marker away from the line's start by half the interval, which centres the pattern and avoids a marker sitting exactly on junctions. Replacing the default simple line with the marker line gives a pure dotted line; appending it to a symbol instead draws the dots on top of a solid line. setPlacements (QGIS 3.24+) accepts combinations of placements as flags; older code uses setPlacement.

Place markers at vertices, ends and the centre

Interval markers decorate; positional markers carry meaning — a valve at each end of a pipe, a station at each vertex of a transit line, a direction arrow at the centre of each segment.

from qgis.core import QgsSimpleLineSymbolLayer

pipes = QgsProject.instance().mapLayersByName("water_mains")[0]
base = QgsSimpleLineSymbolLayer.create({"line_color": "#2563eb", "line_width": "0.6"})

ends = QgsMarkerLineSymbolLayer(True, 0)
ends.setPlacements(Qgis.MarkerLinePlacement.FirstVertex | Qgis.MarkerLinePlacement.LastVertex)
ends.setSubSymbol(QgsMarkerSymbol.createSimple(
    {"name": "square", "color": "#fffdf7", "size": "1.6", "outline_color": "#2563eb", "outline_width": "0.3"}))

centre = QgsMarkerLineSymbolLayer(True, 0)
centre.setPlacements(Qgis.MarkerLinePlacement.CentralPoint)
centre.setSubSymbol(QgsMarkerSymbol.createSimple(
    {"name": "arrowhead", "color": "#2563eb", "size": "1.8"}))

pipe_symbol = QgsLineSymbol([base, ends, centre])
pipes.setRenderer(QgsSingleSymbolRenderer(pipe_symbol))
pipes.triggerRepaint()

Breakdown: Combining first- and last-vertex placements puts a marker at both ends of each feature — valves at pipe ends, here as small hollow squares drawn over the pipe. The central-point placement adds one direction arrow at the middle of each pipe, rotated to follow the line, showing flow direction without cluttering long pipes. Building the symbol from a list of layers makes the stacking order explicit: the line first, then end markers, then the arrow. The full stacking rules are in stacking symbol layers.

Offset and rotate markers

Many symbols sit beside the line rather than on it: the triangles of a cliff edge, the teeth of an embankment, the ticks on one side of a boundary. Offsets across the line and marker rotation produce them.

Markers beside the lineA line with triangle markers offset to one side and rotated to point away from the line, the classic cliff or embankment symbol. The offset across the line moves markers to the left or right of the digitizing direction; rotation with the line keeps them aligned on curves. Reversing the line flips the side.Offset across, rotate with the lineoffset 1 mm to the left, rotated with the line

cliffs = QgsProject.instance().mapLayersByName("cliff_edges")[0]
edge = QgsSimpleLineSymbolLayer.create({"line_color": "#2f3b35", "line_width": "0.35"})

teeth = QgsMarkerLineSymbolLayer(True, 2.5)
teeth.setPlacements(Qgis.MarkerLinePlacement.Interval)
teeth.setOffset(-0.9)                                  # negative = left of digitizing direction
teeth.setSubSymbol(QgsMarkerSymbol.createSimple(
    {"name": "triangle", "color": "#2f3b35", "size": "1.4", "outline_style": "no", "angle": "0"}))
cliffs.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol([edge, teeth])))
cliffs.triggerRepaint()

Breakdown: setOffset moves markers perpendicular to the line, negative to the left of the digitizing direction and positive to the right, so teeth always face the same side relative to the line — which means the data must be digitized consistently, conventionally with the cliff top on one side. With rotation enabled, the triangles turn with the line, staying perpendicular on curves. The marker's own angle adjusts which way the triangle points relative to the line. Lines digitized the wrong way can be flipped with native:reverselinedirection.

Draw hashed lines

Hashed lines draw short line segments across the line at intervals, the basis of railway ties, boundary ticks and fences.

from qgis.core import QgsHashedLineSymbolLayer

rail = QgsProject.instance().mapLayersByName("railways")[0]
rail_base = QgsSimpleLineSymbolLayer.create({"line_color": "#2f3b35", "line_width": "0.5"})

ties = QgsHashedLineSymbolLayer(True, 2.0)
ties.setPlacements(Qgis.MarkerLinePlacement.Interval)
ties.setHashLength(1.6)
ties.setHashAngle(90)
ties.setSubSymbol(QgsLineSymbol.createSimple({"color": "#2f3b35", "width": "0.3"}))

rail.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol([rail_base, ties])))
rail.triggerRepaint()

Breakdown: Each hash is a short line of setHashLength millimetres drawn at setHashAngle degrees to the line — 90 for perpendicular ties, other angles for diagonal hatching. The hash's own appearance comes from its line sub-symbol, so hashes can be dashed, coloured or thick. A thin main line plus perpendicular ties every 2 mm is the familiar railway symbol; widening the ties and adding a white line on top gives the "chequered" style used in some national cartography.

Drive markers from attributes

Marker lines can carry information, not just decoration: the marker at a pipe's start showing the type of fitting, the interval of fence posts reflecting fence type, the colour of boundary ticks showing the boundary's legal status. Data-defined properties on the marker sub-symbol and on the marker line itself make that possible.

from qgis.core import QgsProperty, QgsSymbolLayer

fence = QgsProject.instance().mapLayersByName("fences")[0]
posts = QgsMarkerLineSymbolLayer(True, 4.0)
posts.setPlacements(Qgis.MarkerLinePlacement.Interval)
posts.setDataDefinedProperty(
    QgsSymbolLayer.PropertyInterval,
    QgsProperty.fromExpression("CASE WHEN \"type\" = 'electric' THEN 6 ELSE 3 END"))
post_marker = QgsMarkerSymbol.createSimple({"name": "circle", "size": "0.8", "outline_style": "no"})
post_marker.symbolLayer(0).setDataDefinedProperty(
    QgsSymbolLayer.PropertyFillColor,
    QgsProperty.fromExpression("CASE WHEN \"condition\" = 'damaged' THEN '#b91c1c' ELSE '#2f3b35' END"))
posts.setSubSymbol(post_marker)

line = QgsSimpleLineSymbolLayer.create({"line_color": "#59645f", "line_width": "0.25"})
fence.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol([line, posts])))
fence.triggerRepaint()

Breakdown: The interval is data-defined from the fence type, so electric fences show widely spaced posts and others dense ones, without separate rules. The post marker's fill colour is data-defined from a condition field, so damaged fences stand out in red within the same symbol. Data-defined properties are evaluated per feature, which keeps one symbol definition serving many variants; for very different appearances, a rule-based renderer with separate symbols is clearer. The same technique drives marker size, rotation and offset.

Combine layers into classic symbols

Real cartographic symbols combine several layers. A national boundary, for example: a wide pale band, a dash-dot line on top, and small markers at the vertices where the boundary turns.

A layered boundary symbolA boundary symbol built from three layers: a wide translucent band underneath, a dash-dot line in the centre, and small crosses at vertices marking turning points. Each layer is a separate symbol layer in one line symbol, drawn in order from bottom to top.Band, line, vertex markersband · dash-dot line · vertex markers

boundary = QgsProject.instance().mapLayersByName("admin_boundaries")[0]
band = QgsSimpleLineSymbolLayer.create({"line_color": "185,28,28,60", "line_width": "2.4"})
dashdot = QgsSimpleLineSymbolLayer.create({"line_color": "#b91c1c", "line_width": "0.4",
                                           "use_custom_dash": "1", "customdash": "6;1.5;1;1.5"})
turns = QgsMarkerLineSymbolLayer(False, 0)
turns.setPlacements(Qgis.MarkerLinePlacement.InnerVertices)
turns.setSubSymbol(QgsMarkerSymbol.createSimple(
    {"name": "cross2", "color": "#b91c1c", "size": "1.2", "outline_color": "#b91c1c", "outline_width": "0.2"}))
boundary.setRenderer(QgsSingleSymbolRenderer(QgsLineSymbol([band, dashdot, turns])))
boundary.triggerRepaint()

Breakdown: The translucent band makes the boundary readable over busy basemaps without hiding them. The custom dash pattern — 6 mm dash, 1.5 mm gap, 1 mm dot, 1.5 mm gap — is the dash-dot convention for administrative boundaries. Inner-vertex placement marks turning points but not the line's ends, which usually meet other boundary segments. Save finished symbols to the style library so every project uses the same boundary style, as in using the style manager and symbol library.

QGIS version compatibility

QgsMarkerLineSymbolLayer and QgsHashedLineSymbolLayer are available on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. setPlacements with combinable Qgis.MarkerLinePlacement flags replaced the single-value setPlacement in 3.24; inner-vertex and segment-centre placements arrived in the same period. On QGIS 4, units use Qgis.RenderUnit.Millimeters.

Troubleshooting

  • Markers face the wrong side. The line is digitized in the other direction; reverse it or flip the offset sign.
  • Markers pile up at sharp corners. Interval placement follows the line exactly; increase the interval or simplify the geometry.
  • The pattern does not scale with zoom. Units are map units; use millimetres for a constant look.
  • No markers on short features. The interval is longer than the line; add a central-point placement.

Conclusion

Use marker lines for markers at intervals, vertices, ends or the centre, offset them across the line and rotate them with it for side-specific symbols, use hashed lines for ties and ticks, combine bands, dashed lines and markers into classic symbols, keep units in millimetres, and digitize lines consistently so side and direction are right.

Frequently Asked Questions

Can markers be spaced in map units? Yes — set the interval unit to map units, for example a marker every 100 m along a pipe.

Can the interval be data-defined? Yes, through the marker line's data-defined interval property.

How do I avoid markers on hidden line parts? Use the symbol's clip or draw-only-on-visible-part options on recent releases.

Can I use an SVG as the marker? Yes — any marker symbol, including SVG markers, works as the sub-symbol.