Skip to main content

An ultra simple, modern pub/sub library and blinker alternative for Python

Project description

EZPubSub

badge badge badge badge badge

A tiny, modern alternative to Blinker – typed, thread-safe, sync or async, and designed for today’s Python.

EZPubSub is a zero-dependency pub/sub library focused on one thing: making event publishing and subscribing easy, safe, and predictable. No over-engineered, confusing API. No unnecessary features. Just clean, easy pub/sub that works anywhere.

The core design is inspired by the internal signal system in Textual, refined into a standalone library built for general use.

Quick Start

Synchronous signal:

from ezpubsub import Signal

data_signal = Signal[str]()

def on_data(data: str) -> None:
    print("Received:", data)

data_signal.subscribe(on_data)
data_signal.publish("Hello World")
# Output: Received: Hello World

Asynchronous signal with callback:

from ezpubsub import Signal

async_data_signal = Signal[str]("My Database Update")

async def on_async_data(data: str) -> None:
    await asyncio.sleep(1)  # Simulate async work
    print("Async Received:", data)

async_data_signal.asubscribe(on_async_data)
await async_data_signal.apublish("Hello Async World")
# Output: Async Received: Hello Async World

That’s it. You create a signal, subscribe to it, and publish events.

Why Another Pub/Sub Library?

Because pub/sub in Python is either old and untyped or overengineered and async-only.

Writing a naive pub/sub system is easy. Just keep a list of callbacks and fire them. Writing one that actually works in production is not. You need to handle thread safety, memory management (weak refs for bound methods), error isolation, subscription lifecycles, and type safety. Most libraries get at least one of these wrong.

The last great attempt was Blinker, 15 years ago. It was excellent for its time, but Python has moved on. EZPubSub is what a pub/sub library should look like in 2025: type-safe, thread-safe, ergonomic, and designed for modern Python.

Features

  • Thread-Safe by Default – Publish and subscribe safely across threads.
  • Strongly Typed with GenericsSignal[str], Signal[MyClass], or even TypedDict/dataclasses for structured events. Pyright/MyPy catches mistakes before runtime.
  • Sync or Async – Works in any environment, including mixed sync/async projects.
  • Automatic Memory Management – Bound methods are weakly referenced and auto-unsubscribed when their objects are deleted.
  • No Runtime Guesswork – No **kwargs, no stringly-typed namespaces, no dynamic channel lookups. Opinionated design that enforces type safety and clarity.
  • Lightweight & Zero Dependencies – Only what you need.

How It Compares

EZPubSub vs Blinker

Blinker is great for simple, single-threaded Flask-style apps. But:

Feature EZPubSub Blinker
Design ✅ Instance-based, type-safe ⚠️ Channel-based (runtime filtering, string keys)
Weak Refs ✅ Automatic ✅ Automatic
Type Checking ✅ Full static typing (Signal[T]) ❌ Untyped (Any)
Thread Safety ✅ Built-in ❌ Single-threaded only

If you’re starting a new project in 2025, you deserve type checking and thread safety out of the box.

EZPubSub vs AioSignal

aiosignal is excellent for its niche—managing fixed async callbacks inside aiohttp—but unsuitable as a general pub/sub system:

Feature EZPubSub AioSignal
Sync and Async ✅ Sync and Async friendly ❌ Sync publishing not available
Freezing Subscribers ✅ Optional in both Sync and Async freeze() required to publish; no dynamic add/remove at runtime
Type Checking ✅ Full static typing (Signal[T]) ⚠️ Allows arbitrary **kwargs, undermining type safety
Thread Safety ✅ Built-in ❌ Single-threaded only

aiosignal is great if you’re writing an aiohttp extension. But being required to use async everywhere just to publish signals is unnecessary for most applications. Synchronous first, with optional async support, is simpler and more predictable. That’s why Blinker, Celery, and Django's internal PubSub (based on PyDispatcher) all share this design.

Design Philosophy

Signals vs Channels

EZPubSub uses one object per signal, instead of Blinker’s “one channel, many signals” model.

Blinker (channel-based):

user_signal = Signal()  
user_signal.connect(login_handler, sender=LoginService)
user_signal.send(sender=LoginService, user=user)

EZPubSub (instance-based):

login_signal = Signal[LoginEvent]("user_login")
login_signal.subscribe(login_handler)
login_signal.publish(LoginEvent(user=user))

This matters because:

  • No filtering – Each signal already represents one event type.
  • No runtime lookups – You never hunt down signals by string name.
  • Type safety – Wrong event types are caught by your IDE/type checker.

Fewer magic strings, fewer runtime bugs, and code that reads like what it does.

Why No **kwargs?

Allowing arbitrary keyword arguments is convenient, but it destroys type safety.

# Bad: fragile, stringly typed
signal.publish(user, session_id="abc123", ip="1.2.3.4")

# Good: explicit, type-safe
@dataclass
class UserLoginEvent:
    user: User
    session_id: str
    ip: str

signal.publish(UserLoginEvent(user, "abc123", "1.2.3.4"))

This forces better API design and catches mistakes at compile time instead of runtime. EZPubSub is opinionated about this: no **kwargs. Every signal has a specific type.

It is of course possible to simply not use any type hinting when creating a signal (or use Any), as type hints in Python are optional. But the library is designed to encourage type safety by default. As for why you would ever want to create a signal using Any or without a type hint, I won't ask any questions ;)

Installation

pip install ezpubsub

Or with UV:

uv add ezpubsub

Requires Python 3.10+.


Documentation

Full docs: Click here


License

MIT License. See LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ezpubsub-0.3.0.tar.gz (6.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ezpubsub-0.3.0-py3-none-any.whl (7.5 kB view details)

Uploaded Python 3

File details

Details for the file ezpubsub-0.3.0.tar.gz.

File metadata

  • Download URL: ezpubsub-0.3.0.tar.gz
  • Upload date:
  • Size: 6.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for ezpubsub-0.3.0.tar.gz
Algorithm Hash digest
SHA256 5242b2229561e96f0903163807170560d183afe8e601011c5f32ebf43535f0d5
MD5 daea840d21bcf94da7578d15721f29c0
BLAKE2b-256 7e9df0ba8b794d315cd0fb00f2d03e2036c8301afc678aabd72d8931aca4b28a

See more details on using hashes here.

Provenance

The following attestation bundles were made for ezpubsub-0.3.0.tar.gz:

Publisher: release.yml on edward-jazzhands/ezpubsub

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file ezpubsub-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: ezpubsub-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 7.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.12.9

File hashes

Hashes for ezpubsub-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 82af16d3bc3df43c58ab1fc99d0afd20457dd292ce34569fd12d30f1353c209a
MD5 707288566eabc77b5052491335865e39
BLAKE2b-256 642913f734871548d821eebd51b5ddedef29f9c2791d83dc910347c6411b7c19

See more details on using hashes here.

Provenance

The following attestation bundles were made for ezpubsub-0.3.0-py3-none-any.whl:

Publisher: release.yml on edward-jazzhands/ezpubsub

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page