IoT Tutorials

Paho MQTT 2.0 Callback API Version: Fixing Migration Errors Step by Step

Paho-mqtt 2.0 broke a lot of Python code overnight. Learn what changed, how to fix the callback_api_version error in one line, and how to migrate every callback to the VERSION2 signatures with a working example.

Code diff showing a paho mqtt 2.0 callback api version migration from VERSION1 to VERSION2 callbacks
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

The paho mqtt 2.0 callback API version change is the single most common reason Python MQTT scripts suddenly stop working after a pip install --upgrade. This guide explains exactly what changed in paho-mqtt 2.0, gives you the one-line fix for the error, and then walks through migrating every callback to the recommended VERSION2 signatures. You will finish with a complete, working example connected to broker.connectmq.com that you can use as a template.

The Errors You Are Probably Seeing

If you landed here from a search engine, you most likely have one of these:

ValueError: Unsupported callback API version: version 2.0 added a callback_api_version, see docs/migrations.rst for details
TypeError: Client.__init__() missing 1 required positional argument: 'callback_api_version'
DeprecationWarning: Callback API version 1 is deprecated, update to latest version
TypeError: on_connect() takes 4 positional arguments but 5 were given

The first two come from creating the client the 1.x way. The ValueError appears when you pass a client ID as the first positional argument, as in mqtt.Client("my-device"), because that position now belongs to the callback API version. The third is a warning that you chose the old API explicitly. The fourth appears after you switch to VERSION2 but leave a callback with the old number of parameters.

What Changed in paho-mqtt 2.0

Version 2.0 of the Eclipse Paho Python client, released in early 2024, was the first major release in years. Its headline change is that callbacks are now versioned. In 1.x the parameters passed to callbacks differed between MQTT 3.1.1 and MQTT 5, and some used plain integers while others used reason code objects. Version 2 unifies them: every callback receives the same parameters regardless of protocol version, and result codes are always ReasonCode objects.

To avoid silently breaking code with new signatures, the maintainers made you choose explicitly. The first argument of Client() is now callback_api_version, with two values:

Value Behaviour Recommended?
CallbackAPIVersion.VERSION1 Old 1.x callback signatures, emits a deprecation warning Only as a temporary bridge
CallbackAPIVersion.VERSION2 New unified signatures, ReasonCode objects everywhere Yes, for all new and migrated code

Other notable changes in 2.0 include dropping support for Python 2.7 and very old Python 3 releases, and renaming the ReasonCodes class to ReasonCode. The full list lives in the official paho-mqtt migration notes.

Fix 1: The One-Line Bridge (VERSION1)

If production is down and you need it back now, tell paho to keep the old behaviour:

import paho.mqtt.client as mqtt

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION1, client_id="my-device")

All your existing callbacks keep working unchanged. You will see the deprecation warning, which is a reminder that VERSION1 will be removed in a future major release. Treat this as a bandage, not a fix.

An alternative bandage is pinning the old library with pip install "paho-mqtt<2.0". That works, but it blocks security and bug fixes and tends to conflict with other packages that already require 2.x. Prefer the VERSION1 flag over pinning, and plan the real migration.

Fix 2: Migrate to the Paho MQTT 2.0 Callback API Version 2

The real fix is to pass VERSION2 and update callback signatures. Here is every callback side by side:

Callback 1.x / VERSION1 signature VERSION2 signature
on_connect (client, userdata, flags, rc) (client, userdata, flags, reason_code, properties)
on_disconnect (client, userdata, rc) (client, userdata, disconnect_flags, reason_code, properties)
on_subscribe (client, userdata, mid, granted_qos) (client, userdata, mid, reason_code_list, properties)
on_unsubscribe (client, userdata, mid) (client, userdata, mid, reason_code_list, properties)
on_publish (client, userdata, mid) (client, userdata, mid, reason_code, properties)
on_message (client, userdata, message) unchanged
on_log (client, userdata, level, buf) unchanged

Under VERSION1 with MQTT 5, some of these already had extra parameters; VERSION2 removes that difference, which is the whole point of the change.

Migration checklist

Work through your code in this order:

  1. Change every mqtt.Client(...) call to mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=..., ...), passing all other arguments by keyword.
  2. Add properties to on_connect and rename rc to reason_code. Replace rc == 0 with not reason_code.is_failure (comparison with integers still works, but the attribute reads better).
  3. Replace flags["session present"] with flags.session_present.
  4. Give on_disconnect its new disconnect_flags parameter and properties.
  5. In on_subscribe, replace granted_qos with reason_code_list and check each entry: success codes have values 0 to 2 (the granted QoS), failures have is_failure set.
  6. Add reason_code and properties to on_publish and on_unsubscribe.
  7. Search the code base for ReasonCodes and change it to ReasonCode.
  8. Run your tests with warnings turned into errors (python -W error::DeprecationWarning) to prove no VERSION1 client is left.

A Complete VERSION2 Example

This script uses every common callback with the new signatures. Change k7f3q2 to your own random string so you do not share topics with other users of the public MQTT test broker:

