PyQt5 includes the QtBluetooth module, which gives you access to Bluetooth device discovery, services, and even Bluetooth Low Energy (BLE) communication — all from Python. If you're building a desktop app that needs to talk to Bluetooth devices (sensors, wearables, microcontrollers, etc.), this module can save you from wrestling with lower-level libraries like PyBluez.
In this tutorial, we'll walk through how to scan for Bluetooth devices using QBluetoothDeviceDiscoveryAgent, understand the common "Dummy backend" error, and build a working example that discovers nearby devices and prints their details.
The "Dummy backend" Error
If you've tried running a QtBluetooth script and seen output like this:
qt.bluetooth: Dummy backend running. Qt Bluetooth module is non-functional.
This means that the Qt Bluetooth module could not find a working Bluetooth backend on your system. The module loaded, but it can't actually do anything — it's running a placeholder ("dummy") backend instead.
There are a few common reasons for this:
No Bluetooth adapter present
Your machine might not have a Bluetooth adapter, or it might be disabled. Check your system settings to confirm Bluetooth hardware is available and turned on.
Missing system Bluetooth libraries
On Linux, Qt's Bluetooth support relies on BlueZ (the Linux Bluetooth stack). Make sure BlueZ is installed and running. You can check with:
sudo systemctl status bluetooth
If it's not running, start it:
sudo systemctl start bluetooth
You may also need the development libraries. On Debian/Ubuntu:
Create GUI Applications with Python & Qt5 by Martin Fitzpatrick — (PyQt5 Edition) The hands-on guide to making apps with Python — Save time and build better with this book. Over 15K copies sold.
sudo apt install libbluetooth-dev bluez
Qt was built without Bluetooth support
Some pre-built Qt packages (including those bundled with PyQt5 via pip) may not include full Bluetooth support for your platform. On Windows, Qt Bluetooth support requires the WinRT Bluetooth backend (available in Qt 5.14+). On macOS, it uses the CoreBluetooth framework.
Permissions issues
On Linux, accessing Bluetooth often requires elevated permissions. Try running your script with sudo, or add your user to the bluetooth group:
sudo usermod -aG bluetooth $USER
Then log out and back in.
Once your Bluetooth stack is properly set up and recognized by Qt, the dummy backend message will disappear, and discovery will work.
Scanning for Bluetooth Devices
The main class for device discovery is QBluetoothDeviceDiscoveryAgent. You create an instance, connect its signals to your handler functions, and call start(). The agent will emit a deviceDiscovered signal each time it finds a device, and a finished signal when the scan completes.
Here's a minimal example to see this in action:
import sys
from PyQt5.QtBluetooth import QBluetoothDeviceDiscoveryAgent, QBluetoothDeviceInfo
from PyQt5.QtCore import QCoreApplication, QTimer
def main():
app = QCoreApplication(sys.argv)
agent = QBluetoothDeviceDiscoveryAgent()
def on_device_discovered(info: QBluetoothDeviceInfo):
print(f"Discovered: {info.name() or '(unnamed)'} [{info.address().toString()}]")
def on_finished():
print("Scan complete.")
app.quit()
def on_error(error):
print(f"Scan error: {error}")
app.quit()
agent.deviceDiscovered.connect(on_device_discovered)
agent.finished.connect(on_finished)
agent.error.connect(on_error)
agent.start()
print("Scanning for Bluetooth devices...")
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Run this script, and if your Bluetooth stack is working, you should see device names and addresses printed to the console as they're discovered. The scan runs for a default duration and then the finished signal fires, which quits the application.
A few things to note about this example:
- We use
QCoreApplicationbecause we don't need a GUI — this is a console-only Bluetooth scanner. - The
infoparameter passed toon_device_discoveredis aQBluetoothDeviceInfoobject, which contains the name, address, RSSI, and other metadata about the discovered device. - Some devices may not advertise a name, so
info.name()can return an empty string. Theor '(unnamed)'fallback handles that.
Scanning for BLE Devices Specifically
By default, QBluetoothDeviceDiscoveryAgent scans for classic Bluetooth devices. If you're looking for Bluetooth Low Energy (BLE) devices — such as fitness trackers, IoT sensors, or BLE-enabled microcontrollers — you need to tell the agent to look for them specifically.
You do this by passing QBluetoothDeviceDiscoveryAgent.LowEnergyMethod to the start() method:
agent.start(QBluetoothDeviceDiscoveryAgent.LowEnergyMethod)
You can also set a timeout for BLE discovery. Unlike classic Bluetooth scanning, BLE scanning can run indefinitely, so setting a timeout is a good idea:
agent.setLowEnergyDiscoveryTimeout(5000) # 5 seconds
If you want to find both classic and BLE devices in one scan, you can combine the methods:
agent.start(
QBluetoothDeviceDiscoveryAgent.ClassicMethod
| QBluetoothDeviceDiscoveryAgent.LowEnergyMethod
)
Getting More Information from Discovered Devices
The QBluetoothDeviceInfo object provides a lot of useful information. Here are the most commonly used properties:
def on_device_discovered(info: QBluetoothDeviceInfo):
print(f"Name: {info.name()}")
print(f"Address: {info.address().toString()}")
print(f"RSSI: {info.rssi()} dBm")
# Check if this is a BLE device
core_config = info.coreConfigurations()
if core_config & QBluetoothDeviceInfo.LowEnergyCoreConfiguration:
print(" → This is a BLE device")
else:
print(" → This is a classic Bluetooth device")
# List any service UUIDs the device advertises
uuids = info.serviceUuids()
if uuids:
for uuid in uuids[0]: # serviceUuids() returns (list, completeness)
print(f" Service UUID: {uuid.toString()}")
print("---")
The rssi() method returns the signal strength in dBm, which can help you estimate how close a device is. The coreConfigurations() method tells you whether a device is classic Bluetooth, BLE, or both. And serviceUuids() returns a tuple of (uuid_list, completeness) — the completeness flag indicates whether the list of UUIDs is the full set or just a partial advertisement.
A Note on the Event Loop
It's important to remember that the scanning happens asynchronously — when you call agent.start(), it returns immediately. The actual discovery results arrive later via signals.
This means you must call app.exec_() (or app.exec() in PyQt6) to start the event loop. Without it, your program will exit before any devices are found.
Complete Working Example
Here's a complete, well-structured example that scans for both classic and BLE devices, prints detailed information about each discovery, and exits cleanly when the scan finishes:
import sys
from PyQt5.QtBluetooth import (
QBluetoothDeviceDiscoveryAgent,
QBluetoothDeviceInfo,
)
from PyQt5.QtCore import QCoreApplication, QTimer
class BluetoothScanner:
def __init__(self):
self.agent = QBluetoothDeviceDiscoveryAgent()
self.agent.setLowEnergyDiscoveryTimeout(10000) # 10 second timeout for BLE
self.agent.deviceDiscovered.connect(self.on_device_discovered)
self.agent.finished.connect(self.on_scan_finished)
self.agent.error.connect(self.on_scan_error)
self.devices_found = []
def start(self):
print("Starting Bluetooth scan (classic + BLE)...")
print("This will take a few seconds.\n")
# Scan for both classic and Low Energy devices
self.agent.start(
QBluetoothDeviceDiscoveryAgent.ClassicMethod
| QBluetoothDeviceDiscoveryAgent.LowEnergyMethod
)
def on_device_discovered(self, info: QBluetoothDeviceInfo):
name = info.name() or "(unnamed)"
address = info.address().toString()
# Determine device type
core_config = info.coreConfigurations()
if core_config & QBluetoothDeviceInfo.LowEnergyCoreConfiguration:
device_type = "BLE"
else:
device_type = "Classic"
rssi = info.rssi()
print(f"[{device_type}] {name} ({address}) RSSI: {rssi} dBm")
self.devices_found.append(info)
def on_scan_finished(self):
print(f"\nScan complete. Found {len(self.devices_found)} device(s).")
QCoreApplication.instance().quit()
def on_scan_error(self, error):
error_messages = {
QBluetoothDeviceDiscoveryAgent.NoError: "No error",
QBluetoothDeviceDiscoveryAgent.InputOutputError: "I/O error",
QBluetoothDeviceDiscoveryAgent.PoweredOffError: "Bluetooth adapter is powered off",
QBluetoothDeviceDiscoveryAgent.InvalidBluetoothAdapterError: "Invalid Bluetooth adapter",
QBluetoothDeviceDiscoveryAgent.UnsupportedPlatformError: "Unsupported platform",
QBluetoothDeviceDiscoveryAgent.UnsupportedDiscoveryMethod: "Unsupported discovery method",
}
message = error_messages.get(error, f"Unknown error ({error})")
print(f"Scan error: {message}")
QCoreApplication.instance().quit()
def main():
app = QCoreApplication(sys.argv)
scanner = BluetoothScanner()
# Use a single-shot timer to start the scan after the event loop is running.
QTimer.singleShot(0, scanner.start)
sys.exit(app.exec_())
if __name__ == "__main__":
main()
When you run this on a machine with a working Bluetooth adapter, you'll see output like:
Starting Bluetooth scan (classic + BLE)...
This will take a few seconds.
[BLE] Mi Band 5 (AA:BB:CC:DD:EE:FF) RSSI: -62 dBm
[Classic] Living Room Speaker (11:22:33:44:55:66) RSSI: -45 dBm
[BLE] (unnamed) (77:88:99:AA:BB:CC) RSSI: -78 dBm
Scan complete. Found 3 device(s).
The QTimer.singleShot(0, scanner.start) call ensures the scan starts after the event loop has begun. This is a common Qt pattern — it schedules the start() call to happen on the very next iteration of the event loop, which guarantees everything is properly initialized.
Where to Go from Here
Once you've discovered the device you want to connect to, the next steps in a BLE workflow are:
- Service discovery using
QLowEnergyController— this connects to the device and enumerates its GATT services. - Reading and writing characteristics using
QLowEnergyService— this is where you actually send and receive data. - Subscribing to notifications — many BLE devices push data to you via characteristic notifications.
The Qt documentation for QLowEnergyController and QLowEnergyService covers these classes in detail. The API translates directly to PyQt5 — method names, signal names, and enum values are all the same.
If you're still seeing the "Dummy backend" error after checking your hardware and system libraries, it may be worth trying a different Python environment or building PyQt5 from source against a Qt installation that was compiled with Bluetooth support enabled. On Linux, the Arch Wiki Bluetooth page is an excellent resource for troubleshooting BlueZ setup, regardless of which distribution you're using.