# EZPubSub
[](https://astral.sh/ruff)
[](https://github.com/psf/black)
[-blue?style=for-the-badge&logo=python)](https://mypy-lang.org/)
[-blue?style=for-the-badge&logo=python)](https://microsoft.github.io/pyright/)
[](https://opensource.org/license/mit)
[](https://textual.textualize.io/)
A tiny, modern alternative to [Blinker](https://github.com/pallets-eco/blinker) – typed, thread-safe, and built for today’s Python.
EZpubsub is an ultra-simple pub/sub library for Python. Its only goal is to make publishing and subscribing to events easy and safe. No async complexity, no extra features, no dependencies. Just clean, synchronous pub/sub that works anywhere.
The core design is inspired by the internal pub/sub system used in Textual, [Will McGugan](https://willmcgugan.github.io/)’s TUI framework. Will is one of the world’s foremost Python experts, and his internal implementation is the cleanest I’ve ever seen. EZpubsub takes those same lessons and distills them into a standalone, cross-framework library you can drop into any project.
## Features
- Thread-Safe by Default – Safe to publish and subscribe across threads.
- Strongly Typed with Generics – Signals are fully generic (`Signal[str]`, `Signal[MyClass]`), letting Pyright/MyPy catch mistakes before runtime. This also unlocks powerful combinations with Typed Objects as signal types.
- Synchronous by Design – 100% sync to keep things predictable. Works fine in async projects.
- Automatic Memory Management – Bound methods are weakly referenced and automatically unsubscribed when their objects are deleted. Normal functions are strongly referenced and must be manually unsubscribed.
- Lightweight & Zero Dependencies – Minimal API, no legacy baggage, designed for 2025-era Python.
## Why ezpubsub / Project philosophy
### Why Build Another Pub/Sub Library?
Pub/sub is one of those deceptively simple patterns that suffers from "everyone should just roll their own" syndrome. It's easy to write a basic working version in 20 lines – just maintain a list of callbacks and call them when something happens. This apparent simplicity has led to dozens of half-baked implementations across the Python ecosystem.
The problem is that building a good pub/sub library requires handling a surprising number of edge cases that only surface with experience: thread safety, memory management, error isolation, subscription lifecycle, weak references, exception handling, and type safety. These aren't obvious when you're sketching out the basic concept.
ezpubsub may be only 167 lines, but every line is deliberate. It's the result of encountering all the ways simpler implementations break in production: memory leaks from orphaned bound methods, race conditions in threaded applications, cascading failures when one subscriber throws an exception, and the endless debugging sessions that come with untyped event data.
The real tragedy is that nobody has seriously attempted to build the right tool. Blinker was the last good effort, but it's 15 years old and shows its age. Everything else falls into two categories: either thrown-together weekend projects that clearly weren't meant to be production-ready, or horrifically over-engineered monstrosities that have a very complex and confusing API, the vast majority of which is unrelated to the core goal of just subscribing to an event.
So I thought, hey I build libraries, why not build a pub/sub library that actually works well in 2025? One that is simple, modern, and designed for the way we write Python today. ezpubsub is that library.
### Why not just use Blinker?
Blinker is an excellent, battle-tested library. If you’re writing a simple, single-threaded, synchronous app (e.g., Flask extensions), Blinker is still a great choice.
However, ezpubsub was designed as a modern alternative:
1. **Full Static Typing with Generics**
Blinker’s signals are effectively untyped (Any everywhere). ezpubsub’s `Signal[T]` lets Pyright/MyPy enforce that subscribers receive the correct data type at development time, as well as unlocks powerful combinations with Typed Objects as signal types. This makes it much easier to catch mistakes before they happen, rather than at runtime.
2. **Thread-Safe by Default**
Blinker assumes single-threaded execution. ezpubsub uses proper locking, making it safe in threaded or mixed sync/async environments.
3. **Type Safety Over Dynamic Namespaces**
Blinker’s string-based namespaces allow arbitrary signal creation (`ns.signal("user_created")`), but at the cost of type safety—there’s nothing stopping you from accidentally publishing the wrong object type. ezpubsub treats each signal as an explicitly typed object (`Signal[User]`), making such mistakes enforced at compile time instead of runtime.
### Why Not Just Use One of the Other Libraries?
There are dozens of pub/sub libraries on PyPI, but almost all of them fall into two camps: ancient untyped code that hasn’t been maintained in years, or modern ‘async-first’ libraries that are overengineered and awkward to use for simple event dispatch. Here’s why ezpubsub exists instead of just recommending one of these.
| Library | Why Not? |
| --------------------------- | ---------------------------------------------------------------------------------------------------- |
| **Blinker** | Great for simple Flask-style apps, but assumes single-threaded execution and no static typing. |
| **PyDispatcher** | Unmaintained, completely untyped, API hasn’t been touched in years. |
| **aiopubsub** | Overengineered, async-first, and requires a full asyncio setup even for simple use cases. |
| **aiosubpub** | Forces subscriptions to be asyncio tasks, making ergonomics painful for mixed sync/async projects. |
| **[Some other small libs]** | Typically 50–100 lines, but missing critical things like weakrefs, thread safety, error isolation, and documentation |
| | |
### Why not use "async-first" pub/sub libraries?
There are dozens of tiny “AIO pub/sub” libraries on GitHub. I was personally not satisfied with any of them for these reasons:
1. **Async should not be the core mechanism**
Pub/sub is just a dispatch mechanism. Whether you call a subscriber directly or schedule it on an event loop is application logic. Some people might say this is a hot take, but I believe in it. It's not terrible to include async support as an option, but it should not be the primary focus of a pub/sub library. The sender of a signal can simply await the external data it needs and then send the signal when ready. There's no particular advantage to awaiting the callback itself, and it just adds unnecessary complexity to the API.
2. **Async-First Usually Means Bad Ergonomics**
These libraries often force you into awkward patterns: creating tasks for every subscription, manual event loop juggling, weird API naming. There's no practical benefit to taking up more of your mental real estate.
There is a reason that the most popular pub/sub libraries in the Python ecosystem (blinker, Celery, PyDispatcher, etc) are all synchronous at their core. It’s the simplest, most predictable way to do pub/sub. Async-first versions, in my humble opinion, are [reinventing the square wheel](https://exceptionnotfound.net/reinventing-the-square-wheel-the-daily-software-anti-pattern/).
I would certainly be open to implementing some very simple async support in future versions (As of writing this it's only 0.1.0!), but it would be an optional feature, and need to follow the same principles of simplicity and ergonomics as the rest of the library.
### Comparison table - ezpubsub vs Blinker
| Feature | ezpubsub | blinker | Category |
| ------------------------- | ------------------------ | --------------- | --------------- |
| Thread-Safe by Default | ✅ Yes | ❌ No | Core Philosophy |
| Generically Typed Payload | ✅ Yes (Signal[T]) | ❌ No (**kwargs, Any) | Core Philosophy |
| Weak-Reference Support | ✅ Yes | ✅ Yes | Core Philosophy |
| Sender-Specific Filtering | ❌ No (Not planned) | ✅ Yes | Core (Blinker) |
| Namespacing | ❌ No (Not planned) | ✅ Yes | Core (Blinker) |
| Async Support | ❌ Possible future update | ✅ Yes | Nice-to-have |
| Decorator API | ❌ Possible future update | ✅ Yes | Nice-to-have |
| Context Managers | ❌ Possible future update | ✅ Yes | Nice-to-have |
| Metasignals (on connect) | ❌ Possible future update | ✅ Yes | Nice-to-have |
## Requirements
- Python 3.10 or higher
- Optional: Enable type checking with [Pyright](http://pyright.org), [MyPy](http://mypy-lang.org), or your checker of choice to get the full benefits of static typing and generics.
## Installation
Install from PyPI:
```sh
pip install ezpubsub
```
Or, with [UV](https://github.com/astral-sh/uv):
```sh
uv add ezpubsub
```
## Quick Start
Create a `Signal` instance, subscribe to it, and publish data:
```py
from ezpubsub import Signal
data_signal = Signal[str](name="data_updated")
def my_callback(data: str) -> None:
print("Received data:", data)
data_signal.subscribe(my_callback)
data_signal.publish("Hello World")
# Output: Received data: Hello World
```
## Documentation
### [Click here for full documentation](https://edward-jazzhands.github.io/libraries/ezpubsub/docs/)
## Questions, Issues, Suggestions?
Use the [issues](https://github.com/edward-jazzhands/ezpubsub/issues) section for bugs or problems, and post ideas or feature requests on the [discussion board](https://github.com/edward-jazzhands/ezpubsub/discussions).
## License
MIT License. See [LICENSE](LICENSE) for details.
Raw data
{
"_id": null,
"home_page": null,
"name": "ezpubsub",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": null,
"keywords": "python, pubsub, publish-subscribe, typed, thread-safe, event-driven, events",
"author": "Edward Jazzhands",
"author_email": "Edward Jazzhands <ed.jazzhands@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/7a/58/68e5384aa5a53dcf84638477d47f33dee906ee43f7d8f144852f9ad7ee61/ezpubsub-0.1.2.tar.gz",
"platform": null,
"description": "# EZPubSub\n\n[](https://astral.sh/ruff)\n[](https://github.com/psf/black)\n[-blue?style=for-the-badge&logo=python)](https://mypy-lang.org/)\n[-blue?style=for-the-badge&logo=python)](https://microsoft.github.io/pyright/)\n[](https://opensource.org/license/mit)\n[](https://textual.textualize.io/)\n\nA tiny, modern alternative to [Blinker](https://github.com/pallets-eco/blinker) \u2013 typed, thread-safe, and built for today\u2019s Python.\n\nEZpubsub is an ultra-simple pub/sub library for Python. Its only goal is to make publishing and subscribing to events easy and safe. No async complexity, no extra features, no dependencies. Just clean, synchronous pub/sub that works anywhere.\n\nThe core design is inspired by the internal pub/sub system used in Textual, [Will McGugan](https://willmcgugan.github.io/)\u2019s TUI framework. Will is one of the world\u2019s foremost Python experts, and his internal implementation is the cleanest I\u2019ve ever seen. EZpubsub takes those same lessons and distills them into a standalone, cross-framework library you can drop into any project.\n\n## Features\n\n- Thread-Safe by Default \u2013 Safe to publish and subscribe across threads.\n- Strongly Typed with Generics \u2013 Signals are fully generic (`Signal[str]`, `Signal[MyClass]`), letting Pyright/MyPy catch mistakes before runtime. This also unlocks powerful combinations with Typed Objects as signal types.\n- Synchronous by Design \u2013 100% sync to keep things predictable. Works fine in async projects.\n- Automatic Memory Management \u2013 Bound methods are weakly referenced and automatically unsubscribed when their objects are deleted. Normal functions are strongly referenced and must be manually unsubscribed.\n- Lightweight & Zero Dependencies \u2013 Minimal API, no legacy baggage, designed for 2025-era Python.\n\n## Why ezpubsub / Project philosophy\n\n### Why Build Another Pub/Sub Library?\n\nPub/sub is one of those deceptively simple patterns that suffers from \"everyone should just roll their own\" syndrome. It's easy to write a basic working version in 20 lines \u2013 just maintain a list of callbacks and call them when something happens. This apparent simplicity has led to dozens of half-baked implementations across the Python ecosystem.\n\nThe problem is that building a good pub/sub library requires handling a surprising number of edge cases that only surface with experience: thread safety, memory management, error isolation, subscription lifecycle, weak references, exception handling, and type safety. These aren't obvious when you're sketching out the basic concept.\n\nezpubsub may be only 167 lines, but every line is deliberate. It's the result of encountering all the ways simpler implementations break in production: memory leaks from orphaned bound methods, race conditions in threaded applications, cascading failures when one subscriber throws an exception, and the endless debugging sessions that come with untyped event data.\n\nThe real tragedy is that nobody has seriously attempted to build the right tool. Blinker was the last good effort, but it's 15 years old and shows its age. Everything else falls into two categories: either thrown-together weekend projects that clearly weren't meant to be production-ready, or horrifically over-engineered monstrosities that have a very complex and confusing API, the vast majority of which is unrelated to the core goal of just subscribing to an event.\n\nSo I thought, hey I build libraries, why not build a pub/sub library that actually works well in 2025? One that is simple, modern, and designed for the way we write Python today. ezpubsub is that library.\n\n### Why not just use Blinker?\n\nBlinker is an excellent, battle-tested library. If you\u2019re writing a simple, single-threaded, synchronous app (e.g., Flask extensions), Blinker is still a great choice.\n\nHowever, ezpubsub was designed as a modern alternative:\n\n1. **Full Static Typing with Generics** \n Blinker\u2019s signals are effectively untyped (Any everywhere). ezpubsub\u2019s `Signal[T]` lets Pyright/MyPy enforce that subscribers receive the correct data type at development time, as well as unlocks powerful combinations with Typed Objects as signal types. This makes it much easier to catch mistakes before they happen, rather than at runtime.\n2. **Thread-Safe by Default** \n Blinker assumes single-threaded execution. ezpubsub uses proper locking, making it safe in threaded or mixed sync/async environments.\n3. **Type Safety Over Dynamic Namespaces** \n Blinker\u2019s string-based namespaces allow arbitrary signal creation (`ns.signal(\"user_created\")`), but at the cost of type safety\u2014there\u2019s nothing stopping you from accidentally publishing the wrong object type. ezpubsub treats each signal as an explicitly typed object (`Signal[User]`), making such mistakes enforced at compile time instead of runtime.\n\n### Why Not Just Use One of the Other Libraries?\n\nThere are dozens of pub/sub libraries on PyPI, but almost all of them fall into two camps: ancient untyped code that hasn\u2019t been maintained in years, or modern \u2018async-first\u2019 libraries that are overengineered and awkward to use for simple event dispatch. Here\u2019s why ezpubsub exists instead of just recommending one of these.\n\n| Library | Why Not? |\n| --------------------------- | ---------------------------------------------------------------------------------------------------- |\n| **Blinker** | Great for simple Flask-style apps, but assumes single-threaded execution and no static typing. |\n| **PyDispatcher** | Unmaintained, completely untyped, API hasn\u2019t been touched in years. |\n| **aiopubsub** | Overengineered, async-first, and requires a full asyncio setup even for simple use cases. |\n| **aiosubpub** | Forces subscriptions to be asyncio tasks, making ergonomics painful for mixed sync/async projects. |\n| **[Some other small libs]** | Typically 50\u2013100 lines, but missing critical things like weakrefs, thread safety, error isolation, and documentation |\n| | |\n\n### Why not use \"async-first\" pub/sub libraries?\n\nThere are dozens of tiny \u201cAIO pub/sub\u201d libraries on GitHub. I was personally not satisfied with any of them for these reasons:\n\n1. **Async should not be the core mechanism** \n Pub/sub is just a dispatch mechanism. Whether you call a subscriber directly or schedule it on an event loop is application logic. Some people might say this is a hot take, but I believe in it. It's not terrible to include async support as an option, but it should not be the primary focus of a pub/sub library. The sender of a signal can simply await the external data it needs and then send the signal when ready. There's no particular advantage to awaiting the callback itself, and it just adds unnecessary complexity to the API.\n2. **Async-First Usually Means Bad Ergonomics** \n These libraries often force you into awkward patterns: creating tasks for every subscription, manual event loop juggling, weird API naming. There's no practical benefit to taking up more of your mental real estate.\n\nThere is a reason that the most popular pub/sub libraries in the Python ecosystem (blinker, Celery, PyDispatcher, etc) are all synchronous at their core. It\u2019s the simplest, most predictable way to do pub/sub. Async-first versions, in my humble opinion, are [reinventing the square wheel](https://exceptionnotfound.net/reinventing-the-square-wheel-the-daily-software-anti-pattern/).\n\nI would certainly be open to implementing some very simple async support in future versions (As of writing this it's only 0.1.0!), but it would be an optional feature, and need to follow the same principles of simplicity and ergonomics as the rest of the library.\n\n### Comparison table - ezpubsub vs Blinker\n\n| Feature | ezpubsub | blinker | Category |\n| ------------------------- | ------------------------ | --------------- | --------------- |\n| Thread-Safe by Default | \u2705 Yes | \u274c No | Core Philosophy |\n| Generically Typed Payload | \u2705 Yes (Signal[T]) | \u274c No (**kwargs, Any) | Core Philosophy |\n| Weak-Reference Support | \u2705 Yes | \u2705 Yes | Core Philosophy |\n| Sender-Specific Filtering | \u274c No (Not planned) | \u2705 Yes | Core (Blinker) |\n| Namespacing | \u274c No (Not planned) | \u2705 Yes | Core (Blinker) |\n| Async Support | \u274c Possible future update | \u2705 Yes | Nice-to-have |\n| Decorator API | \u274c Possible future update | \u2705 Yes | Nice-to-have |\n| Context Managers | \u274c Possible future update | \u2705 Yes | Nice-to-have |\n| Metasignals (on connect) | \u274c Possible future update | \u2705 Yes | Nice-to-have |\n\n## Requirements\n\n- Python 3.10 or higher\n- Optional: Enable type checking with [Pyright](http://pyright.org), [MyPy](http://mypy-lang.org), or your checker of choice to get the full benefits of static typing and generics.\n\n## Installation\n\nInstall from PyPI:\n\n```sh\npip install ezpubsub\n```\n\nOr, with [UV](https://github.com/astral-sh/uv):\n\n```sh\nuv add ezpubsub\n```\n\n## Quick Start\n\nCreate a `Signal` instance, subscribe to it, and publish data:\n\n```py\nfrom ezpubsub import Signal\n\ndata_signal = Signal[str](name=\"data_updated\")\n\ndef my_callback(data: str) -> None:\n print(\"Received data:\", data)\n\ndata_signal.subscribe(my_callback)\ndata_signal.publish(\"Hello World\")\n# Output: Received data: Hello World\n```\n\n## Documentation\n\n### [Click here for full documentation](https://edward-jazzhands.github.io/libraries/ezpubsub/docs/)\n\n## Questions, Issues, Suggestions?\n\nUse the [issues](https://github.com/edward-jazzhands/ezpubsub/issues) section for bugs or problems, and post ideas or feature requests on the [discussion board](https://github.com/edward-jazzhands/ezpubsub/discussions).\n\n## License\n\nMIT License. See [LICENSE](LICENSE) for details.\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "An ultra simple, modern pub/sub library and blinker alternative for Python",
"version": "0.1.2",
"project_urls": {
"Changelog": "https://github.com/edward-jazzhands/ezpubsub/blob/master/Changelog.md",
"Repository": "https://github.com/edward-jazzhands/ezpubsub"
},
"split_keywords": [
"python",
" pubsub",
" publish-subscribe",
" typed",
" thread-safe",
" event-driven",
" events"
],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "4c7cf605d22d85e12912b4270999cda76cf25a0e192882e2b856daff343a633f",
"md5": "dee0dc336b7ba754bbacefef154cbfa4",
"sha256": "47c8301e7f6dc062a68ff18e79d7e557ab93afcdce36eea060840e38bec0fe6a"
},
"downloads": -1,
"filename": "ezpubsub-0.1.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "dee0dc336b7ba754bbacefef154cbfa4",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 7888,
"upload_time": "2025-07-25T20:44:07",
"upload_time_iso_8601": "2025-07-25T20:44:07.253329Z",
"url": "https://files.pythonhosted.org/packages/4c/7c/f605d22d85e12912b4270999cda76cf25a0e192882e2b856daff343a633f/ezpubsub-0.1.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "7a5868e5384aa5a53dcf84638477d47f33dee906ee43f7d8f144852f9ad7ee61",
"md5": "60ce069dcd6d3a3588fc13cbff156f00",
"sha256": "3321c5f9822fd79e8dfe68dbae72d703ae7b50e2f8ad966bc58f800fde5fe309"
},
"downloads": -1,
"filename": "ezpubsub-0.1.2.tar.gz",
"has_sig": false,
"md5_digest": "60ce069dcd6d3a3588fc13cbff156f00",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 7168,
"upload_time": "2025-07-25T20:44:08",
"upload_time_iso_8601": "2025-07-25T20:44:08.006212Z",
"url": "https://files.pythonhosted.org/packages/7a/58/68e5384aa5a53dcf84638477d47f33dee906ee43f7d8f144852f9ad7ee61/ezpubsub-0.1.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2025-07-25 20:44:08",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "edward-jazzhands",
"github_project": "ezpubsub",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "ezpubsub"
}