Python: Windows Toast Notifications

Python: Windows Toast Notifications

·

10 min read

A major part of my work is creating spreadsheets. With Python, I can make these over and over again much quicker than manually doing the edits in Excel. Some of the scripts can take some time to run. I decided to add notifications to let me know the script is finished. And more importantly, adding notifications when the script has issues.

Doing a search on PyPI provides a long list of packages available. After looking at quite a few, I tried narrowing it down based on when was the last update, how active the developers were and how well the documentation was. Three packages stood out.

For the most part each package handles the same toasts and have well laid out documentation. Below I'll show a few common toasts between the three packages and also show a couple of features that each package can shine with.

Installing

Using pip is a common method of installation.

pip install win11toast
pip install Windows-Toasts
pip install winsdk-toast

You only need to include the package you decide to go with. 😀

Win11toast

This has good documentation and is kept updated.

from win11toast import toast

Jupyter Notebook

For Win11toast to work with Jupyter Notebook it takes a little change. Instead of using toast you'll need to import and use toast_async instead of toast.

from win11toast import toast_async

await toast_async('Hello Python', 'Click to open url', on_click='https://www.python.org')

Basic

A quick notification will include a title and message. Everything else will be set to defaults. If the message is long, it'll wrap around.

toast('Hello Python🐍', 
        'Lorem ipsum dolor sit amet, consectetur adipisicing elit. Earum accusantium porro numquam aspernatur voluptates cum, odio in, animi nihil cupiditate molestias laborum. Consequatur exercitationem modi vitae. In voluptates quia obcaecati!')

Adding an Icon

Icons can be added by specifying the absolute location.

toast('Hello Python🐍',
      'An Icon is displayed',
      icon=str(Path('Smile_24px.ico').resolve()))

The Smile_24px.ico file is in the same directory as the Python file. I'm using Path to select a relative location of a file and convert it to absolute. Path will also need to be imported.

Playing a Windows Sound

Most of these packages use the standard Windows Sounds. For Win11toast you'll use the Window's name of the sound. Microsoft - Element Audio Docs lists out the sounds and you can pick any of them.

toast('Hello', 'Hello from Python', audio='ms-winsoundevent:Notification.Looping.Alarm')

For those times you don't want even the default sound, silent can be set to true.

toast('Hello Python🐍', audio={'silent': 'true'})

Playing a sound file

Win11toast has the extra ability to play a sound file either from the local drive or from a URL.

