Tuesday Tooling: Get Notifications and More With Plyer

Hello! Yes, it has been some time. How are you? 2026 has put me through the wringer a bit, but I'm still here, so let's look at a cool Python module.

So, What Is It?

"Plyer is a platform-independent Python API for accessing hardware features of various platforms (Android, iOS, macOS, Linux and Windows)." – Plyer project website.

So, What Features Does It Have?

Notifications and alerts

  • notification: The main feature. Shows desktop or mobile pop-up notifications with a title, message, icon and timeout. Icons are a bit of an issue. They don't seem to work on my Ubuntu 26.04 laptop, something relating to dbus it seems. You can use emojiis in your text.
  • vibrator: Makes a phone vibrate for a set time or in a pattern (obviously this is mobile only).
  • flash: Turns the device's camera torch on or off (mobile only).
  • tts (text-to-speech): Speaks a string of text aloud. On Linux this relies on espeak.

Sensors (mostly mobile only)

  • accelerometer: Reads device movement and tilt on three axes.
  • gyroscope: Reads rotation speed around each axis.
  • compass: Gives the device's magnetic heading.
  • gps: Provides location data (latitude, longitude, speed, altitude) through a callback.
  • barometer, light, proximity, humidity, temperature, gravity, spatialorientation: Read the matching hardware sensors where the device has them.

Device and system information

  • battery: Reports battery status and charge level.
  • cpu: Lists CPU details and core count.
  • storagepath: Returns storage paths for common directories.
  • uniqueid: Gives a device-specific identifier.
  • devicename: Returns the device's hostname.
  • wifi: Scans for networks and connects or disconnects (limited platform support; it didn't work on my Ubuntu laptop).
  • bluetooth: Basic Bluetooth control (it sort of worked on Ubuntu).
  • brightness: Gets or sets screen brightness.
  • orientation: Reads or sets screen rotation / orientation (mobile only).

User interaction

  • filechooser: Uses the operating system's native "open", "save" or "choose" dialog to create a user interface.
  • camera: Takes a photo or records video using the system camera app (this did not work for me on Ubuntu but it should work on mobile and Windows / macOS).
  • audio: Records audio from the microphone and plays it back.
  • email: Opens the default email client with a pre-filled recipient, subject and body.
  • sms: Sends a text message on your mobile phone.
  • call: Starts a phone call on your mobile phone.

So, What Can I Use It For?

You want an easy way to show notifications in your Python projects across a plethora of platforms. The same code that pops up a notification on your laptop or desktop PC can also work on Android or iOS, although on mobile it normally runs inside an app packaged with a tool such as Kivy or Buildozer, rather than as a plain python test.py. I've never used Kivy or Buildozer, so I can't comment on that.

So, How Do I Install It?

Using a virtual environment (virtualenv) and the Python package manager, pip!

💡
Before moving on, you will need to understand that many operating systems now protect their own Python installation. If you try to pip install globally, you may see an "externally-managed-environment" error (PEP 668). The solution is to use a virtual environment. I've got a guide that you can follow!

Open a terminal and run the following to install:

pip install plyer

It is now ready for use, so let's use it!

So, How Do I Use It?

We'll start with the "Hello World" of notifications.

Open a text editor and import the notification feature from plyer:

from plyer import notification

Next, call the notify() function. We give it three arguments: the title of the pop-up, the message that we want to show the user, and the app_name, which is the name of the application sending the message. In this case we are just playing around, but in reality this would clearly identify the application. There are optional extras too, such as timeout (how many seconds it stays on screen) and app_icon.
As I said earlier, icons didn't work for me on Ubuntu 26.04.

notification.notify(
    title='Important Message',
    message='The first TuesdayTooling blog post in many years!',
    app_name='bigl.es woz ere'
)

Save the code as test.py and then run it:

python test.py

You should see the notification appear in the notification area of your OS.

So, Now Do Something Useful With It!

First project: selecting a file using the Operating System's native file manager

I'm on Ubuntu 26.04.1 and, in order to do this, I needed to install setuptools. Why? It seems that distutils was removed from the standard Python library, and setuptools provides a compatibility "shim" for it.

While we are still inside the Python virtual environment, run this command:

pip install setuptools

We can now write the code for this first project.

Open your preferred text editor and, on the first line, import filechooser:

from plyer import filechooser

Create a function, handle_selection(), which will print the full path of the selected file. Note that plyer gives us a list of paths (you can select more than one file), so you will see square brackets in the output. Use selection[0] if you only want the first path.

def handle_selection(selection):
    print("You picked:", selection)

Call the file open dialog. When a selection is made, the function is called and the path is shown in the terminal:

filechooser.open_file(on_selection=handle_selection)

Save the code as files.py and run it:

python files.py

The file open dialog will appear. Select a file and you'll see its path printed in the terminal. (If you'd like to pick a folder instead, use filechooser.choose_dir() in the same way.)

