Python library for cross-platform desktop notifications
—
Data classes and types for constructing rich, interactive notifications with buttons, reply fields, media attachments, and custom styling options.
The main data class representing a complete desktop notification with all possible configuration options and interaction handlers.
@dataclass(frozen=True)
class Notification:
title: str
"""Notification title"""
message: str
"""Notification message body"""
urgency: Urgency = field(default=Urgency.Normal, repr=False)
"""
Notification urgency level affecting appearance and behavior.
Determines stickiness, visual styling, and ability to break through
do-not-disturb settings.
"""
icon: Icon | None = field(default=None, repr=False)
"""Custom icon for this notification (overrides app default)"""
buttons: tuple[Button, ...] = field(default_factory=tuple, repr=False)
"""Interactive buttons for user actions (platform limits apply)"""
buttons_dict: immutabledict[str, Button] = field(
default_factory=immutabledict, init=False, repr=False, compare=False
)
"""Buttons indexed by identifier for lookup (computed automatically)"""
reply_field: ReplyField | None = field(default=None, repr=False)
"""Text input field for user responses"""
on_dispatched: Callable[[], Any] | None = field(default=None, repr=False)
"""Callback when notification is sent to notification server"""
on_clicked: Callable[[], Any] | None = field(default=None, repr=False)
"""Callback when notification body is clicked"""
on_dismissed: Callable[[], Any] | None = field(default=None, repr=False)
"""Callback when notification is dismissed"""
attachment: Attachment | None = field(default=None, repr=False)
"""File attachment displayed as preview (platform-dependent file types)"""
sound: Sound | None = field(default=None, repr=False)
"""Custom sound to play when notification appears"""
thread: str | None = field(default=None, repr=False)
"""
Thread identifier for grouping related notifications.
Useful for chat apps, email threads, or any sequence of related notifications.
"""
timeout: int = field(default=-1, repr=False)
"""
Display duration in seconds.
-1 uses system default, 0 makes sticky, positive values set timeout.
"""
identifier: str = field(default_factory=uuid_str)
"""
Unique notification identifier.
Generated automatically if not provided, used for callbacks and clearing.
"""Interactive button component for notifications enabling custom user actions with callback support.
@dataclass(frozen=True)
class Button:
title: str
"""
Localized button text displayed to user.
Should be concise and action-oriented (e.g., "Reply", "Mark as Read").
"""
on_pressed: Callable[[], Any] | None = None
"""
Callback function invoked when button is pressed.
Receives no arguments, use closures to capture context.
"""
identifier: str = dataclasses.field(default_factory=uuid_str)
"""
Unique button identifier for callback routing.
Generated automatically if not provided.
"""Text input field component enabling direct text responses from notifications without opening the main application.
@dataclass(frozen=True)
class ReplyField:
title: str = "Reply"
"""
Title for the reply field itself.
On macOS, this becomes the title of the button that reveals the field.
"""
button_title: str = "Send"
"""
Title of the button that submits the reply text.
Should be localized for the target user's language.
"""
on_replied: Callable[[str], Any] | None = None
"""
Callback function invoked when user submits reply.
Receives the reply text as a string argument.
"""Exception types for notification authorization and delivery failures.
class AuthorisationError(Exception):
"""
Raised when application lacks permission to send notifications.
Typically occurs on first run before user grants permission,
or if user later revokes notification permissions in system settings.
"""from desktop_notifier import Notification, Urgency, DEFAULT_SOUND
# Simple notification
notification = Notification(
title="System Alert",
message="Your backup has completed successfully",
urgency=Urgency.Normal
)
# Rich notification with all options
rich_notification = Notification(
title="New Message",
message="John Doe: Hey, are you available for a quick call?",
urgency=Urgency.Critical,
sound=DEFAULT_SOUND,
thread="chat_john_doe",
timeout=30,
on_clicked=lambda: print("User clicked the notification"),
on_dismissed=lambda: print("User dismissed the notification")
)from desktop_notifier import Notification, Button, Urgency
def handle_approve():
print("User approved the request")
# Add approval logic here
def handle_deny():
print("User denied the request")
# Add denial logic here
# Create notification with action buttons
notification = Notification(
title="Permission Request",
message="App wants to access your camera. Allow access?",
urgency=Urgency.Critical,
buttons=(
Button(title="Allow", on_pressed=handle_approve),
Button(title="Deny", on_pressed=handle_deny),
),
timeout=60 # Give user time to respond
)from desktop_notifier import Notification, ReplyField, Button
def handle_reply(reply_text: str):
print(f"User replied: {reply_text}")
# Process the reply (send to chat, save to database, etc.)
def handle_mark_read():
print("Message marked as read")
# Notification with reply capability (like messaging apps)
message_notification = Notification(
title="Sarah Connor",
message="The meeting has been moved to 3 PM. Can you make it?",
buttons=(
Button(title="Mark as Read", on_pressed=handle_mark_read),
),
reply_field=ReplyField(
title="Reply",
button_title="Send",
on_replied=handle_reply
),
thread="chat_sarah_connor"
)from desktop_notifier import Notification, Attachment
from pathlib import Path
# Notification with image preview
image_notification = Notification(
title="Photo Shared",
message="New photo from your camera roll",
attachment=Attachment(path=Path("/path/to/photo.jpg")),
on_clicked=lambda: print("User wants to view the photo")
)
# Notification with document attachment
document_notification = Notification(
title="Document Ready",
message="Your report has been generated",
attachment=Attachment(uri="file:///path/to/report.pdf")
)from desktop_notifier import DesktopNotifier, Notification
import asyncio
async def send_conversation_notifications():
notifier = DesktopNotifier(app_name="Chat App")
# All notifications with same thread ID will be grouped together
thread_id = "conversation_team_alpha"
await notifier.send_notification(Notification(
title="Team Alpha",
message="Alice: Has everyone reviewed the proposal?",
thread=thread_id
))
await asyncio.sleep(2)
await notifier.send_notification(Notification(
title="Team Alpha",
message="Bob: I'm still going through it, need 10 more minutes",
thread=thread_id
))
await asyncio.sleep(2)
await notifier.send_notification(Notification(
title="Team Alpha",
message="Carol: Looks good to me, just one minor suggestion",
thread=thread_id
))
asyncio.run(send_conversation_notifications())from desktop_notifier import DesktopNotifier, Notification
import asyncio
class NotificationHandler:
def __init__(self, app_name: str):
self.notifier = DesktopNotifier(app_name=app_name)
self.active_notifications = {}
async def send_with_tracking(self, title: str, message: str):
notification = Notification(
title=title,
message=message,
on_dispatched=self.on_notification_sent,
on_clicked=self.on_notification_clicked,
on_dismissed=self.on_notification_dismissed
)
notification_id = await self.notifier.send_notification(notification)
self.active_notifications[notification_id] = {
'title': title,
'sent_at': asyncio.get_event_loop().time()
}
return notification_id
def on_notification_sent(self):
print("Notification successfully displayed to user")
def on_notification_clicked(self):
print("User interacted with notification")
def on_notification_dismissed(self):
print("User dismissed notification")
# Usage
async def main():
handler = NotificationHandler("Tracking App")
await handler.send_with_tracking(
"Task Complete",
"Your data processing job has finished"
)
asyncio.run(main())Install with Tessl CLI
npx tessl i tessl/pypi-desktop-notifier