toast('Hello Python🐍', 
    'Play a sound file', 
    audio=r'c:\Users\Admin\Downloads\MUS_Mysterious_Stranger_A_0001.mp3'

Using Windows Speech

Windows has Speech built directly in. It's been there for quite a few versions. Win11toast has an attribute that lets you pass text for Windows to say out loud.

toast('Hello Python🐍', 'Did you hear it?', dialogue='Let Windows do the talking.')

It's common today to turn off the notification's sounds and Windows Speech. While I really love this feature, I won't rely on it.

Click on the notification

Besides passing along information, another very common use of the notification is to let the user trigger a new step. By setting the on_click attribute to a string with a URL, Windows will attempt to open it in the default browser.

toast('Hello Python🐍', 
        'Click toast to open url', 
        on_click='https://www.python.org')

Adding a Button

Sometimes having a button, just looks more friendly. Adding a dismiss button is quick.

toast('Hello', 'Hello from Python', button='Dismiss')

To get the button to do something else just takes a dictionary of what we want.

toast('Hello', 'Hello from Python', 
      button={'activationType': 'protocol', 'arguments': 'https://google.com', 'content': 'Open Google'})

In this example we'll have two buttons. The first with the label of Play will open the file in the argument in the default application. The second button will open the URL in the default application. Since that is a location on the drive, it should open the File Explorer.

buttons = [
    {'activationType': 'protocol', 'arguments': 'C:\Windows\Media\Alarm01.wav', 'content': 'Play'},
    {'activationType': 'protocol', 'arguments': 'file:///C:/Windows/Media', 'content': 'Open Folder'}
]

toast('Music Player', 'Download Finished', buttons=buttons)

Dropdown Selection

The selection argument will create a list of options to select.

toast('Hello', 'Which do you like?', 
                        selection=['Apple', 'Banana', 'Grape'], 
                        button='Submit',
                        on_click=lambda args: print('clicked!', args))

When I selected Banana the args comes back as {'arguments': 'http:', 'user_input': {'selection': 'Banana'}}.

Windows-Toasts

Windows-Toasts more separate steps to build and set off a toast notification.

This package worked quite while inside Jupyter Lab and Jupyter Notebook. If you need it to run in Jupyter Notebook this would be the package for easiest starting.

import windows_toasts

Basic

As shown in even the basic toast, it takes a few steps to get the basic toast out. I think the advantage would be in more complex toasts.

# Create a Windows Toast Object
wintoaster = windows_toasts.WindowsToaster('Hello Python🐍')
newToast = windows_toasts.Toast()

# Add text to the Toast
newToast.text_fields = ['Hello, World!']

wintoaster.show_toast(newToast)

Clickable toast

With this Clickable Toast we're adding an action to occur if the toast is clicked. Clicking the x or letting the toast expire will not execute the action.

The example is using a lambda expression but could be a function as well.

wintoaster = windows_toasts.WindowsToaster('Hello Python🐍')
newToast = windows_toasts.Toast()
newToast.text_fields = ['Hello, World!']

# Add an action to clicking the Toast
newToast.on_activated = lambda _: print('Toast clicked!')

wintoaster.show_toast(newToast)

Adding an Image

Adding an image uses the ToastDisplayImage function.

from pathlib import Path

toaster = windows_toasts.WindowsToaster('Windows-Toasts')

newToast = windows_toasts.Toast()
newToast.text_fields = ['Look an image']

# str or PathLike
newToast.AddImage(ToastDisplayImage.fromPath(str(Path('Coffe-To-Go_24px.png').resolve())))

toaster.show_toast(newToast)

Playing a Windows Sound

There is a set list of sounds that are accessible for Windows Toast. The list is available in the documentation

toaster = WindowsToaster('Windows-Toasts')
newToast = windows_toasts.Toast()
newToast.text_fields = ['Ding ding! Ding ding! Ding ding!']

newToast.audio = ToastAudio(AudioSource.IM, looping=True)

toaster.show_toast(newToast)

Buttons on the notification

Here we are adding 2 buttons and a on_activated attribute that contains a function to be called and the response is passed in. Instead of a lambda any function could be used. When either button is clicked the same function is executed. Using the reponse.arguments we can access the string value passed to ToastButton.

toaster = InteractableWindowsToaster('Hello Python🐍')
newToast = windows_toasts.Toast(['Hello, how are you?'])

# Add two actions (buttons)
newToast.AddAction(ToastButton(content='Decent', arguments='decent'))
newToast.AddAction(ToastButton('Not so good', 'bad'))

newToast.on_activated = lambda response: print(response.arguments)

# Send it
toaster.show_toast(newToast)

The value in response.arguments will be the value of arguments in the AddAction method call. This will let you know which button was clicked.

Launch a Website

Opening website is very common so there is a bit of a shortcut.

toaster = WindowsToaster('Rick Astley')

newToast = windows_toasts.Toast()
newToast.text_fields = ['Hello there! You just won a thousand dollars! Click me to claim it!']

newToast.launch_action = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'

# Send it
toaster.show_toast(newToast)

Scheduled Notification

If you need to schedule a notification at a specific time, Windows-Toast has the option. Because it's passing the notification directly to Windows, the Python script does not need to be running. I tried it with a Launch_action and it still was able to continue the action. I don't know how much action it can delay. If you find a limit (or get it to do a lot of actions) comment below.

from datetime import datetime, timedelta

toaster = windows_toasts.WindowsToaster('Python')

displayTime = datetime.now() + timedelta(seconds=20)

newToast = windows_toasts.Toast([f'This will pop up at {displayTime}'])

newToast.text_fields = ['Hello there! You just won a thousand dollars! Click me to claim it!']
newToast.launch_action = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'

toaster.schedule_toast(newToast, displayTime)

Winsdk_toast

Winsdk_toast doesn't have the best of documentation. A lot of how I learned to use this is from studying the code. But I do like the methods that are used.

Unlike the others, this package didn't work in Jupyter Notebook.

from winsdk_toast import Notifier, Toast
from winsdk_toast.event import EventArgsActivated, EventArgsDismissed, EventArgsFailed

Basic

For each .add_text() used a new line of text will be added.

# Step1. Create Notifier with applicationId
notifier = Notifier("Hello Python🐍")

# Step2. Create Toast which contains the message to be shown
toast = Toast()
toast.add_text("Simple toast, just notifies.")

# Step3. Show the Toast
notifier.show(toast)

Adding an image

notifier = Notifier("Hello Python🐍")
toast = Toast()
toast.add_text("Add Icon")

# Add image to the Toast
toast.add_image(str(Path("Smile_24px.ico").resolve()))

notifier.show(toast)

Playing a Windows Sound

Most of these packages use the standard Windows Sounds. For winsdk-toasts you'll use the Window's name of the sound. Microsoft - Element Audio Docs lists out the sounds and you can pick any of them.

notifier = Notifier("Hello Python🐍")
toast = Toast()
toast.add_text("Add Sound")

# Add sound to the Toast
toast.set_audio(src="ms-winsoundevent:Notification.Looping.Alarm")

notifier.show(toast)

Passing silent='true' will silence the notification.

notifier = Notifier("Hello Python🐍")
toast = Toast()
toast.add_text("Silent")
toast.set_audio(silent='true')
notifier.show(toast)

Button on Notification

The .add_action() method will add a button to the Toast.

notifier = Notifier("Hello Python🐍")
toast = Toast()
toast.add_text("The script is finished.")

# Add button to the Toast
toast.add_action('OK')

notifier.show(toast)

Adding events

Here's the part where this package really is different than the other two.

There are three possible endings to a notification:

  • activated - It was clicked on
  • dismissed - the x was clicked, or it expired.
  • failed - Something went wrong, and the toast didn't work correctly

Each of these possibilities can have a method that will run. These functions are specified in the notifier.show() call. Only the functions that you want to use need to be overridden.

# This will run if the button is clicked.
def handle_activated(event_args_activated: EventArgsActivated):
    print('inputs:', event_args_activated.inputs)
    print('argument:', event_args_activated.argument)

# This will run if the `x` is clicked or the notification times out.
def handle_dismissed(event_args_dismissed: EventArgsDismissed):
    print('dismissed')
    print('reason:', event_args_dismissed.reason)

# This will run if the notification failed/errored
def handle_failed(event_args_failed: EventArgsFailed):
    print('failed')
    print('error_code:', event_args_failed.error_code)

notifier = Notifier("Hello Python🐍")
toast = Toast()
toast.add_text('What up?', hint_align='center', hint_style='caption')

# Add input Box
toast.add_input(type_='text', id_='input_name')

toast.add_action('Close')

# The functions and the attributes do not have to match.
notifier.show(
    toast, 
    handle_activated=handle_activated, 
    handle_dismissed=handle_dismissed, 
    handle_failed=handle_failed
)

Also we add here is the .add_input(). This adds an input box the user can enter data. Inside the handle_activated will be passed a dictionary that will contain {'input_name': '/userinput/'}.

  • input_name - the id_ from the .add_input() statement
  • /userinput/ - whatever the user entered in the input field

Multiple fields can be added to the Toast.

While working learning how this package worked, I was able to show multiple buttons. But I wasn't able to identify which button was clicked. The next section goes over a Drop Down Selection. This is a very nice way to have the user select different options.

Drop down selection

Adding multiple input fields is great, but sometimes we want the user to choose from a preselected set of answers. That's where .add_selection() comes to play.

def notifyActiveHandler(event_args_activated: EventArgsActivated):
    print('activated')
    print('inputs:', event_args_activated.inputs)
    print('argument:', event_args_activated.argument)

def handle_dismissed(event_args_dismissed: EventArgsDismissed):
    print('dismissed')
    print('reason:', event_args_dismissed.reason)

notifier = Notifier("Hello Python🐍")
toast = Toast()
toast.add_text('Which topping do you want?', hint_align='center', hint_style='caption')
toast.add_input('selection', id_='topping')

# Add Selection items for the input box
toast.add_selection(content='Apple', id_='apple')
toast.add_selection(content='Chocolate', id_='chocolate')
toast.add_selection(content='Sugar', id_='sugar')
toast.add_selection(content='Whipped Cream', id_='whipped_cream')

toast.add_action('Pick!')

# Step3. Show the Toast
notifier.show(toast, 
              handle_activated=notifyActiveHandler, 
              handle_dismissed=handle_dismissed
             )

Creed

While getting advice concerning this article, I was asked about Linux and macOS. I did find the Python package Creed. It only handles a basic toast but it checks the OS and provides the proper toast for that OS.

For Windows it's using win10toast which is an old version of win11toast. For MacOS and Linux it makes a call directly to the OS to create the Toast.

from creed import Notif

Notif(title="Python", message="Hello, World", duration=20).Toast()

Conclusion

This is by no means an exhaustive look at each of these packages. I tried to pick situations that might be useful. I've decided to go with win11toast. I found it a simpler syntax and gave all the features that I needed.

If you are favoring a different package or found a feature I should have listed, please comment below.

Resources

These are some of the links I studied for this post.

Did you find this article valuable?

Support Russ Eby by becoming a sponsor. Any amount is appreciated!