Create a Memory Layer in PyQGIS
A memory layer is a vector layer whose features live only in RAM. Nothing is written to disk, nothing needs a path, and creating one takes a single line. That makes it the natural scratch surface for a script: intermediate results between Processing steps, points generated from an API response, a selection you want to style separately, or the output of a quick experiment in the Python console.
This recipe belongs to Features, Geometries & Memory Layers. It shows how to declare a memory layer completely in its URI, add fields and features efficiently, copy the schema of an existing layer, and turn the temporary layer into a permanent file before the work is lost.
Prerequisites
- QGIS 3.34 LTR, 3.40 LTR or the QGIS 4 series. The code runs unchanged in the Python console, a script in the Python editor, or a standalone script with an initialised
QgsApplication. - Basic familiarity with
QgsVectorLayerandQgsFeature. If those are new, read adding features to a vector layer alongside this page.
Declare the layer in its URI
The memory provider takes everything it needs from a URI string. The part before the question mark is the geometry type; the query parameters set the CRS, the fields and, optionally, a spatial index. Declaring all of it up front means the layer is complete the moment it exists.
from qgis.core import QgsVectorLayer, QgsProject
uri = (
"Point?crs=EPSG:25832"
"&field=id:integer"
"&field=name:string(80)"
"&field=height_m:double(10,2)"
"&field=surveyed:date"
"&index=yes"
)
masts = QgsVectorLayer(uri, "masts", "memory")
if not masts.isValid():
raise RuntimeError("memory layer could not be created")
print(masts.wkbType(), masts.crs().authid(), masts.fields().names())
QgsProject.instance().addMapLayer(masts)
Breakdown: The geometry part accepts Point, LineString, Polygon, their Multi forms, the Z, M and ZM variants (for example PointZ or MultiPolygonZM), and None for an attribute-only table. Field definitions are name:type(length,precision), with types integer, int8 (64-bit), double, string, date, time, datetime, bool and, on recent releases, binary and list types such as stringlist. index=yes builds a spatial index the provider keeps up to date as you add features, which makes later setFilterRect requests fast. Always pass crs= explicitly: a memory layer without it has an invalid CRS and will be placed wherever the project guesses.
Add fields and features in bulk
Fields can also be added after creation through the data provider, which is convenient when the schema is computed rather than known in advance. Features should then be added in one call rather than in a loop of single additions.
from qgis.core import QgsField, QgsFeature, QgsGeometry, QgsPointXY
from qgis.PyQt.QtCore import QVariant, QDate
provider = masts.dataProvider()
provider.addAttributes([QgsField("operator", QVariant.String, len=40)])
masts.updateFields()
rows = [
(1, "Hochberg", 412.5, QDate(2025, 6, 3), "NetCo", 571230.0, 5934010.0),
(2, "Kalkwerk", 388.0, QDate(2025, 6, 4), "NetCo", 573980.0, 5931475.0),
(3, "Seeblick", 405.2, QDate(2025, 6, 9), "AirLink", 569410.0, 5929920.0),
]
features = []
for mid, name, height, when, op, x, y in rows:
f = QgsFeature(masts.fields())
f.setAttributes([mid, name, height, when, op])
f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))
features.append(f)
ok, added = provider.addFeatures(features)
masts.updateExtents()
print(ok, len(added), "features; extent", masts.extent().toString(1))
Breakdown: updateFields() makes the layer pick up attributes added through the provider; skip it and masts.fields() still reports the old schema, so QgsFeature(masts.fields()) creates features one attribute short. Creating each feature from the layer's fields gives it the right number of attribute slots and lets you use f["name"] = … by name. addFeatures returns a success flag and the features with their new ids. updateExtents() matters for memory layers specifically: without it the cached extent is empty and "zoom to layer" goes nowhere. On QGIS 4 builds based on Qt6, use QMetaType.Type.QString and friends instead of QVariant.String — see the compatibility notes below.
Writing through the provider bypasses the edit buffer, which is what you want for a script that builds a layer from scratch. When the user should be able to undo the additions, or when you are adding to a layer that is already in edit mode, use masts.startEditing(), masts.addFeatures(...) and masts.commitChanges() instead, as described in undoing edits with the edit buffer.
Clone the schema of an existing layer
A common need is a memory layer that looks exactly like an existing one — same geometry type, CRS and fields — but holds a filtered or modified copy. QgsMemoryProviderUtils builds it directly from those three properties.
from qgis.core import (QgsMemoryProviderUtils, QgsFeatureRequest,
QgsVectorLayer, QgsProject)
roads = QgsProject.instance().mapLayersByName("roads")[0]
# 1. an empty layer with the same schema, filled by your own logic
copy = QgsMemoryProviderUtils.createMemoryLayer(
"roads (edited copy)", roads.fields(), roads.wkbType(), roads.crs())
request = QgsFeatureRequest().setFilterExpression("\"class\" = 'primary'")
batch = []
for f in roads.getFeatures(request):
f.setGeometry(f.geometry().simplify(2.0))
batch.append(f)
copy.dataProvider().addFeatures(batch)
copy.updateExtents()
# 2. a filled snapshot in one call
snapshot = roads.materialize(QgsFeatureRequest().setFilterExpression("\"class\" = 'primary'"))
snapshot.setName("primary roads (snapshot)")
QgsProject.instance().addMapLayers([copy, snapshot])
Breakdown: Features read from the source keep their attribute order, so they can be added to a layer with the same fields unchanged — no copying attribute by attribute. The simplify step stands for whatever modification the script needs. materialize is the shortest route when no per-feature logic is involved; it also accepts setSubsetOfAttributes and setDestinationCrs on the request, giving you a reprojected, column-trimmed copy in a single call. Both results are fully independent: editing them never touches roads.gpkg.
Keep the layer alive in scripts and plugins
A memory layer owns its features, and Python owns the layer until something else takes it over. That ownership rule explains the most confusing memory-layer bug: a layer created inside a function, returned to nothing and never added to a project, is garbage-collected when the function returns — and a layer added to the project from a plugin, then removed, takes its features with it.
from qgis.core import QgsVectorLayer, QgsProject
def build_scratch():
layer = QgsVectorLayer("LineString?crs=EPSG:4326&field=label:string", "scratch", "memory")
# ... fill it ...
return layer # caller must keep this reference
scratch = build_scratch() # alive while `scratch` is referenced
QgsProject.instance().addMapLayer(scratch, addToLegend=False) # project now owns it
# later: the project deletes the C++ object; the Python name becomes a dead wrapper
QgsProject.instance().removeMapLayer(scratch.id())
try:
scratch.featureCount()
except RuntimeError as err:
print("layer already deleted:", err)
Breakdown: addMapLayer(layer, addToLegend=False) is a useful middle ground for helper layers: the project owns and keeps the layer, Processing and expressions can see it by id, but it does not clutter the Layers panel. Once the project removes a layer, the underlying C++ object is destroyed and any Python variable still pointing at it raises RuntimeError: wrapped C/C++ object … has been deleted. The fix is not to keep the reference longer, but to stop using the layer after removal — or to call scratch.clone() first if the data is still needed. The wider rules behind this are covered in object ownership and crashes.
Memory layers are also handy for reading a layer's definition back. layer.source() on a memory layer returns its URI including the fields that were added later, so QgsVectorLayer(layer.source(), "empty twin", "memory") produces an empty layer with an identical schema — another quick way to clone the structure without the data.
Save the result before it disappears
A memory layer is lost when QGIS closes. Recent versions warn when a project with memory layers is closed, but scripts that run headless get no warning at all. When the layer is a result rather than a scratch surface, write it out.
import processing
out = processing.run("native:savefeatures", {
"INPUT": masts,
"OUTPUT": "/data/results/masts.gpkg",
"LAYER_NAME": "masts",
})["OUTPUT"]
saved = QgsVectorLayer(out, "masts", "ogr")
print(saved.isValid(), saved.featureCount(), "features written")
# keep the styling: copy the renderer from the scratch layer
saved.setRenderer(masts.renderer().clone())
QgsProject.instance().addMapLayer(saved)
QgsProject.instance().removeMapLayer(masts.id())
Breakdown: native:savefeatures writes any vector layer to any OGR format and returns the path with the layer name appended, ready to pass back to QgsVectorLayer. Swapping the memory layer for the saved one, with the renderer cloned across, means the map looks the same but now survives a restart. Recent QGIS versions can also store memory layer features inside the .qgz when a project is saved from Python, which suits interactive work; for automation, an explicit GeoPackage is clearer and readable by any other tool.
QGIS version compatibility
The memory provider and its URI syntax are stable across all QGIS 3 releases and QGIS 4. QgsMemoryProviderUtils.createMemoryLayer has existed since 3.0; QgsVectorLayer.materialize since 3.0 as well. The int8 field type and list types need 3.28 or newer. The one breaking change is in field types for Qt6-based QGIS 4 builds, where QgsField takes QMetaType.Type values (QMetaType.Type.QString, QMetaType.Type.Double, QMetaType.Type.Int) rather than QVariant constants; QGIS 3.38 and later accept both, so writing the QMetaType form today keeps a script portable.
Troubleshooting
- The layer is valid but shows nothing. The CRS was omitted, so coordinates in metres are being drawn as degrees somewhere off the map; or
updateExtents()was never called. - Attributes are all NULL. The feature was created with
QgsFeature()and no fields, or with fields fetched beforeupdateFields(). addFeaturesreturns False. A feature carries more attributes than the layer has fields, or a geometry of the wrong type — aMultiPolygoninto aPolygonlayer. Create the layer as theMultitype to accept both.- The layer vanished after reopening the project. Memory layers are not stored unless saved; write them out as shown above.
Conclusion
Declare a memory layer completely in its URI — geometry type, CRS, fields and index — add features in batches through the provider, use createMemoryLayer or materialize to copy an existing schema, and save anything worth keeping with native:savefeatures before the session ends.
Frequently Asked Questions
Are memory layers faster than GeoPackage for intermediate results? Usually yes for small and medium layers, because there is no disk I/O. For millions of features the RAM cost becomes the limit, and a temporary GeoPackage is the safer choice.
Can Processing algorithms write to a memory layer?
Yes — pass "OUTPUT": "memory:" or QgsProcessing.TEMPORARY_OUTPUT, as covered in using memory layers between algorithms.
How do I make an attribute-only table?
Use None as the geometry type: QgsVectorLayer("None?field=code:string", "lookup", "memory").
Does a memory layer support curved geometries?
Yes. Use CompoundCurve, CurvePolygon or MultiSurface as the geometry type.