Skip to main content

PyMediate logo

A type-safe request mediator for Python 3.12+

PyPI version Python versions MIT License Documentation
Tests Coverage Checked with mypy (strict) OpenSSF Scorecard SLSA Build Level 2


Features

  • Type safe. Full runtime validation with mypy support.
  • Events. One-to-many publish() alongside one-to-one send() — 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-injector integration.
  • 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.sync instead of pymediate.
  • The handler's __call__ method is a plain def.
  • mediator.send(...) blocks and returns the response directly — no await.
  • 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

📚 Full 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 sync alone only installs the default dev dependency group (ruff, mypy, poethepoet). Test dependencies (pytest and friends) live in the separate test group and won't be installed unless you pass --group test (or --all-groups) — otherwise poe test fails with Failed to spawn: pytest.

Requirements

  • Python 3.12+.
  • Optional: dependency-injector>=4.41.0 for 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

pymediate-0.6.0.tar.gz (1.2 MB view details)

Uploaded Source

Built Distribution

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

pymediate-0.6.0-py3-none-any.whl (56.4 kB view details)

Uploaded Python 3

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

Hashes for pymediate-0.6.0.tar.gz
Algorithm Hash digest
SHA256 b0768167493ebfd871db58c480f32e614e58c7cec0bd3177d3fa0414e2f39f8e
MD5 0fa1b3a024f8ac158e90e0b3a390b690
BLAKE2b-256 a9cda6ac5f36f6d534dc1593a5c7e374a6c01d53a85a4b05924760116920a94d

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymediate-0.6.0.tar.gz:

Publisher: release.yml on sina-al/pymediate

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

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

Hashes for pymediate-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 126f28572259bbbcce8796561945011b3b91d42c814d6a5a880c3b0ff7412aa7
MD5 782a6c82dbb096006c19a4f6323d7667
BLAKE2b-256 7010ef8b89d40b2c15b35801d756ad0afcd00086ca00e5d3a419a4be304212fa

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymediate-0.6.0-py3-none-any.whl:

Publisher: release.yml on sina-al/pymediate

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

Release history Release notifications | RSS feed

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.1

2 files

This release

0.6.0 This release

2 files

0.5.0

2 files

0.4.0

2 files

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

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