This could easily be implemented in a project such as a gallery image viewer, a media player, etc.

Second project: there's somebody at the door!

Those of us of a certain age will have just had a core memory unlocked.

I've got a Home Assistant setup, and my doorbell is a Sonoff SNZB-01P Zigbee wireless switch, which I got for work so that I could write about Home Assistant projects. It works great, and I've got a custom audio player in the living room that reacts to it. The button sends a message via Zigbee to my Home Assistant Green, which then publishes an MQTT message on a certain topic, doorbell, and the player reacts accordingly.

The thing is, I spend 90% of my time in my home office, so I needed something that I would always see. I thought, "Perhaps a plyer notification, sent over the same MQTT setup to my laptop / PC, would work?" It did, and here is how I made it.
This is still inside the plyer virtual environment that I created earlier.

Firstly, I installed the paho-mqtt module for Python. This enables me to publish and subscribe via an MQTT broker:

pip install paho-mqtt

I created a new file in a text editor, doorbell.py, and started by importing the modules that I needed:

  • time: Used to take timestamps, so that we can work out how long ago the last ring was.
  • paho.mqtt.client: Enables me to connect to an MQTT broker and wait for messages.
  • plyer: The notification tool that is the focus of this blog post.
import time
import paho.mqtt.client as mqtt
from plyer import notification

Next, create a series of constants that handle the MQTT connection details:

  • BROKER: The MQTT server, which handles communication between devices publishing information and those that are subscribed to receive it. This broker is my Home Assistant Green, but it could be a Pi running an MQTT service or even a public broker. Hostnames or IP addresses can be used.
  • PORT: MQTT typically uses port 1883 for connections, but this can be changed to suit your setup.
  • TOPIC: MQTT uses topics (channels) as routes between publishers and subscribers.
  • CLIENT_ID: Devices on the MQTT network need a unique ID. If two devices share the same ID, one will knock the other off the network. I found this out the hard way!
  • USERNAME: The username that I set up in Home Assistant.
  • PASSWORD: The password that I set up in Home Assistant.
  • COOLDOWN: The time (in seconds) during which repeated doorbell presses are ignored.
BROKER = "homeassistant.local"
PORT = 1883
TOPIC = "doorbell"
CLIENT_ID = "YOUR UNIQUE CLIENT ID HERE"
USERNAME = "mqtt-user"
PASSWORD = "YOUR PASSWORD HERE"
COOLDOWN = 5

Store the time of the last ring in a variable, last_ring. It starts at 0, so the first ring always gets through:

last_ring = 0

Now create a function that the MQTT module calls every time the client connects to the broker. We use an if conditional to test the broker's response to our connection request. If it failed, we show a notification and use return to leave the function, so we don't go on to claim that we're connected.

If the connection succeeded, we show a "connected" notification and subscribe to the TOPIC ("doorbell"). Subscribing inside on_connect() means the subscription is restored automatically after any reconnection. Be aware that this also means the "connected" notification appears every time the client reconnects, not just the first time.

def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        notification.notify(
            title="⚠️ Doorbell connection failed",
            message=f"Could not connect to {BROKER}: {reason_code}",
            app_name="Doorbell",
            timeout=10,
        )
        return
    notification.notify(
        title="📡 Doorbell connected",
        message=f"Connected to {BROKER} as '{CLIENT_ID}', listening on '{TOPIC}'...",
        app_name="Doorbell",
        timeout=5,
    )
    client.subscribe(TOPIC)

