How it compares
A source-level comparison with Python mediator libraries, plus dated dispatch benchmarks.
This page compares two specific parts of PyMediate with other Python mediator libraries: the information preserved by the public types, and the runtime cost of dispatch. It is a dated snapshot, not a general ranking of the libraries.
Scope and method
In July 2026, the source of six Python mediator libraries was reviewed. The selection was based on GitHub stars and PyPI downloads at the time. The review covered dispatch code, registries, and type signatures rather than relying on project descriptions.
The results are reported in aggregate. This keeps the comparison focused on common API approaches rather than individual projects. It also means the table should not be read as a claim about every mediator library in Python. Because the library identities and per-library profiles are not published, readers cannot independently map each aggregate claim back to its source project. Treat the table as a maintainer-produced survey snapshot, and verify any row that is material to a library decision.
Capability comparison
| Capability | PyMediate | Finding across the six surveyed libraries |
|---|---|---|
| Request-to-handler link | RequestHandler[PlaceOrder] links the handler to the exact request class. Dispatch uses that class as its key. | Links used class-name strings, naming conventions, or a separate bind(request, handler) call. None used a type parameter as the dispatch key. |
| Response type at the call site | PlaceOrder(Request[OrderReceipt]) makes send(PlaceOrder(...)) return OrderReceipt to a static type checker. | The return type was object, Any, or a generic that was not bound to the request. |
| Handler signature checks | The request parameter and response annotation are checked when Python defines the handler class. | None checked these signatures at class-definition time; mismatches surfaced during dispatch. |
| Duplicate request handlers | Defining a second handler for one request raises an error. | Registration either replaced the existing handler or ignored the new one without raising. |
| Synchronous use | The top-level package is async; pymediate.sync provides the corresponding synchronous API. | Almost all were async-only. |
| Notification publishing | NotificationHandler[OrderPlaced] is typed and checked. Sync handlers run sequentially; async handlers run concurrently. Ordinary failures are grouped after delivery; fatal base exceptions can interrupt it. | Five supported publishing. Each surveyed implementation was type-erased or async-only. |
| Streaming responses | StreamRequest[ChunkT] and StreamRequestHandler[StreamReqT] produce a lazy Iterator[ChunkT] or AsyncIterator[ChunkT]. | One supported streaming, without a typed chunk relationship. |
| Required runtime dependencies | The core package has no runtime dependencies. Integration with dependency-injector is optional. | Requirements varied; several required a serialization framework or dependency-injection container. |
The table covers only the properties that were checked in source. It does not compare documentation, release history, framework integrations, community size, or support for older Python versions. PyMediate is an early-stage package, requires Python 3.12 or later, and may be a worse fit when one of those other factors is more important than its typing model.
Dispatch cost
A mediated call performs more work than a direct call. The benchmark measures PyMediate against the direct calls it replaces; it does not benchmark other libraries with different feature sets.
These results came from PyMediate 0.6.0 on a 2020 Intel MacBook Pro running CPython 3.13.0 and macOS 15.7.4. Each value is the median of five samples of 100,000 calls. The handlers do little work so that dispatch remains visible in the result.
| Scenario | Median per call | Relative to its direct call |
|---|---|---|
Sync: handler(request) — direct call | 0.98 µs | Baseline |
Sync: mediator.send(request) | 3.1 µs | 3.2x |
Sync: send() plus one pipeline behavior | 3.6 µs | 3.7x |
Sync: handler(notification) — direct call | 0.25 µs | Baseline |
Sync: mediator.publish(notification) — one subscriber | 1.1 µs | 4.5x |
Async: await handler(request) — direct call | 0.92 µs | Baseline |
Async: await mediator.send(request) | 2.3 µs | 2.5x |
Async: await handler(notification) — direct call | 0.85 µs | Baseline |
Async: await mediator.publish(notification) — one subscriber | 247 µs | 290x |
On this run, synchronous send() added about 2.1 microseconds to a direct request-handler
call, and asynchronous send() added about 1.4 microseconds. Absolute values vary with the
machine and its current load, so use the table to judge scale rather than to predict an exact
value in another application.
The async publish result includes the cost of scheduling and awaiting subscribers
concurrently with asyncio.gather. That fixed cost is visible because the benchmark handler
does no I/O. In an application, compare it with the work each subscriber performs and with
the notification rate.
Read scripts before you run them
This applies to any script fetched from a URL, including this one. The source is committed as scripts/benchmark.py.
uv run https://pymediate.sina-al.uk/benchmark.pyThe remote command runs the method against the package version it resolves and prints that version
with the Python and platform details. It does not necessarily reproduce this dated 0.6.0 table
after a later release. To run the same source version, check out the v0.6.0 tag and use
uv run poe benchmark; absolute results will still vary by machine and load.
When a direct call is a better fit
Use an ordinary function or callable object when callers can depend on it directly and that relationship does not create repeated wiring. A direct call avoids a service provider, registration, runtime lookup, and indirect navigation. It is also the appropriate choice for code called very frequently to perform small in-memory operations.
A mediator becomes relevant when several callers need one dispatch boundary and the typed request-to-response relationship or early handler checks justify that extra mechanism. The architecture article develops that decision from a direct call, and the introduction explains the resulting API declarations.