Clip a Raster by a Mask Layer in PyQGIS
Clipping a raster to a polygon boundary is how you restrict a DEM, satellite scene, or land-cover grid to a study area, a watershed, or an administrative unit. Unlike clipping a vector layer, a raster clip has to decide what happens to the pixels outside the mask: they become nodata, and the output is usually cropped to the mask's bounding box to avoid carrying a sea of empty cells. In PyQGIS the workhorse is gdal:cliprasterbymasklayer. This is a fundamental step in Raster Analysis Workflows.
This page shows a correct clip that preserves the source resolution and CRS, how to set the nodata value, how cropping to extent works, and when to reach for the native: equivalent instead.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) with Processing available.
- A raster to clip (single- or multi-band GeoTIFF).
- A polygon vector layer to use as the mask.
- The QGIS Python Console (
Plugins > Python Console).
The raster and the mask should share a CRS. gdal:cliprasterbymasklayer can reproject the mask on the fly, but matching them up front avoids surprises at the boundary.
Run a Basic Raster Clip
Provide the raster as INPUT and the polygon layer as MASK. The example crops to the mask extent and writes a GeoTIFF.
import processing
result = processing.run("gdal:cliprasterbymasklayer", {
"INPUT": "/data/dem.tif",
"MASK": "/data/watershed.gpkg|layername=basin",
"CROP_TO_CUTLINE": True,
"KEEP_RESOLUTION": True,
"NODATA": -9999,
"OUTPUT": "/data/output/dem_clipped.tif",
})
print("Clipped raster:", result["OUTPUT"])
Breakdown: INPUT is the raster and MASK is the polygon layer (the |layername= suffix targets one layer inside a GeoPackage). CROP_TO_CUTLINE=True trims the output to the mask's bounding box; KEEP_RESOLUTION=True forces the output pixel size to match the source so cells stay aligned; NODATA=-9999 is the value assigned to pixels outside the polygon. The output path's .tif extension selects the GeoTIFF driver.
Set Nodata and Crop Behavior
Two parameters control the shape and fill of the result, and getting them right matters for downstream statistics:
NODATA— the value written to cells outside the mask polygon. Choose a value that cannot occur in your real data (for elevation,-9999; for an 8-bit byte raster,0or255if those are unused). If your source already defines a nodata value, reuse it for consistency.CROP_TO_CUTLINE— whenTrue, the output extent shrinks to the mask's bounding box. WhenFalse, the output keeps the input raster's full extent and merely sets outside-the-polygon pixels to nodata, leaving a large mostly-empty grid.
import processing
# Keep the full input extent, only mask out the values
processing.run("gdal:cliprasterbymasklayer", {
"INPUT": "/data/landcover.tif",
"MASK": "/data/county.gpkg",
"CROP_TO_CUTLINE": False,
"KEEP_RESOLUTION": True,
"NODATA": 0,
"OUTPUT": "/data/output/landcover_masked.tif",
})
Breakdown: With CROP_TO_CUTLINE=False the file footprint matches the original raster, which is useful when several masked outputs must stay on the same grid for overlay or differencing. Set NODATA to a code that is genuinely absent from the land-cover legend so masked cells are unambiguous. The masked output is then ready for accurate summaries with Calculate Raster Statistics in PyQGIS.
Preserve Resolution and CRS
KEEP_RESOLUTION=True is the safeguard against silent resampling. Without it, GDAL may snap the output to a slightly different grid, shifting pixel centers and corrupting later raster math. Pairing it with an explicit SOURCE_CRS/TARGET_CRS keeps the projection intact:
import processing
from qgis.core import QgsRasterLayer
raster = QgsRasterLayer("/data/dem.tif", "dem")
src_crs = raster.crs().authid()
processing.run("gdal:cliprasterbymasklayer", {
"INPUT": "/data/dem.tif",
"MASK": "/data/watershed.gpkg|layername=basin",
"SOURCE_CRS": src_crs,
"TARGET_CRS": src_crs, # no reprojection — keep the source CRS
"CROP_TO_CUTLINE": True,
"KEEP_RESOLUTION": True,
"NODATA": -9999,
"OUTPUT": "/data/output/dem_clipped.tif",
})
Breakdown: Reading raster.crs().authid() gives the source CRS as an authority string (e.g. EPSG:32633). Passing the same value to both SOURCE_CRS and TARGET_CRS explicitly tells GDAL to clip without reprojecting, so resolution and alignment survive. To actually reproject during the clip, set a different TARGET_CRS — but for separate reprojection of many files see Batch Reprojecting Raster Datasets in PyQGIS.
The native Equivalent
QGIS also ships native:cliprasterbymasklayer, a thin wrapper exposing similar options without invoking the GDAL provider directly. It is convenient when you want a TEMPORARY_OUTPUT layer object for chaining rather than a file on disk:
import processing
clipped = processing.run("native:cliprasterbymasklayer", {
"INPUT": "/data/dem.tif",
"MASK": "/data/watershed.gpkg|layername=basin",
"SOURCE_CRS": None,
"TARGET_CRS": None,
"TARGET_EXTENT": None,
"NODATA": -9999,
"OUTPUT": "TEMPORARY_OUTPUT",
})["OUTPUT"]
print("In-memory clip:", clipped.width(), "x", clipped.height(), "px")
Breakdown: The native: variant returns a QgsRasterLayer when given TEMPORARY_OUTPUT, ideal for feeding the next step in a script without writing intermediate files. The GDAL variant (gdal:cliprasterbymasklayer) exposes the full set of GDAL warp options like KEEP_RESOLUTION and creation OPTIONS, so prefer it for final outputs where compression and tiling matter.
Compress and Tile the Output
For deliverables, an uncompressed clipped GeoTIFF can be many times larger than necessary. Pass GDAL creation options through the OPTIONS parameter to compress and tile the result in the same step:
import processing
processing.run("gdal:cliprasterbymasklayer", {
"INPUT": "/data/dem.tif",
"MASK": "/data/watershed.gpkg|layername=basin",
"CROP_TO_CUTLINE": True,
"KEEP_RESOLUTION": True,
"NODATA": -9999,
"OPTIONS": "COMPRESS=DEFLATE|TILED=YES|BIGTIFF=IF_SAFER",
"OUTPUT": "/data/output/dem_clipped.tif",
})
Breakdown: OPTIONS takes pipe-separated GDAL creation flags. COMPRESS=DEFLATE shrinks the file losslessly, TILED=YES stores the raster in internal tiles for faster windowed reads, and BIGTIFF=IF_SAFER automatically switches to the BigTIFF format if the output would exceed the 4 GB classic-TIFF limit. These travel through to GDAL untouched.
Clip Many Rasters to One Mask
When a whole folder of tiles or scenes must be clipped to the same boundary, loop over them and reuse the mask. Wrapping each call in a try/except keeps a single failure from stopping the batch:
from pathlib import Path
import processing
source_dir = Path("/data/scenes")
output_dir = Path("/data/clipped")
output_dir.mkdir(parents=True, exist_ok=True)
mask = "/data/watershed.gpkg|layername=basin"
for src in sorted(source_dir.glob("*.tif")):
out_path = output_dir / f"{src.stem}_clip.tif"
try:
processing.run("gdal:cliprasterbymasklayer", {
"INPUT": str(src),
"MASK": mask,
"CROP_TO_CUTLINE": True,
"KEEP_RESOLUTION": True,
"NODATA": -9999,
"OUTPUT": str(out_path),
})
print(f"Clipped {src.name}")
except Exception as exc:
print(f"Failed {src.name}: {exc}")
Breakdown: Path.glob("*.tif") collects the rasters, the mask path is reused for every input, and per-file error handling logs problems while the loop continues. This is the raster analog of the vector batch pattern and slots directly into a headless pipeline once Processing is initialized.
QGIS Version Compatibility
The code targets QGIS 3.34 LTR (Python 3.12).
| QGIS version | Python | Notes |
|---|---|---|
| 3.28 LTR | 3.9 | gdal:cliprasterbymasklayer identical; native: variant present. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40 / 3.44 | 3.12 | Same algorithm IDs and parameters; newer GDAL improves nodata handling. |
Both algorithm IDs and their CROP_TO_CUTLINE, KEEP_RESOLUTION, and NODATA parameters are stable across the current 3.x line. The exact GDAL version bundled differs per release, which can affect edge-pixel handling on very large rasters, but the PyQGIS interface is unchanged.
Troubleshooting
- Output is all nodata. The mask does not overlap the raster, or their CRS differ so the polygon lands outside the raster footprint. Confirm both extents intersect in a common CRS.
- Pixels look shifted after clipping.
KEEP_RESOLUTIONwasFalse, letting GDAL resample to a new grid. Set it toTrueto preserve the source cell size and alignment. - Output extent is huge and mostly empty.
CROP_TO_CUTLINEisFalse. Set itTrueto trim the output to the mask's bounding box. - Statistics include the fill value. The clip's
NODATAwas not recognized downstream. Verify withgdalinfothat the output declares the nodata value, and reuse the source's existing nodata when possible. Mask layer geometry invalid. Self-intersecting mask polygons can break the cutline. Runnative:fixgeometrieson the mask first.
Conclusion
Clipping a raster by a mask in PyQGIS is reliable once three settings are deliberate: a NODATA value that cannot collide with real data, CROP_TO_CUTLINE to control whether the output is trimmed or kept full-extent, and KEEP_RESOLUTION to stop GDAL from silently resampling. Use gdal:cliprasterbymasklayer for final, compressed file outputs and the native: variant when you want an in-memory layer to chain. Keeping the raster and mask in the same CRS ties it all together and produces clean, analysis-ready clips.
Frequently Asked Questions
What value should I use for NODATA?
Pick a number that cannot appear in your real data — -9999 for elevation, or an unused legend code for categorical rasters. If the source already declares a nodata value, reuse it so masked cells stay consistent through the pipeline.
What is the difference between CROP_TO_CUTLINE True and False?True shrinks the output to the mask's bounding box, dropping the empty surround. False keeps the input raster's full extent and only sets outside-the-polygon pixels to nodata, leaving a larger, mostly-empty grid useful for keeping multiple outputs on one grid.
How do I keep the original resolution?
Set KEEP_RESOLUTION to True. Otherwise GDAL may snap the output to a slightly different grid, shifting pixel centers and breaking later raster algebra.
When should I use the native variant instead of the GDAL one?
Use native:cliprasterbymasklayer with TEMPORARY_OUTPUT when you want an in-memory layer to feed straight into the next algorithm. Use gdal:cliprasterbymasklayer for final files where you need GDAL options like compression, tiling, and explicit resolution control.