Tutorials

Paho MQTT 2.0 Migration: Fix CallbackAPIVersion Errors

Upgrade paho-mqtt 1.x to 2.x: fix the CallbackAPIVersion ValueError, update on_connect and other callback signatures, and handle the new ReasonCode objects.

Updated · 8 min read

Paho MQTT 2.0 Migration: Fix CallbackAPIVersion Errors

paho-mqtt 2.0 changed how you create a client and what your callbacks receive. If your script broke after a pip install --upgrade, the fix is almost always the same: pass mqtt.CallbackAPIVersion.VERSION2 as the first argument to mqtt.Client() and update the callback signatures. This guide shows the exact errors, what changed, and a complete before/after example.

The errors you are probably seeing

ErrorCauseFix
ValueError: Unsupported callback API version: version 2.0 added a callback_api_version, see docs/migrations.rst for detailsThe client ID was passed as the first positional argument: mqtt.Client("my-id")mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="my-id")
DeprecationWarning: Callback API version 1 is deprecated, update to latest versionmqtt.Client() without a version, or with VERSION1Pass VERSION2 and update callbacks
TypeError: on_connect() takes 4 positional arguments but 5 were givenVERSION2 with a 1.x-style on_connect(client, userdata, flags, rc)Add the properties parameter (see below)
TypeError: on_disconnect() takes 3 positional arguments but 5 were givenSame for on_disconnect(client, userdata, rc)New signature with disconnect_flags, reason_code, properties
AttributeError: module 'paho.mqtt.client' has no attribute 'CallbackAPIVersion'Your code is for 2.x but the environment still has 1.xpip install "paho-mqtt>=2.0" and check which interpreter runs the script

Check the installed version first — virtual environments and system Python often disagree:

Which paho-mqtt is installed?
python -c "import paho.mqtt; print(paho.mqtt.__version__)"
pip show paho-mqtt

What changed in 2.0

Version 1.x had a single callback style whose arguments depended on the protocol version: MQTT 3.1.1 callbacks received an integer rc, MQTT 5 callbacks received reason_code and properties. That made it easy to write code that worked on one protocol and crashed on the other.

2.0 introduces callback API versions. With CallbackAPIVersion.VERSION2, every callback has the same signature on MQTT 3.1.1 and MQTT 5, reason codes are always ReasonCode objects, and properties are always passed (as None when the protocol has none). The constructor now looks like this:

python
mqtt.Client(
    callback_api_version,          # new, first positional argument
    client_id="",
    clean_session=None,
    userdata=None,
    protocol=mqtt.MQTTv311,
    transport="tcp",
    reconnect_on_failure=True,
    manual_ack=False,
)

Other things to know: 2.x requires Python 3.7 or newer, and the publish.single() / subscribe.simple() helpers handle the version for you, so code that only uses them usually keeps working.

Callback signatures: 1.x vs VERSION2

Callback1.x (MQTT 3.1.1)2.x with VERSION2
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

Three details matter in practice:

  • flags is no longer a dict. In on_connect it is a ConnectFlags object: use flags.session_present instead of flags["session present"].
  • disconnect_flags.is_disconnect_packet_from_server tells you whether the broker sent a DISCONNECT (MQTT 5) or the library detected the loss itself.
  • on_subscribe gets reason codes, not plain QoS integers. For a granted QoS 1 subscription, the reason code's value is 1; a refused subscription has a value of 0x80 or higher, so rc.is_failure is true.

Reason codes are objects now

With VERSION2, reason_code is a ReasonCode. It compares equal to an integer and to its name, prints as its name, and has an is_failure property:

python
def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        print(f"connect failed: {reason_code} ({reason_code.value})")
        return
    print("connected, session present:", flags.session_present)

reason_code == 0 and reason_code == "Success" both still work for the success case, so the most common check survives the upgrade unchanged.

A complete before/after example

This is typical 1.x code — it breaks on 2.x with the ValueError above:

client_v1.py — paho-mqtt 1.x
import paho.mqtt.client as mqtt

def on_connect(client, userdata, flags, rc):
    if rc == 0:
        client.subscribe("testmqtt/paho2/#", qos=1)
    else:
        print("connect failed, rc =", rc)

