Bluetooth Device Scanning with PyQt5 QtBluetooth

How to discover Bluetooth and Bluetooth Low Energy devices using PyQt5's QtBluetooth module
Heads up! You've already completed this tutorial.

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:

python
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:

sh
sudo systemctl status bluetooth

If it's not running, start it:

sh
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.

Get the book

sh
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:

sh
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:

python
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 QCoreApplication because we don't need a GUI — this is a console-only Bluetooth scanner.
  • The info parameter passed to on_device_discovered is a QBluetoothDeviceInfo object, 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. The or '(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:

python
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:

python
agent.setLowEnergyDiscoveryTimeout(5000)  # 5 seconds

If you want to find both classic and BLE devices in one scan, you can combine the methods:

python
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:

python
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:

python
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:

python
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.

The complete guide to packaging Python GUI applications with PyInstaller.
Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak
Martin Fitzpatrick

Bluetooth Device Scanning with PyQt5 QtBluetooth was written by Martin Fitzpatrick.

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.