Apply a Color Ramp to a Raster in PyQGIS
Single-band rasters — elevation models, NDVI grids, temperature surfaces — carry one continuous value per pixel, and a colour ramp is what turns those numbers into a readable map. In PyQGIS this means assembling three classes: a QgsColorRampShader that maps values to colours, a QgsRasterShader that wraps it, and a QgsSingleBandPseudoColorRenderer that draws the band. This page is a focused recipe inside the Programmatic Layer Styling in PyQGIS cluster; where that cluster styles vectors, this one handles the raster path, including how to classify by the band's real data range with QgsRasterBandStats.
Prerequisites
- QGIS 3.34 LTR (Python 3.12), in the Python Console or a standalone application.
- A loaded, valid single-band
QgsRasterLayer(for example a DEM GeoTIFF). - Imports from
qgis.corefor the shader and renderer classes, andQColorfromqgis.PyQt.QtGui. - The band number you want to style (band indexing is 1-based in the raster API).
The three-class pipeline at a glance
Unlike vector styling, where you mutate a symbol that already exists, raster pseudo-color rendering is built from scratch each time out of three cooperating objects. The QgsColorRampShader is the brain: it holds the value-to-colour mapping and the classification logic. The QgsRasterShader is a thin adapter that lets the renderer call the shader as a function. The QgsSingleBandPseudoColorRenderer is the painter: it knows which band to read and hands each pixel value to the shader to look up a colour. You always build them inner-to-outer — shader, then raster shader, then renderer — and the recipes below follow that order.
Recipe 1: Read the band's value range
A ramp is only meaningful when stretched across the data's actual range. Pull the minimum and maximum from QgsRasterBandStats rather than hard-coding them.
from qgis.core import QgsProject, QgsRasterBandStats
raster = QgsProject.instance().mapLayersByName("dem")[0]
provider = raster.dataProvider()
band = 1
stats = provider.bandStatistics(band, QgsRasterBandStats.All)
v_min = stats.minimumValue
v_max = stats.maximumValue
print(f"Band {band} range: {v_min:.2f} to {v_max:.2f}")
Breakdown: dataProvider().bandStatistics() computes statistics for one band; passing QgsRasterBandStats.All requests the full set, exposing minimumValue, maximumValue, mean, and stdDev. The first argument is the 1-based band number. On very large rasters QGIS samples rather than reading every pixel, so the range is representative, not exhaustive. These bounds feed the ramp classification below. If you need the full statistical picture for analysis rather than styling, see Calculate Raster Statistics in PyQGIS.
Recipe 2: Build a color ramp shader from a gradient
The QgsColorRampShader holds a list of ColorRampItem entries — value/colour pairs. The simplest classification spreads N evenly spaced stops across the min–max range.
from qgis.core import (
QgsColorRampShader, QgsGradientColorRamp,
)
from qgis.PyQt.QtGui import QColor
# A two-stop gradient: dark teal (low) to amber (high).
ramp = QgsGradientColorRamp(QColor("#0f766e"), QColor("#b45309"))
shader = QgsColorRampShader(v_min, v_max)
shader.setColorRampType(QgsColorRampShader.Interpolated)
shader.setSourceColorRamp(ramp)
# Generate N evenly spaced classified items across the range.
shader.classifyColorRamp(classes=5, band=band, input=provider.input())
Breakdown: QgsGradientColorRamp(c1, c2) defines a two-colour gradient; you can add intermediate stops with the stops argument if needed. QgsColorRampShader(v_min, v_max) anchors the shader to the data range. setColorRampType(Interpolated) blends colours smoothly between stops — use Discrete for hard class boundaries or Exact for categorical rasters. classifyColorRamp() populates the item list automatically; passing band and input lets it align to the actual band. The colours here come straight from this site's palette to keep the example concrete.
Recipe 3: Use a named ramp from the default style
QGIS ships dozens of named ramps (Viridis, Spectral, RdYlGn). Pull one from QgsStyle.defaultStyle() instead of hand-building a gradient.
from qgis.core import QgsStyle, QgsColorRampShader
style = QgsStyle.defaultStyle()
print(style.colorRampNames()) # inspect what is available
ramp = style.colorRamp("Viridis")
shader = QgsColorRampShader(v_min, v_max)
shader.setColorRampType(QgsColorRampShader.Interpolated)
shader.setSourceColorRamp(ramp)
shader.classifyColorRamp(classes=7, band=band, input=provider.input())
Breakdown: QgsStyle.defaultStyle() is the application-wide style database; colorRampNames() lists every installed ramp so you can confirm a name before requesting it. colorRamp("Viridis") returns a ready-made QgsColorRamp clone you can hand straight to setSourceColorRamp(). Viridis is perceptually uniform and colour-blind friendly, which is why it is a good default for continuous data. Everything downstream is identical to the hand-built gradient case.
Recipe 4: Assemble the renderer and apply it
The shader goes inside a QgsRasterShader, which goes inside a QgsSingleBandPseudoColorRenderer, which becomes the layer's renderer.
from qgis.core import QgsRasterShader, QgsSingleBandPseudoColorRenderer
raster_shader = QgsRasterShader()
raster_shader.setRasterShaderFunction(shader)
renderer = QgsSingleBandPseudoColorRenderer(
provider, band, raster_shader
)
raster.setRenderer(renderer)
raster.triggerRepaint()
Breakdown: QgsRasterShader is a thin wrapper; setRasterShaderFunction() plugs in the colour-ramp shader you built. QgsSingleBandPseudoColorRenderer(provider, band, raster_shader) ties the renderer to the data provider and the 1-based band number. setRenderer() replaces the layer's current renderer, and triggerRepaint() redraws the canvas. If the GUI is open, also call iface.layerTreeView().refreshLayerSymbology(raster.id()) so the legend shows the new ramp.
Recipe 5: Classify by quantile instead of linear min/max
Linear min–max stretching washes out when a few outlier pixels stretch the range. Quantile (equal-count) classification distributes class breaks so each holds a similar number of pixels, revealing structure in skewed data.
from qgis.core import (
QgsColorRampShader, QgsStyle, QgsRasterShader,
QgsSingleBandPseudoColorRenderer,
)
ramp = QgsStyle.defaultStyle().colorRamp("Spectral")
shader = QgsColorRampShader(v_min, v_max)
shader.setColorRampType(QgsColorRampShader.Interpolated)
shader.setSourceColorRamp(ramp)
shader.setClassificationMode(QgsColorRampShader.Quantile)
shader.classifyColorRamp(classes=6, band=band, input=provider.input())
raster_shader = QgsRasterShader()
raster_shader.setRasterShaderFunction(shader)
raster.setRenderer(
QgsSingleBandPseudoColorRenderer(provider, band, raster_shader)
)
raster.triggerRepaint()
Breakdown: setClassificationMode(QgsColorRampShader.Quantile) switches break placement from Continuous (evenly spaced) to equal-count, so classifyColorRamp() computes breaks from the band's value distribution. This is the raster analogue of the natural-breaks and quantile choices you make for vectors in Graduated & Categorized Renderers. Quantile is the right call for elevation, population density, or any heavy-tailed surface where a linear stretch would crush most of the contrast into one band of colour.
Recipe 6: Discrete classes with explicit breaks
When the audience needs to read distinct categories off the map — elevation bands, risk tiers — a discrete shader with hand-set breaks beats a smooth gradient. You build the ColorRampItem list yourself, controlling both the break value and the legend label.
from qgis.core import (
QgsColorRampShader, QgsRasterShader,
QgsSingleBandPseudoColorRenderer,
)
from qgis.PyQt.QtGui import QColor
items = [
QgsColorRampShader.ColorRampItem(200, QColor("#22c55e"), "0-200 m"),
QgsColorRampShader.ColorRampItem(500, QColor("#b45309"), "200-500 m"),
QgsColorRampShader.ColorRampItem(1000, QColor("#7c2d12"), "500-1000 m"),
QgsColorRampShader.ColorRampItem(3000, QColor("#f6f3ea"), ">1000 m"),
]
shader = QgsColorRampShader()
shader.setColorRampType(QgsColorRampShader.Discrete)
shader.setColorRampItemList(items)
raster_shader = QgsRasterShader()
raster_shader.setRasterShaderFunction(shader)
raster.setRenderer(
QgsSingleBandPseudoColorRenderer(provider, band, raster_shader)
)
raster.triggerRepaint()
Breakdown: Each ColorRampItem(value, color, label) defines the upper bound of a class in Discrete mode, plus the legend text. setColorRampItemList() installs the whole list at once, bypassing classifyColorRamp() entirely since you are specifying breaks manually. Discrete mode paints a flat colour up to each value, producing a stepped legend that maps cleanly to named bands — ideal when "is this above 500 m?" is the question the map must answer.
Recipe 7: Reuse the styling as a wrapped function
For automation you want the whole pipeline behind one call so it can run over a folder of rasters. The function below classifies and applies a named ramp to any single-band layer.
from qgis.core import (
QgsRasterBandStats, QgsColorRampShader, QgsStyle,
QgsRasterShader, QgsSingleBandPseudoColorRenderer,
)
def apply_named_ramp(raster, ramp_name="Viridis", classes=7, band=1):
"""Classify a single-band raster with a named color ramp."""
provider = raster.dataProvider()
stats = provider.bandStatistics(band, QgsRasterBandStats.All)
ramp = QgsStyle.defaultStyle().colorRamp(ramp_name)
if ramp is None:
raise ValueError(f"Unknown color ramp: {ramp_name}")
shader = QgsColorRampShader(stats.minimumValue, stats.maximumValue)
shader.setColorRampType(QgsColorRampShader.Interpolated)
shader.setSourceColorRamp(ramp)
shader.classifyColorRamp(classes=classes, band=band, input=provider.input())
raster_shader = QgsRasterShader()
raster_shader.setRasterShaderFunction(shader)
raster.setRenderer(
QgsSingleBandPseudoColorRenderer(provider, band, raster_shader)
)
raster.triggerRepaint()
return raster
Breakdown: The function folds Recipes 1 through 4 into one reusable call, reading the band range fresh each time so it adapts to whatever raster it receives. The None guard turns a typo'd ramp name into a clear exception rather than a silent failure. Returning the layer lets you chain or log the result. Drop this into a loop over QgsRasterLayer instances to restyle an entire directory of grids consistently — the styling counterpart to the batch analysis patterns in the raster workflows cluster.
QGIS Version Compatibility
All snippets target QGIS 3.34 LTR with Python 3.12. The pseudo-color rendering classes have been stable across the 3.x line.
| QGIS version | Python | Notes |
|---|---|---|
| 3.28 LTR | 3.9 | All classes present; classifyColorRamp() signature identical. |
| 3.34 LTR | 3.12 | Reference version; run as written. |
| 3.40 / 3.44 | 3.12 | QgsColorRampShader.Interpolated / .Quantile still resolve; scoped QgsColorRampShader.Type.Interpolated is the newer spelling. |
QgsRasterBandStats.All and the 1-based band convention apply across all three versions, so a script written for 3.34 runs unchanged on 3.28 and 3.40+.
Troubleshooting
The raster renders all one color or all black.
The shader's v_min/v_max do not match the data. Confirm you read them from bandStatistics() for the correct 1-based band, and that you passed them to QgsColorRampShader(v_min, v_max).
classifyColorRamp() produced no items.
You likely called it before setSourceColorRamp(), so there was no ramp to sample. Set the source ramp first, then classify.
colorRamp("Viridis") returned None.
The name is wrong or that ramp is not installed. Print QgsStyle.defaultStyle().colorRampNames() and copy an exact name; ramp names are case-sensitive.
The legend did not update though the canvas did.triggerRepaint() only refreshes the canvas. Add iface.layerTreeView().refreshLayerSymbology(raster.id()) when the GUI is open.
NoData pixels show a solid colour.
Set a NoData value on the provider or call renderer.setNodataColor(QColor(0, 0, 0, 0)) to render them transparent rather than mapping them onto the ramp.
Conclusion
Applying a colour ramp to a raster is a fixed three-object pipeline: a QgsColorRampShader mapping values to colours, wrapped in a QgsRasterShader, driven by a QgsSingleBandPseudoColorRenderer. The decisions that matter are where the ramp comes from (a hand-built QgsGradientColorRamp or a named ramp from QgsStyle.defaultStyle()) and how breaks are placed (linear min–max for evenly distributed data, quantile for skewed surfaces). Anchor the shader to the band's real range via QgsRasterBandStats, classify, set the renderer, and repaint.
Frequently Asked Questions
How do I find the value range of my raster band?
Call provider.bandStatistics(band, QgsRasterBandStats.All) and read minimumValue and maximumValue. Band numbers are 1-based, and QGIS may sample large rasters rather than reading every pixel.
Where do the named color ramps like Viridis come from?
From QgsStyle.defaultStyle(), the application's style database. List them with colorRampNames() and fetch one with colorRamp("Viridis"). Names are case-sensitive.
What is the difference between Interpolated, Discrete, and Exact shader types?Interpolated blends colours smoothly between stops for continuous data. Discrete applies a flat colour up to each break for stepped legends. Exact matches specific values, which suits categorical rasters such as land-cover codes.
When should I use quantile instead of linear classification? Use quantile when the data is skewed or has outliers — elevation, density, rainfall — so each class holds a similar pixel count and contrast is preserved. Linear min–max suits roughly uniform value distributions, while a discrete shader with hand-set breaks is best when the map must communicate named bands rather than a smooth surface.