Skip to content

Asyncio example is completely wrong, update #988

Open

Description

@agronick

Issue Summary

I'm talking about this example https://github.com/sendgrid/sendgrid-python/blob/main/use_cases/asynchronous_mail_send.md

Steps to Reproduce

Using asyncio is more than just putting async in front of the function doing the http request. In fact this is the completely wrong way to use asyncio and will actually destroys performance as this blocks the event loop from doing any other work while the http request is being made.

To use asyncio correctly the tcp socket sending the data must be using asyncio and one of the low level asyncio socket functions. https://docs.python.org/3/library/asyncio-stream.html These are awaited, allowing the event loop to do other work.

What you have suggested is EXTREMELY bad practice and will stall the servers of anyone who uses it. This is why you can't use requests or urllib with asyncio. You have to use something like aiohttp.

The right way to use asyncio with your library is to run it in a thread pool executor so that the blocking IO stays off the main thread.

You would be better off deleting that example than keeping it in it's current form.

Activity

  1. changed the title [-]Asyncio example is completely wrong[/-] [+]Asyncio example is completely wrong, update[/+] on Apr 27, 2021
  2. thinkingserious commented on Apr 27, 2021

    @thinkingserious
    Contributor

    Hello @agronick,

    Thank you for raising this issue!

    This issue has been added to our internal backlog to be prioritized. Pull requests and +1s on the issue summary will help it move up the backlog.

    Here are some related issues and PRs to consider upon update:

    With best regards,

    Elmer

  3. agronick commented on Apr 28, 2021

    @agronick
    Author

    #953 is exactly what I'm talking about but it was closed.

  4. parikls commented on Sep 24, 2021

    @parikls

    upvote. unfortunately current SDK is not compatible with asyncio which makes it unusable with such things like aiohttp or fastapi

  5. dacevedo12 commented on May 30, 2024

    @dacevedo12
    Contributor

    Hi, it's 2024 and the example is still wrong. Open to PRs? I can help

    It's as simple as wrapping the call using asyncio.to_thread so it doesn't block the main thread.

    Ideally though the SDK itself would provide support for asyncio, maybe through a transport layer that uses aiohttp or httpx

  6. urbanonymous commented on Jun 1, 2024

    @urbanonymous

    I'll just use the api lol

  7. added a commit that references this issue on Oct 30, 2024
    0136955
  8. RobertoPrevato commented on Apr 10, 2025

    @RobertoPrevato

    @agronick is right. The async example is wrong and misleading for less experienced developers. And I agree with @urbanonymous that it's best using the API directly.
    I prepared an example of SendGrid client that sends async emails using httpx:

    # example.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
    
    
    # ----------------------------------------------
    # SendGrid implementation of EmailHandler
    # ----------------------------------------------
    import httpx
    
    
    class SendGridClient(EmailHandler):
        def __init__(self, api_key: str, http_client: httpx.AsyncClient):
            if not api_key:
                raise ValueError("API key is required")
            self.http_client = http_client
            self.api_key = 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),
            )
            # Note: in case of error, inspect 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}],
            }
    
    # ----------------------------------------------
    # Test
    # ----------------------------------------------
    import os
    
    async def example():
    
        async with httpx.AsyncClient() as http_client:
            sendgrid = SendGridClient(
                api_key=os.environ.get("SENDGRID_API_KEY"), http_client=http_client
            )
    
            await sendgrid.send(
                Email(
                    recipients=["hello-world@gmail.com"],
                    sender="example@somedomain.com",
                    sender_name="Example",
                    subject="Test email",
                    body="<h1>Hello</h1>",
                )
            )
    
    
    if __name__ == "__main__":
        import asyncio
    
        asyncio.run(example())

    PowerShell:

    $env:SENDGRID_API_KEY="<YOUR_API_KEY>"
    
    python example.py

    Bash:

    SENDGRID_API_KEY="<YOUR_API_KEY>" python example.py

    Tip

    Do not instantiate a new AsyncClient every time you want to use it, reuse one in your application lifetime.
    If in doubt, read about TCP connection pooling. https://www.python-httpx.org/advanced/clients/#why-use-a-client

    Tip

    To understand why the example in this repository is wrong, watch this: Ryan Dahl: Original Node.js presentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions