Log Messages to the QGIS Message Log in PyQGIS

print() goes to the Python console, which is fine while you are looking at it and useless everywhere else — inside a plugin a user is running, inside a Processing algorithm, inside a background task, on a server. QGIS has a proper logging surface: a message log with tags and severity levels, and a message bar for the things a user needs to see right now. Knowing which of the three to reach for is most of the skill.

This recipe belongs to Debugging PyQGIS Scripts. It covers writing to the message log, choosing tags and levels, using the message bar for user-facing notices, reading what QGIS itself logs, and forwarding everything into Python's logging for unattended runs.

Three places a message can go, for three different readersThe Python console shows print output and suits interactive experimentation only. The message log panel keeps tagged, levelled, timestamped entries and is where diagnostics belong. The message bar shows a transient banner over the canvas and is for messages the user must act on. A fourth path forwards message log entries into Python logging for headless runs.Who is going to read this message?print()the Python consoleyou, right nowinvisible in a pluginQgsMessageLogthe Log Messages panelwhoever debugs this latertagged, levelled, timestampediface.messageBar()a banner over the canvasthe user, right nowuse sparinglymessageReceived → loggingfor servers and scheduled jobs

Prerequisites

  • QGIS 3.34 LTR (bundled Python 3.12) or newer.
  • The Log Messages panel open (View → Panels), or the small triangle icon in the status bar's bottom-right corner.
  • For the message-bar examples, iface — available inside a plugin and in the Python console.

Write to the message log

from qgis.core import QgsMessageLog, Qgis

QgsMessageLog.logMessage("Loaded 12 480 parcels", "ParcelTools", Qgis.Info)
QgsMessageLog.logMessage("3 features had null geometry", "ParcelTools", Qgis.Warning)
QgsMessageLog.logMessage("Could not open /data/wards.gpkg", "ParcelTools", Qgis.Critical)

Breakdown: The three arguments are message, tag and level. The tag becomes a tab in the Log Messages panel, so giving your plugin its own tag separates its output from the dozens of messages QGIS produces — and makes it findable when a user sends a screenshot. The levels are Qgis.Info, Qgis.Warning, Qgis.Critical and Qgis.Success; Critical makes the log panel's status-bar indicator turn red, which is the closest thing to an alert QGIS offers without interrupting the user. Entries are timestamped automatically.

Keep messages specific and countable. "Loaded 12 480 parcels" tells a future reader whether the run was normal; "done" tells them nothing, and two consecutive runs of "done" cannot be compared.

Choose the right level

LevelUse it forThe reader's reaction
Qgis.InfoNormal progress, counts, chosen parametersReads it only when investigating
Qgis.SuccessA completed operation worth confirmingReassured
Qgis.WarningSomething recoverable and unexpected — skipped rows, a fallback usedInvestigates later
Qgis.CriticalThe operation failed and produced no resultActs now

Breakdown: The discipline that makes levels useful is refusing to inflate them. A warning for something that happens on every run trains everyone to ignore warnings, and then the one that mattered is invisible too. If a condition is normal, log it as info; if it is genuinely unexpected, warn once per run rather than once per feature.

Speak to the user with the message bar

The log is for diagnosis. When the user needs to know something now, use the message bar.

from qgis.core import Qgis
from qgis.utils import iface

iface.messageBar().pushMessage(
    "Parcels",
    "12 480 features loaded; 3 had null geometry",
    level=Qgis.Warning,
    duration=8,
)

Breakdown: The first argument is a bold prefix and the second the body. duration is in seconds; passing 0 makes the message persist until dismissed, which is right for an error and rude for a success. The message bar is modeless — it does not steal focus or block work — which is exactly why it should be preferred over QMessageBox for anything that is not a question. One bar message per user action is a good ceiling; the details belong in the log, and pointing at the log in the bar text is a reasonable way to connect them.

Choosing where a message belongsIf the user must act, use the message bar with a short body and details in the log. If the message is diagnostic, write it to the message log with your plugin's tag. If the code runs unattended, forward the message log into Python logging so the entries reach a file or the journal. Print is only for interactive experimentation.One question decides it: who acts on this?the situationwhere it goesthe user must do somethingmessage bar, short body, details in the logsomeone may debug this latermessage log, your own tag, honest levelnobody is watching at allforward messageReceived into Python loggingyou, in the console, nowprint() — and delete it before committing

Read what QGIS is telling you

The most useful messages in the log are usually not yours. Providers, algorithms and plugins all write there, and the answer to "why did that layer fail to load" is frequently already sitting in the panel.

from qgis.core import QgsApplication

def on_message(message, tag, level):
    if level >= Qgis.Warning:
        print(f"[{tag}] {message}")

QgsApplication.messageLog().messageReceived.connect(on_message)

Breakdown: Connecting to messageReceived gives you every entry as it is written, including QGIS's own. Filtering by level keeps the noise down while you are hunting a specific failure. This is also the hook that makes headless runs observable — replacing the print with a call into Python's logging sends provider errors to the same file as everything else, which is the pattern developed in Handle Errors and Logging in Unattended Scripts.

Log from a Processing algorithm

Inside a custom algorithm the feedback object is the correct channel, because it routes to whichever front end is running — the dialog, the console, or qgis_process.

def processAlgorithm(self, parameters, context, feedback):
    feedback.pushInfo(f"Processing {source.featureCount()} features")
    if skipped:
        feedback.reportError(f"{skipped} features had invalid geometry", fatalError=False)
    return {"OUTPUT": sink_id}

Breakdown: pushInfo() and reportError() reach the user wherever the algorithm was launched from, which QgsMessageLog does not — a message logged to the panel during a qgis_process run may never be seen. fatalError=False reports a problem without aborting, which suits a per-feature complaint. Writing an algorithm this way is what makes it usable from the toolbox, from a model and from the command line, as covered in Write a Custom Processing Algorithm in PyQGIS.

Write messages that can be searched later

A log is read under pressure, usually months after it was written, often by someone else. Two habits make that reading fast.

Put the numbers in, and keep the shape constant. A message built as prose varies from run to run and cannot be compared; a message built from fixed keys and changing values can be grepped, diffed and even parsed.

def log_summary(tag, **fields):
    body = " ".join(f"{key}={value}" for key, value in sorted(fields.items()))
    QgsMessageLog.logMessage(body, tag, Qgis.Info)

log_summary("ParcelTools", stage="load", source="parcels", features=12480, seconds=3.4)
log_summary("ParcelTools", stage="buffer", distance=25, features=12480, seconds=41.2)

Breakdown: Sorting the keys means the same fields always appear in the same order, so two runs line up when diffed. Using key=value pairs rather than a sentence makes grep 'stage=buffer' | awk viable, which is the difference between answering "did this get slower?" in ten seconds and re-running the job to find out. Keeping the tag constant per plugin, rather than varying it per operation, keeps everything in one tab of the log panel where it can be read as a sequence.

Log the decision, not just the outcome. "Skipped 3 features" prompts an immediate second question; "skipped=3 reason=null_geometry ids=104,220,881" answers it. Where the identifiers are few, include them — a message that lets someone open exactly the three offending features has saved a round trip that would otherwise take a morning.

Prose reads once; key-value pairs read foreverProse log lines describe what happened in different words each run, so two runs cannot be compared. Key-value lines keep the same field names in the same order with different values, so a grep isolates one stage and a diff between two nights shows exactly which number changed.Written for the person reading it in three monthsproseLoaded the parcels layer OKBuffering took a while todayFinished, some features skippedkey=valuestage=load features=12480 seconds=3.4stage=buffer features=12480 seconds=41.2skipped=3 reason=null_geometry ids=104,220,881Constant field names in a constant order are what make two nights comparable

QGIS version compatibility

The examples target QGIS 3.34 LTR (Python 3.12).

QGIS versionPythonNotes
3.22 LTR3.9QgsMessageLog.logMessage and levels identical.
3.28 LTR3.9Adds notifyUser argument to control the status-bar indicator.
3.34 LTR3.12Baseline for this page.
3.40 / 3.443.12Qgis.MessageLevel members are scoped (Qgis.MessageLevel.Warning); the short spelling still resolves.

Troubleshooting

  • Nothing appears in the panel. The tag has its own tab — check the tab strip along the bottom of the Log Messages panel rather than only the General tab.
  • print() output has vanished. The code is not running in the console. Switch to QgsMessageLog.
  • The message bar notice disappears too quickly. Raise duration, or pass 0 for anything the user must acknowledge.
  • iface is not defined. You are outside the QGIS GUI. There is no message bar in a headless run; log instead.
  • The log fills with one message per feature. Aggregate: count the occurrences and log once at the end with the total.
  • Messages from a background task never appear. They do, but QgsMessageLog calls from a worker thread are queued; if the task crashed before returning, the queue may not have flushed. Log from finished() where possible.

Conclusion

Use QgsMessageLog.logMessage() with your own tag and an honest level for anything diagnostic, iface.messageBar() for the one thing the user needs to know, and a Processing feedback object inside algorithms. Connect to messageReceived when you need to see what QGIS itself is reporting, or to forward everything into Python logging for a run nobody is watching.

Frequently Asked Questions

Where is the log stored on disk? It is not — the panel is in-memory and cleared when QGIS closes. For a persistent record, forward the messages into Python logging and write them to a file.

Can I clear the log from Python? Not directly; the panel provides a clear action in its interface. A plugin that needs a clean slate should use its own tag so its entries are separable anyway.

Should a plugin log on every action? Log the ones with consequences — files written, features changed, parameters chosen — at info level. Skip narration of trivial steps; a log nobody can skim is a log nobody reads.

How do I show a progress message that updates? The message bar can host widgets, including a progress bar. That pattern is in Show Progress and Support Cancellation in a QGIS Plugin.

Is logging slow? Not at a sane rate. It becomes slow when called once per feature over a large layer, which is a reason to aggregate rather than a reason to avoid logging.