Catch Exceptions and Read Tracebacks in PyQGIS

PyQGIS has two failure modes and only one of them raises. Python errors — a typo, a None, a bad index — produce a traceback you can read. The C++ API underneath mostly does not raise at all: it returns False, or an invalid layer, or an error code you did not check, and the script carries on until something unrelated breaks twenty lines later. Handling both is what separates a script that tells you what went wrong from one that produces an empty output and a shrug.

This recipe belongs to Debugging PyQGIS Scripts. It covers reading a traceback quickly, catching exceptions where you can act on them, checking the return values that never raise, and logging failures in a form that is useful when nobody is watching.

Read a traceback from the bottom upThe last line of a traceback names the exception type and message and is where to start. The frame directly above it is the line that actually failed, usually inside your own code. Further up are the calls that led there, and at the very top is the entry point, which is the least informative part despite being read first by most people.The useful part is at the end, not the startFile console.py, line 1, in module — the entry point, least usefulFile tools.py, line 44, in run — how you got hereFile tools.py, line 91, in summarise — the line that failedAttributeError: NoneType object has no attribute getFeaturesreadupwardType and message first, then the deepest frame in your own code

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • The Python console open, and a script that fails — the easiest kind to obtain.
  • The Log Messages panel visible, from the icon at the bottom right of the QGIS window.

Read the traceback in the right order

Start at the bottom. The final line gives the exception type and message, and those two together usually identify the problem. The frame immediately above it is the line that failed, which in a PyQGIS script is nearly always in your own code rather than in QGIS. Work upward only if the failing line does not make sense on its own.

Three exception types cover most PyQGIS failures, and each has a characteristic cause:

  • AttributeError: 'NoneType' object has no attribute ... — something returned None and you used it anyway. iface.activeLayer() with no layer selected, mapLayersByName() indexed with [0] on an empty list, a field lookup that missed.
  • KeyError or IndexError on a feature — a field name that does not exist, or an attribute index that is off by one. Print layer.fields().names() and the answer is immediate.
  • TypeError: arguments did not match any overloaded call — the distinctive PyQt message meaning you passed the wrong type to a C++ method. The message lists the signatures it tried, which tells you exactly what was expected.

That last one deserves attention because it looks alarming and is usually trivial: a Python float where a QVariant was wanted, a str where a QgsField was wanted, a list where a single value was wanted.

Catch what you can actually handle

from qgis.core import QgsProject, QgsVectorLayer


def load_parcels(path):
    layer = QgsVectorLayer(path, "Parcels", "ogr")
    if not layer.isValid():
        raise RuntimeError(f"cannot open {path}: {layer.error().summary()}")
    return layer


try:
    layer = load_parcels("/data/city.gpkg|layername=parcels")
except RuntimeError as error:
    print(f"Skipping this dataset — {error}")
    layer = None

Breakdown: The rule for try/except is to catch only where you can do something specific: skip this file, fall back to a default, retry once. Wrapping a whole script in a bare except and printing "an error occurred" destroys the traceback and with it any chance of a quick fix. Note that the layer failure is converted into an exception first — the API returned an invalid object rather than raising, and turning that into a raise at the boundary is what lets the rest of the code use ordinary exception handling. layer.error().summary() gives the provider's own message, which distinguishes a missing file from a missing driver from a layer name that is not in the container.

Never catch Exception without re-raising unless the handler genuinely ends the operation:

import traceback

try:
    run_analysis()
except Exception:
    print(traceback.format_exc())     # keep the full detail
    raise                             # and let it propagate

Breakdown: traceback.format_exc() returns the whole traceback as a string, which can be logged, written to a file or shown in a dialog. Re-raising afterwards means the failure still reaches whatever is above — a scheduler, a test runner, the console — instead of being silently absorbed. A handler that logs and swallows is the single most effective way to make a problem un-debuggable.

Check the returns that never raise

Much of the QGIS API predates exceptions and reports failure by return value. These are the ones worth checking every time:

layer = QgsVectorLayer(uri, "Parcels", "ogr")
assert layer.isValid()                                  # invalid, not raised

ok = layer.startEditing()                               # False if not editable
ok = layer.addFeatures([feature])                       # False on rejection
ok = layer.commitChanges()                              # False, errors in commitErrors()
if not ok:
    print(layer.commitErrors())

error, message, _, _ = QgsVectorFileWriter.writeAsVectorFormatV3(...)
if error != QgsVectorFileWriter.NoError:
    raise RuntimeError(message)

ok = QgsProject.instance().write(path)                  # False if the write failed

Breakdown: Every one of these returns a boolean or an error code that a script can ignore without any warning. commitChanges() is the most consequential: it returns False when a constraint, a provider rejection or an invalid geometry blocked the commit, and commitErrors() holds the reason. A script that ignores it reports success while having written nothing — the exact failure mode that makes people distrust automation. The habit worth building is to treat every one of these calls as a potential raise site and convert it, as the loader above does.