def on_disconnect(client, userdata, rc):
    print("disconnected, rc =", rc)

def on_message(client, userdata, msg):
    print(msg.topic, msg.payload.decode())

client = mqtt.Client("testmqtt-paho2-demo")
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, 60)
client.loop_forever()

The same client migrated to VERSION2:

client_v2.py — paho-mqtt 2.x
import os
import paho.mqtt.client as mqtt

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

def on_subscribe(client, userdata, mid, reason_code_list, properties):
    for rc in reason_code_list:
        print("subscribe:", "refused" if rc.is_failure else f"granted QoS {rc.value}")

def on_disconnect(client, userdata, disconnect_flags, reason_code, properties):
    print(f"disconnected: {reason_code} (from server: {disconnect_flags.is_disconnect_packet_from_server})")

def on_message(client, userdata, msg):
    print(msg.topic, msg.payload.decode())

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="testmqtt-paho2-demo")
if os.environ.get("MQTT_USER"):
    client.username_pw_set(os.environ["MQTT_USER"], os.environ.get("MQTT_PASS"))
client.on_connect = on_connect
client.on_subscribe = on_subscribe
client.on_disconnect = on_disconnect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_forever()

The same callbacks work unchanged with protocol=mqtt.MQTTv5 — that is the main benefit of VERSION2. Only the constructor arguments differ: MQTT 5 does not accept clean_session (it raises ValueError: Clean session is not used for MQTT 5.0); use clean_start and session expiry in connect() instead. The MQTT 5 vs 3.1.1 migration guide covers those options.

A migration checklist

  1. Pin the version you migrate to, for example paho-mqtt>=2.1,<3 in requirements.txt, so the next major release cannot surprise you.
  2. Search the codebase for mqtt.Client( and pass mqtt.CallbackAPIVersion.VERSION2 first, with client_id= as a keyword.
  3. Update every callback to the new signature — including ones assigned in other modules or libraries.
  4. Replace integer comparisons on rc with reason_code.is_failure or name comparisons.
  5. Replace flags["session present"] with flags.session_present.
  6. Run with warnings visible (python -W error::DeprecationWarning app.py) to catch any client still on VERSION1.

If you are rewriting a lot of code anyway, consider whether an async client fits better — aiomqtt for asyncio is built on paho-mqtt 2.x. For other paho exceptions, see paho-mqtt Python errors.

Test the migrated client

Run client_v2.py, then open the online MQTT client in your browser, connect to broker.connectmq.com and publish to testmqtt/paho2/hello. Your script should print the topic and payload. To exercise the error paths, connect with a wrong password to your own broker and check that on_connect prints a readable reason such as Bad user name or password instead of a bare number.

Publish a test message to your migrated client

The paho-mqtt 2.0 migration is mostly mechanical: one new constructor argument, five callback signatures and a few integer checks that become reason code checks. Do it once with VERSION2, pin the major version, and your callbacks will behave the same on MQTT 3.1.1 and MQTT 5. For a full walkthrough of the library, see the Python MQTT guide.

Frequently asked questions

What does 'Unsupported callback API version' mean in paho-mqtt?

Since paho-mqtt 2.0 the first argument of mqtt.Client() is the callback API version. Code written for 1.x that passes the client ID as the first positional argument now raises this ValueError. Pass mqtt.CallbackAPIVersion.VERSION2 first and the client ID as client_id=.

Can I keep my old callbacks with paho-mqtt 2.x?

Yes, temporarily. Pass mqtt.CallbackAPIVersion.VERSION1 and the 1.x callback signatures keep working, but the library emits a DeprecationWarning and version 1 is planned to be removed in a future major release. Migrate to VERSION2 when you can.

Why does my 'if rc == 5' check no longer work after upgrading?

With VERSION2, on_connect receives a ReasonCode object even on MQTT 3.1.1, and 3.1.1 return codes are mapped to MQTT 5 reason codes. 'Not authorized' is 135 (0x87), not 5. Use reason_code.is_failure or compare against the name, for example reason_code == 'Not authorized'.