A type-safe request mediator for Python 3.12+
Features
- Type safe. Full runtime validation with mypy support.
- Events. One-to-many
publish()alongside one-to-onesend()— same type-safe, validated-at-import design. - Async-first. The top-level API is async; the full sync mirror lives in
pymediate.sync. - DI ready. Built-in
dependency-injectorintegration. - Well tested. Comprehensive test suite.
Wondering how this stacks up against other Python mediator libraries — and what send() and
publish() cost over direct calls? See How it compares,
a source-level survey of the ecosystem plus a reproducible micro-benchmark you can run against
the latest release with uv run https://pymediate.sina-al.uk/benchmark.py (read it first, as
with any script from the network).
Quick example
import asyncio
from dataclasses import dataclass
from pymediate import Request, RequestHandler, Mediator, Services
# Define response and request as pure dataclasses
@dataclass
class UserCreated:
user_id: int
username: str
@dataclass
class CreateUser(Request[UserCreated]):
username: str
email: str
# RequestHandler automatically linked by type
class CreateUserHandler(RequestHandler[CreateUser]):
async def __call__(self, req: CreateUser) -> UserCreated:
return UserCreated(user_id=1, username=req.username)
# Set up and use
async def main():
services = Services()
services.add(CreateUserHandler())
provider = services.provider()
mediator = Mediator(provider)
response = await mediator.send(CreateUser(username="alice", email="alice@example.com"))
print(f"User {response.username} created with ID {response.user_id}")
asyncio.run(main())
Sync support
Not every application runs an event loop. The pymediate.sync package is the
full sync mirror of the top-level API — the same names, with plain def
handlers and a blocking send():
from dataclasses import dataclass
from pymediate.sync import Request, RequestHandler, Mediator, Services
@dataclass
class UserCreated:
user_id: int
username: str
@dataclass
class CreateUser(Request[UserCreated]):
username: str
email: str
class CreateUserHandler(RequestHandler[CreateUser]):
def __call__(self, req: CreateUser) -> UserCreated:
return UserCreated(user_id=1, username=req.username)
services = Services()
services.add(CreateUserHandler())
provider = services.provider()
mediator = Mediator(provider)
response = mediator.send(CreateUser(username="alice", email="alice@example.com"))
print(f"User {response.username} created with ID {response.user_id}")
Key differences for sync:
- Import from
pymediate.syncinstead ofpymediate. - The handler's
__call__method is a plaindef. mediator.send(...)blocks and returns the response directly — noawait.- Shared names (
Request,Event,Services, errors) are the same objects on both sides, so the two APIs mix freely in one codebase.
Pipeline behaviors
PyMediate supports pipeline behaviors (middleware) that automatically wrap request processing for cross-cutting concerns like logging, validation, caching, and more:
from pymediate import Request, PipelineBehavior
# Universal behavior - applies to all requests
class LoggingBehavior(PipelineBehavior[Request]):
async def __call__(self, request, next):
print(f"Handling: {type(request).__name__}")
response = await next()
print(f"Completed: {type(request).__name__}")
return response
# Selective behavior - only applies to CreateUser requests
class ValidationBehavior(PipelineBehavior[CreateUser]):
async def __call__(self, request, next):
# Validate before processing
if not request.username:
raise ValueError("Username is required")
return await next()
# Register behaviors and handlers
services = Services()
services.add(LoggingBehavior()) # Applied to all requests
services.add(ValidationBehavior()) # Only applied to CreateUser
services.add(CreateUserHandler())
mediator = Mediator(services.provider())
# Behaviors automatically wrap matching requests (inside an async context)
response = await mediator.send(CreateUser(username="alice", email="alice@example.com"))
# Output:
# Handling: CreateUser
# Completed: CreateUser
Behaviors can be universal (PipelineBehavior[Request]) or selective (PipelineBehavior[SpecificRequest]), applying only to matching request types or mixins. They're resolved per request and work with any dependency-injector provider lifetime — Factory, Singleton, or a scoped variant like ContextLocalSingleton. See the Pipeline behaviors guide for more examples.
Events
send() routes one request to its one handler. publish() is the one-to-many counterpart: announce a fact once, and every subscribed EventHandler reacts — the publisher never knows who's listening:
from dataclasses import dataclass
from pymediate import Event, EventHandler, Mediator, Services
@dataclass
class OrderPlaced(Event):
order_id: int
class SendConfirmation(EventHandler[OrderPlaced]):
async def __call__(self, event: OrderPlaced) -> None:
print(f"Confirming order {event.order_id}")
class UpdateAnalytics(EventHandler[OrderPlaced]):
async def __call__(self, event: OrderPlaced) -> None:
print(f"Recording order {event.order_id}")
services = Services()
services.add(SendConfirmation()).add(UpdateAnalytics())
mediator = Mediator(services.provider())
await mediator.publish(OrderPlaced(order_id=42)) # inside an async context
# Output:
# Confirming order 42
# Recording order 42
Handlers run concurrently via asyncio.gather (sequentially, in registration order, in the sync API), zero subscribers is a no-op, and a raising handler never stops the others — failures aggregate into an ExceptionGroup. See the Events guide.
Installation
# Core package
pip install pymediate
# With dependency injection support
pip install pymediate[di]
Documentation
Development
Quick start
# Clone and install
git clone https://github.com/sina-al/pymediate.git
cd pymediate
uv sync --all-extras --group test
# Optional: commit-time format/lint gate (same checks CI runs)
uvx pre-commit install
# Run tests
poe test
# Run all checks
poe check:all
# See all available tasks
poe
Available commands
PyMediate uses Poe the Poet for task running. Run poe to see all commands, or check tasks.toml.
Note:
uv syncalone only installs the defaultdevdependency group (ruff, mypy, poethepoet). Test dependencies (pytest and friends) live in the separatetestgroup and won't be installed unless you pass--group test(or--all-groups) — otherwisepoe testfails withFailed to spawn: pytest.
Requirements
- Python 3.12+.
- Optional:
dependency-injector>=4.41.0for DI support.
Versioning
PyMediate follows ZeroVer — the major version stays at 0 indefinitely,
with no planned 1.0. Expect the public API to keep evolving: a minor release (0.X.0) can
include breaking changes, while a patch release (0.1.X) is backward-compatible.
Contributing
Contributions are welcome. See CONTRIBUTING.md for guidelines.
License
MIT License — see LICENSE for details.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pymediate-0.6.0.tar.gz.
File metadata
- Download URL: pymediate-0.6.0.tar.gz
- Upload date:
- Size: 1.2 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b0768167493ebfd871db58c480f32e614e58c7cec0bd3177d3fa0414e2f39f8e
|
|
| MD5 |
0fa1b3a024f8ac158e90e0b3a390b690
|
|
| BLAKE2b-256 |
a9cda6ac5f36f6d534dc1593a5c7e374a6c01d53a85a4b05924760116920a94d
|
Provenance
The following attestation bundles were made for pymediate-0.6.0.tar.gz:
Publisher:
release.yml on sina-al/pymediate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pymediate-0.6.0.tar.gz -
Subject digest:
b0768167493ebfd871db58c480f32e614e58c7cec0bd3177d3fa0414e2f39f8e - Sigstore transparency entry: 2157956249
- Sigstore integration time:
-
Permalink:
sina-al/pymediate@8687243c41368076e01086ab88fc4436de4317af -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/sina-al
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8687243c41368076e01086ab88fc4436de4317af -
Trigger Event:
push
-
Statement type:
File details
Details for the file pymediate-0.6.0-py3-none-any.whl.
File metadata
- Download URL: pymediate-0.6.0-py3-none-any.whl
- Upload date:
- Size: 56.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
126f28572259bbbcce8796561945011b3b91d42c814d6a5a880c3b0ff7412aa7
|
|
| MD5 |
782a6c82dbb096006c19a4f6323d7667
|
|
| BLAKE2b-256 |
7010ef8b89d40b2c15b35801d756ad0afcd00086ca00e5d3a419a4be304212fa
|
Provenance
The following attestation bundles were made for pymediate-0.6.0-py3-none-any.whl:
Publisher:
release.yml on sina-al/pymediate
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pymediate-0.6.0-py3-none-any.whl -
Subject digest:
126f28572259bbbcce8796561945011b3b91d42c814d6a5a880c3b0ff7412aa7 - Sigstore transparency entry: 2157956344
- Sigstore integration time:
-
Permalink:
sina-al/pymediate@8687243c41368076e01086ab88fc4436de4317af -
Branch / Tag:
refs/tags/v0.6.0 - Owner: https://github.com/sina-al
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8687243c41368076e01086ab88fc4436de4317af -
Trigger Event:
push
-
Statement type: