Add and Remove Layers from a Project in PyQGIS

The first PyQGIS surprise almost everybody meets is a layer that loads successfully, reports isValid() as True, and then disappears the moment the script finishes. Nothing went wrong: a layer that has not been added to the project is owned only by your Python variable, and when that variable goes out of scope the layer is destroyed. Adding it to QgsProject transfers ownership, which is what makes it persist, appear in the Layers panel, render on the canvas and get written into the project file.

This recipe belongs to Working with QGIS Projects in PyQGIS. It covers adding one layer or many, choosing whether they show up in the panel, removing them cleanly, and the ownership rules that explain the odd behaviour beginners run into.

Who owns the layer decides whether it survivesOn the left a layer object is referenced only by a local Python variable; when the function returns, the reference is dropped and the underlying C++ object is destroyed, so nothing appears on the map. On the right the same layer is passed to addMapLayer, the project takes ownership, and the layer stays alive, renders, and is written into the project file.A layer nobody owns is a layer nobody seeslocal variable onlylayer = QgsVectorLayer(path, name, "ogr")function returns, reference droppeddestroyed — nothing on the mapadded to the projectproject.addMapLayer(layer)project holds the only reference it needsrenders, saves, keeps its id

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • A dataset to load — a GeoPackage, shapefile or raster on disk is enough.
  • Familiarity with loading layers; if QgsVectorLayer is new, start at QGIS Python Console Basics.

Add a layer

from qgis.core import QgsProject, QgsVectorLayer, QgsRasterLayer

project = QgsProject.instance()

roads = QgsVectorLayer("/data/city.gpkg|layername=roads", "Roads", "ogr")
dem = QgsRasterLayer("/data/terrain/dem.tif", "Terrain")

for layer in (roads, dem):
    if not layer.isValid():
        raise RuntimeError(f"{layer.name()} failed to load: {layer.error().summary()}")

project.addMapLayer(roads)
project.addMapLayer(dem)

Breakdown: Validating before adding keeps invalid layers out of the project entirely, which is what you want in a script — an invalid layer added to a project is saved into the file and reappears as a broken entry every time it is opened. error().summary() gives the provider's own message, which is far more useful than "layer failed": it distinguishes a missing file from a missing driver from a layer name that does not exist inside the container. addMapLayer() returns the layer on success and None on failure, so the return value is worth checking when the input is not under your control.

Adding several layers at once is a single call, and is meaningfully faster than a loop because the canvas refreshes once rather than per layer:

project.addMapLayers([roads, dem, parcels])

Breakdown: addMapLayers() takes a list and returns the list of layers actually added. On a project with many listeners — a running QGIS with several plugins — the difference between one batch call and twenty individual ones is visible to the user as a single redraw instead of a flicker per layer.

Add without showing it in the panel

The second argument decides whether the layer joins the layer tree. Passing False registers it while leaving the panel untouched, which is what you want for intermediate results, for layers you are about to place inside a specific group, and for anything a plugin needs to keep alive without cluttering the user's map.

project.addMapLayer(scratch, False)                 # registered, not shown

root = project.layerTreeRoot()
group = root.findGroup("Analysis") or root.addGroup("Analysis")
group.insertLayer(0, scratch)                       # now it appears, exactly where you chose

Breakdown: With addToLegend set to False the layer is in project.mapLayers() but has no node in the tree, so it does not render and does not appear in the panel — although it is still written into the project file when saved. The two-step form is the only way to control position: the one-argument call always inserts at the top of the tree. findGroup() returning None for a missing group is why the or idiom appears in most scripts that build structure.

Three ways to bring a layer into a scriptThree rows compare approaches. Adding with the legend flag true suits a layer the user should see immediately at the top of the panel. Adding with the flag false and inserting into the tree suits a layer that belongs in a specific group or position. Not adding at all suits a purely temporary layer used inside one function, which must not outlive it.Pick by what the user should end up seeingthe callwhat happensuse it whenaddMapLayer(layer)registered and shown on topthe result the user asked foraddMapLayer(layer, False)registered, you place itit belongs in a groupno add at alldies with your variablestrictly temporary work

Remove a layer

Removal takes the layer id, not the layer object, and it destroys the layer — every Python reference to it becomes dangerous immediately.

layer_id = roads.id()
project.removeMapLayer(layer_id)
roads = None                                  # do not touch the old reference again