The on_message() function runs whenever a message arrives on the topic, which in my case means the doorbell has been pressed.

  • last_ring is declared global, so the function can change the variable that lives outside it.
  • The current time is saved in now, and we subtract the time of the last ring. If the result is less than the cooldown, someone is holding down the doorbell, so we ignore the message.
  • Otherwise, last_ring is updated to the current time and we show a notification to say that there is someone at the door. Then the function ends.
def on_message(client, userdata, msg):
    global last_ring
    now = time.time()
    if now - last_ring < COOLDOWN:
        return
    last_ring = now

    notification.notify(
        title="🔔 Doorbell!",
        message="Someone is at the door.",
        app_name="Doorbell",
        timeout=10,
    )

Create the client, which is the object that represents our connection to the MQTT broker. VERSION2 tells paho that we're writing our callback functions in the newer paho 2.x style, and client_id is the name the broker sees:

client = mqtt.Client(
    callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
    client_id=CLIENT_ID,
)

Store the username and password on the client. They're sent when the connection is made:

client.username_pw_set(USERNAME, PASSWORD)

Tell the client to run on_connect() when it connects to the broker:

client.on_connect = on_connect

Tell it to run on_message() when a message is received:

client.on_message = on_message

Next, record the broker's details. connect_async() doesn't connect straight away. It makes a note of where to connect, and the real connection happens when the loop starts. The keepalive=60 argument means that if the connection has been quiet for 60 seconds, the client sends a ping to the broker to keep it alive:

client.connect_async(BROKER, PORT, keepalive=60)

Finally, start the loop. This makes the connection, handles incoming messages and automatically reconnects if the connection drops. Adding retry_first_connection=True means that if the very first attempt fails (for example, if homeassistant.local can't be found just after your laptop wakes up), it keeps trying instead of crashing:

client.loop_forever(retry_first_connection=True)

Complete code listing

import time
import paho.mqtt.client as mqtt
from plyer import notification

BROKER = "homeassistant.local"
PORT = 1883
TOPIC = "doorbell"
CLIENT_ID = "YOUR UNIQUE CLIENT ID HERE"
USERNAME = "mqtt-user"
PASSWORD = "YOUR PASSWORD HERE"
COOLDOWN = 5
last_ring = 0


def on_connect(client, userdata, flags, reason_code, properties):
    if reason_code.is_failure:
        notification.notify(
            title="⚠️ Doorbell connection failed",
            message=f"Could not connect to {BROKER}: {reason_code}",
            app_name="Doorbell",
            timeout=10,
        )
        return
    notification.notify(
        title="📡 Doorbell connected",
        message=f"Connected to {BROKER} as '{CLIENT_ID}', listening on '{TOPIC}'...",
        app_name="Doorbell",
        timeout=5,
    )
    client.subscribe(TOPIC)

def on_message(client, userdata, msg):
    global last_ring
    now = time.time()
    if now - last_ring < COOLDOWN:
        return
    last_ring = now

    notification.notify(
        title="🔔 Doorbell!",
        message="Someone is at the door.",
        app_name="Doorbell",
        timeout=10,
    )


client = mqtt.Client(
    callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
    client_id=CLIENT_ID,
)
client.username_pw_set(USERNAME, PASSWORD)
client.on_connect = on_connect
client.on_message = on_message

client.connect_async(BROKER, PORT, keepalive=60)
client.loop_forever(retry_first_connection=True)

Testing it

As I write this, it is early November and the Northern Hemisphere is in Autumn / Winter and going out to test a doorbell is far too much like hard work. So, instead do it all from a second terminal!

You will need to install mosquitto-clients via your package manager sudo apt install mosquitto-clients and use the app to publish an empty message to the topic:

mosquitto_pub -h homeassistant.local -u mqtt-user -P "YOUR PASSWORD HERE" -t doorbell -n

So, There We Have It!

The first "proper" Tuesday Tooling in some time. I hope that you have fun with Plyer.

Happy Hacking!