Working with async
As explained in Getting Started, Rodi's objective is to
simplify constructing objects based on constructors and class properties.
Support for async resolution is intentionally out of the scope of the library because
constructing objects should be lightweight.
This page provides guidelines for working with objects that require asynchronous
initialization or disposal.
A common example
A common example of objects requiring asynchronous disposal are objects that
handle TCP/IP connection pooling, such as HTTP clients and database clients.
These objects are typically implemented as context managers in Python because
they need to manage connection pooling and gracefully close TCP connections
upon disposal.
Python provides asynchronous context managers for this kind of scenario.
Consider the following example, of a SendGrid API client to send emails using the
SendGrid API, with asynchronous code and using httpx.
|  | # domain/emails.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
# TODO: use Pydantic for the Email object.
@dataclass
class Email:
    recipients: list[str]
    sender: str
    sender_name: str
    subject: str
    body: str
    cc: list[str] = None
    bcc: list[str] = None
class EmailHandler(ABC):  # interface
    @abstractmethod
    async def send(self, email: Email) -> None:
        pass
 | 
|  | # data/apis/sendgrid.py
import os
from dataclasses import dataclass
import httpx
from domain.emails import Email, EmailHandler
@dataclass
class SendGridClientSettings:
    api_key: str
    @classmethod
    def from_env(cls):
        api_key = os.environ.get("SENDGRID_API_KEY")
        if not api_key:
            raise ValueError("SENDGRID_API_KEY environment variable is required")
        return cls(api_key=api_key)
class SendGridClient(EmailHandler):
    def __init__(
        self, settings: SendGridClientSettings, http_client: httpx.AsyncClient
    ):
        if not settings.api_key:
            raise ValueError("API key is required")
        self.http_client = http_client
        self.api_key = settings.api_key
    async def send(self, email: Email) -> None:
        response = await self.http_client.post(
            "https://api.sendgrid.com/v3/mail/send",
            headers={
                "Authorization": f"Bearer {self.api_key}",
                "Content-Type": "application/json",
            },
            json=self.get_body(email),
        )
        # TODO: in case of error, log response.text
        response.raise_for_status()  # Raise an error for bad responses
    def get_body(self, email: Email) -> dict:
        return {
            "personalizations": [
                {
                    "to": [{"email": recipient} for recipient in email.recipients],
                    "subject": email.subject,
                    "cc": [{"email": cc} for cc in email.cc] if email.cc else None,
                    "bcc": [{"email": bcc} for bcc in email.bcc] if email.bcc else None,
                }
            ],
            "from": {"email": email.sender, "name": email.sender_name},
            "content": [{"type": "text/html", "value": email.body}],
        }
 | 
The official SendGrid Python SDK does not support async.
At the time of this writing, the official SendGrid Python SDK does not support async.
Its documentation provides a wrong example for async code (see issue #988).
The SendGrid REST API is very well documented and comfortable to use! Use a class like
the one shown on this page to send emails using SendGrid in async code.
 
The SendGridClient depends on an instance of SendGridClientSettings (providing a
SendGrid API Key), and on an instance of httpx.AsyncClient to make async HTTP requests.
The code below shows how to register the object that requires asynchronous
initialization and use it across the lifetime of your application.
|  | # main.py
import asyncio
from contextlib import asynccontextmanager
import httpx
from rodi import Container
from data.apis.sendgrid import SendGridClient, SendGridClientSettings
from domain.emails import EmailHandler
@asynccontextmanager
async def register_http_client(container: Container):
    async with httpx.AsyncClient() as http_client:
        print("HTTP client initialized")
        container.add_instance(http_client)
        yield
    print("HTTP client disposed")
async def application_runtime(container: Container):
    # Entry point for what your application does
    email_handler = container.resolve(EmailHandler)
    assert isinstance(email_handler, SendGridClient)
    assert isinstance(email_handler.http_client, httpx.AsyncClient)
    # We can use the HTTP Client during the lifetime of the Application
    print("All is good! ✨")
def sendgrid_settings_factory() -> SendGridClientSettings:
    return SendGridClientSettings.from_env()
async def main():
    # Bootstrap code for the application
    container = Container()
    container.add_singleton_by_factory(sendgrid_settings_factory)
    container.add_singleton(EmailHandler, SendGridClient)
    async with register_http_client(container) as http_client:
        container.add_instance(
            http_client
        )  # <-- Configure the HTTP client as singleton
        await application_runtime(container)
if __name__ == "__main__":
    asyncio.run(main())
 | 
The above code displays the following:
$ SENDGRID_API_KEY="***" python main.py
HTTP client initialized
All is good! ✨
HTTP client disposed
Considerations
- It is not Rodi's responsibility to administer the lifecycle of the
  application. It is the responsibility of the code that bootstraps the
  application, to handle objects that require asynchronous initialization and
  disposal.
- Python's asynccontextmanageris convenient for these scenarios.
- In the example above, the HTTP Client is configured as singleton to benefit from TCP
  connection pooling. It would also be possible to configure it as transient or scoped
  service, as long as all instances share the same connection pool. In the case of
  httpx,  you can read on this subject here: Why use a Client?.
- Dependency Injection likes custom classes to describe settings for types,
  because registering simple types (str,int,float, etc.) in the container does
  not scale and should be avoided.
The next page explains how Rodi handles context managers.
Last modified on: 2025-04-17 07:04:37