Two failure styles, one of them silentPython errors raise an exception, which stops execution and produces a traceback naming the line. QGIS API calls typically return false or an invalid object, so execution continues and the failure surfaces later somewhere unrelated. Converting the second kind into the first at the point it happens gives every failure the same shape.Make the quiet failures loud at the point they happenPython errorsraise and stop immediatelytraceback names the lineeasy to diagnoseAPI return valuesreturn False or an invalid objectexecution continuessurfaces later, somewhere elsecheck the return, raise your own errornow both kinds fail the same way, at the right line

Log failures where they can be found later

print() is fine in the console and useless in a plugin or a scheduled job. QGIS's message log is the right destination for anything a user might need to see.

from qgis.core import QgsMessageLog, Qgis
import traceback

try:
    run_analysis(layer)
except Exception as error:
    QgsMessageLog.logMessage(
        f"Analysis failed on {layer.name()}: {error}\n{traceback.format_exc()}",
        "Parcel Tools",
        level=Qgis.Critical,
    )
    raise

Breakdown: The second argument is the tag, which becomes a tab in the Log Messages panel — using your plugin's name keeps your output separate from QGIS's own. Including both the exception and the formatted traceback means a user can copy one block that contains everything you need. The Critical level makes the panel's indicator turn red, which is the only reliable way a user notices something happened. Adding the layer name to the message is the difference between a report you can act on and one that says only that something failed.

Where an error message should goDuring development the console is the right destination because you are watching it. In a plugin the QGIS message log is right, because the user can find it and copy it. In a scheduled job a log file plus a non-zero exit status is right, because nobody is watching and a monitoring system needs a signal.Nobody is watching standard output at three in the morningin the consoleprint and the tracebackyou are looking at itdevelopment onlyin a pluginQgsMessageLog with a tagCritical turns the light redthe user can copy itin a scheduled joba log file with timestampsand a non-zero exitmonitoring can see it The wider logging patterns, including writing to files for unattended runs, are in Log Messages to the QGIS Message Log in PyQGIS and Handle Errors and Logging in Unattended Scripts.

Fail early with cheap checks

Most PyQGIS failures are avoidable by validating inputs at the top of a function rather than discovering them in the middle of a loop.

def summarise(layer, field):
    if layer is None or not layer.isValid():
        raise ValueError("a valid vector layer is required")
    if field not in layer.fields().names():
        raise ValueError(f"{field} is not in {layer.name()}: "
                         f"{', '.join(layer.fields().names())}")
    if layer.featureCount() == 0:
        raise ValueError(f"{layer.name()} has no features")
    ...

Breakdown: Three checks costing microseconds prevent three errors that would otherwise appear thousands of iterations later, with no indication of which input was wrong. Listing the available field names in the message turns "field not found" into a message that contains the answer. Raising ValueError rather than a bare RuntimeError also lets a caller distinguish "you gave me bad input" from "something went wrong while working", which matters as soon as the function is called from more than one place.

QGIS version compatibility

QGIS versionPythonNotes
3.22 LTR3.9Same behaviour; some newer API calls raise where older ones returned codes.
3.28 LTR3.9Identical.
3.34 LTR3.12Baseline for this page. Python 3.11 and later add finer-grained error locations in tracebacks, which point at the exact expression.
3.40 / 3.443.12Identical; more of the API is gradually moving to exceptions, particularly around provider connections.

Where an API call raises on one release and returns a code on another, checking both — a try around a call whose return you also check — is ugly but portable.

Troubleshooting

  • The traceback stops at a QGIS module. The error came from inside C++. The message and the arguments you passed are the evidence; check types first.
  • No traceback at all, and no result. An API call returned a failure code that was not checked. Add checks to the writer, commit and project calls.
  • TypeError: arguments did not match any overloaded call. A wrong argument type. The listed signatures show what was expected.
  • The error only happens in a scheduled run. Something in the environment differs — a path, a provider, an authentication entry. Log the environment at startup.
  • The exception is swallowed somewhere. A bare except in your code or a plugin's. Search for except: and except Exception without a re-raise.
  • The Log Messages panel shows nothing. The wrong tab is selected, or the messages went to print() and into the console instead.

Conclusion

Read tracebacks from the bottom up: type and message first, then the deepest frame in your own code. Catch exceptions only where the handler can do something specific, and always re-raise after logging. Most importantly, check the return values of the API calls that never raise — validity, commit, write, project save — and convert them into exceptions at the point of failure, so every problem fails at the line that caused it.

Frequently Asked Questions

Why does QGIS crash instead of raising an exception? A hard crash usually means a C++ object was used after being deleted — most often a layer that was removed while a Python variable still referenced it. That is not catchable; avoid it by clearing references after removal.

Should I use assertions for validation? Not in shipped code: assertions can be disabled and their messages are for developers. Raise ValueError with a message the user can act on.

How do I see the traceback from a background task? Catch it inside the task's run() and log it there — an exception on a worker thread does not reach the console. See Run a Background Task with QgsTask in PyQGIS.

What does commitErrors() return when a commit fails? A list of strings from the provider, naming constraint violations, invalid geometries or permission problems. Print it — it is far more specific than the boolean.

Can I make QGIS show a dialog on every exception? It already does for uncaught exceptions in plugins. For your own code, decide deliberately: a dialog for something the user must act on, the message log for everything else.

Is print() ever the right choice? In the console, yes. Anywhere that runs unattended, no — use the message log or a file, because nobody is watching standard output.