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.5.0.tar.gz (1.1 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.5.0-py3-none-any.whl (46.7 kB view details)

Uploaded Python 3

File details

Details for the file pymediate-0.5.0.tar.gz.

File metadata

  • Download URL: pymediate-0.5.0.tar.gz
  • Upload date:
  • Size: 1.1 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for pymediate-0.5.0.tar.gz
Algorithm Hash digest
SHA256 1fb25b48f6302ec95558732824d74a04d7faed949b56b1f7c54bc4a6bfc9e37e
MD5 4ee0a421e4143d3e3f045cc4122a5b94
BLAKE2b-256 33c2f0f3d83d0333b102b21739b4cc653226517c8ca82183dca4ce394aae0b6b

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymediate-0.5.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.5.0-py3-none-any.whl.

File metadata

  • Download URL: pymediate-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 46.7 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.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 8edfef331992524a623f9a4edc05ba4baf81bd23cfeca674862f395b130cf3bf
MD5 976d6eb5d26c1a93918e44db291a8cd8
BLAKE2b-256 397cee294c7dbe5994f530f9b64aa44f852578f447c6c8521ca0abab6aab04cd

See more details on using hashes here.

Provenance

The following attestation bundles were made for pymediate-0.5.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

0.6.0

2 files

This release

0.5.0 This release

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