import time
import uuid

import paho.mqtt.client as mqtt

BROKER = "broker.connectmq.com"
PREFIX = "connectmq/demo/k7f3q2"


def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print(f"Connect failed: {reason_code}")
        return
    print(f"Connected (session present: {flags.session_present})")
    client.subscribe(f"{PREFIX}/#", qos=1)


def on_subscribe(client, userdata, mid, reason_code_list, properties):
    for rc in reason_code_list:
        print("Subscribe rejected:" if rc.is_failure else "Granted QoS:", rc)


def on_publish(client, userdata, mid, reason_code, properties):
    print(f"Publish {mid} acknowledged: {reason_code}")


def on_message(client, userdata, msg):
    print(f"{msg.topic}: {msg.payload.decode()}")


def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
    print(f"Disconnected: {reason_code}")


client = mqtt.Client(
    mqtt.CallbackAPIVersion.VERSION2,
    client_id=f"migrate-{uuid.uuid4().hex[:8]}",
    protocol=mqtt.MQTTv5,
)
client.on_connect = on_connect
client.on_subscribe = on_subscribe
client.on_publish = on_publish
client.on_message = on_message
client.on_disconnect = on_disconnect

client.connect(BROKER, 1883, keepalive=60)
client.loop_start()
time.sleep(1)

for i in range(3):
    client.publish(f"{PREFIX}/test", f"hello {i}", qos=1)
    time.sleep(1)

client.disconnect()
client.loop_stop()

Run it and you should see the connect, the granted QoS, three publish acknowledgments and the three messages echoed back by the ConnectMQ public broker. Switch protocol to mqtt.MQTTv311 and the same callbacks still work without edits, which is exactly what VERSION2 promises. If you are new to the library itself, start with the paho mqtt python tutorial first.

Pitfalls After Migrating

Third-party libraries pinned to 1.x. Some packages that wrap paho declared paho-mqtt<2.0 in their requirements. Check with pip show and pip check. Recent releases of most maintained wrappers support 2.x; upgrade them, or isolate legacy tools in their own virtual environment.

Positional arguments. Any mqtt.Client("id", False, None, mqtt.MQTTv311) call is now shifted by one. Convert all of them to keyword arguments; it makes future upgrades painless.

Callbacks registered with decorators. Code using @client.connect_callback() and similar decorators must update the decorated function signatures too.

Swallowed exceptions. By default paho logs exceptions raised inside callbacks instead of crashing your program. A wrong signature can therefore look like "nothing happens". Attach a logger with client.enable_logger() while migrating so the TypeError is visible.

Reconnect logic. If you compared rc integers in on_disconnect to decide whether to reconnect, rewrite it around reason_code. The paho-mqtt reconnect guide shows robust patterns, and the Python MQTT TLS guide covers the encrypted connection you should use in production.

Should You Consider aiomqtt Instead?

If your application is built on asyncio, migrating callbacks may be a good moment to move to an async API altogether. aiomqtt wraps paho and replaces callbacks with async for message in client.messages. Our aiomqtt and asyncio tutorial shows the same publish/subscribe flow written that way. For a broader comparison of Python and other language clients, see the MQTT client libraries overview.

Test Your Migrated Code on ConnectMQ

ConnectMQ runs a free public broker at broker.connectmq.com that is ideal for verifying a migration, and a fully managed MQTT service for production with MQTT 3.1.1 and 5.0, TLS on 8883 and generated credentials. Its portal includes a live topic explorer so you can confirm migrated clients still publish what you expect. When you are ready to move off the public broker, run your clients on a private cloud MQTT broker: sign up for the free Serverless plan with no credit card.

Frequently Asked Questions

What is callback_api_version in paho-mqtt?

It is the first argument of mqtt.Client() since paho-mqtt 2.0. It selects which callback signatures the client uses: VERSION1 for the old 1.x style or VERSION2 for the new unified style.

Should I use VERSION1 or VERSION2?

Use VERSION2. VERSION1 exists only to keep old code running and prints a deprecation warning, so it will eventually be removed.

Why do I get "Unsupported callback API version" when passing a client ID?

You passed the client ID as the first positional argument, which is now reserved for the callback API version. Pass mqtt.CallbackAPIVersion.VERSION2 first and the ID as client_id=....

Do I need to change on_message when migrating?

No. on_message(client, userdata, message) keeps the same signature in both versions. The changes affect connect, disconnect, subscribe, unsubscribe and publish callbacks.

Related articles

Best MQTT CLI Tools for Testing, Scripting and Debugging

A hands-on comparison of mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, the MQTT.js CLI, curl and mqttui, with the same test run against broker.connectmq.com so you can choose the right tool for scripting and debugging.

8 min read

How to Store MQTT Data in InfluxDB with Python

Build a Python bridge with paho-mqtt and influxdb-client that subscribes to a broker, turns topic levels into InfluxDB tags, writes numeric JSON values as fields in batches and handles bad payloads and type conflicts.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.