Breakdown: removeMapLayer() takes the layer out of both the registry and the layer tree, then deletes the underlying C++ object. Any surviving Python variable is now a wrapper around freed memory, and calling a method on it crashes QGIS outright rather than raising a Python exception — one of the few ways PyQGIS can take the whole application down. Setting the variable to None immediately after removal is a cheap habit that prevents it. To remove several, removeMapLayers([id1, id2]) takes a list of ids, and removeAllMapLayers() empties the project.

What a removed layer leaves behindRemoving a layer by identifier takes it out of the registry and the layer tree and destroys the underlying object. A Python variable that still referenced it now points at freed memory, and calling any method on it crashes the application rather than raising an exception, which is why the reference should be cleared immediately.The variable outlives the object it points atremoveMapLayer(id)registry and tree clearedobject destroyedthe C++ side is goneyour variable still existsand now points at nothinglayer.name() nowcrashes QGIS outrightlayer = None firstand the mistake is impossible

Removing by name, which is what most scripts actually want, goes through a lookup:

for layer in project.mapLayersByName("Scratch"):
    project.removeMapLayer(layer.id())

Breakdown: mapLayersByName() returns a list because names are not unique — two layers can happily be called "Scratch", and a script that assumes [0] will remove one of them and leave the other. Iterating the list handles both cases. Collect the ids first if you need to remove layers while iterating the registry, because mutating mapLayers() during a loop over it is undefined behaviour in the same way as mutating a Python dictionary while iterating.

Replace a layer's data without removing it

Removing and re-adding is often the wrong tool: it loses the styling, the layer id, and every reference from layouts, joins and relations. Re-pointing the existing layer keeps all of it.

roads.setDataSource("/data/city_2026.gpkg|layername=roads",
                    roads.name(), roads.providerType())
roads.triggerRepaint()

Breakdown: setDataSource() swaps the underlying data while the layer object — and therefore its id — stays the same, so a layout that draws this layer keeps working and the symbology is preserved. The provider type must be passed unchanged unless you really are switching provider, for instance from ogr to postgres. triggerRepaint() asks the canvas to redraw; without it the map can show the old rendering until something else forces a refresh.

QGIS version compatibility

QGIS versionPythonNotes
3.22 LTR3.9All calls behave as described.
3.28 LTR3.9Identical.
3.34 LTR3.12Baseline for this page.
3.40 / 3.443.12Identical; addMapLayer gained no signature changes, and layer removal is unchanged.

QgsMapLayerRegistry, seen in pre-3.0 examples still circulating online, no longer exists — its methods moved onto QgsProject and the names are otherwise the same. Any snippet importing it is written for QGIS 2 and will need more than this one change.

Troubleshooting

  • The layer loads but never appears. It was not added to the project, or it was added with addToLegend as False and never inserted into a tree node.
  • QGIS crashes right after removing a layer. Something still holds a Python reference to the removed layer. Clear your references before removing, and never keep long-lived module-level references to layers.
  • addMapLayer() returns None. The layer was invalid. Check isValid() and error().summary() first — the project refuses invalid layers only in some builds, so relying on it is not portable.
  • Duplicate layers pile up on repeated runs. The script adds without checking. Look up by name first and either reuse or remove the existing one before adding.
  • Removing by name removed the wrong layer. Names are not unique. Store the id returned by layer.id() when you add the layer, and remove by that.
  • The panel shows the layer twice. A tree node was inserted for a layer that was already in the tree. Add with False when you intend to place it yourself.

Conclusion

addMapLayer() transfers ownership and is what keeps a layer alive; pass False as the second argument when you want to control where it appears, then insert a node into the layer tree yourself. Remove by id, drop your Python references immediately afterwards, and prefer setDataSource() over remove-and-re-add whenever the layer's styling and identity should survive.

Frequently Asked Questions

Why does my layer disappear when the function ends? Because nothing owns it. Add it to the project — or, for a genuinely temporary layer, keep it alive by holding a reference for as long as you need it.

What is the difference between addMapLayer and addMapLayers? Only batching. The plural form takes a list, emits fewer signals and triggers one canvas refresh, which is noticeably smoother when adding many layers at once.

Can I add the same layer object to two projects? No. Ownership is exclusive; the second project would try to take ownership of an object the first already owns. Build a second layer from the same data source instead.

How do I add a layer to a specific position in the panel? Register it with addMapLayer(layer, False), then call insertLayer(index, layer) on the root or on a group node — see Organise the Layer Tree with Groups in PyQGIS.

Does removing a layer delete the underlying file? No. Removal affects the project only; the GeoPackage, shapefile or database table is untouched.