Recently, more and more Python applications have been built based on async communication using the asyncio library that allows a programmer to write concurrent code with async/await syntax. This tutorial shows how to connect to the PostgreSQL database within an asynchronous environment using SQLAlchemy and asyncio.

Installing all necessary libraries

To connect to the PostgreSQL database using asynchronous communication, we need several libraries that will enable us to do so.

To get the ball rolling, we need the asyncio PostgreSQL client library for Python that will allow us to connect to the database. Luckily, there is one called asyncpg. To install the package, you should execute the following command:

$ pip install asyncpg

Second of all, we need to install SQLAlchemy. Sadly, we cannot just install the casual SQLAlchemy dependency. With a focus on async communication, we will use one that supports asyncio operations. In order to install the correct version of SQLAlchemy, we will use the following PIP command:

$ pip install sqlalchemy[asyncio]

Last but not least, we should install the Alembic dependency:

$ pip install alembic

to generate the database migration files.

Connecting to the PostgreSQL database

Now that all libraries have been installed, we can eventually connect to the PostgreSQL database. To achieve that, we need the connection string. 

Depending on your database credentials, the connection string should look like this:

postgresql+asyncpg://<db_username>:<db_secret>@<db_host>:<db_port>/<db_name>

You should pass the above connection string to the create_async_engine function provided by the asyncio SQLAlchemy extension. For instance, it could look like this:

from sqlalchemy.ext.asyncio import create_async_engine

engine = create_async_engine(
    <your_connection_string>,
    echo=True,
    future=True,
)

You can read more on create_async_engine parameters here in the SQLAlchemy documentation.

Generating database migrations

It is usually advisable to generate database migrations for your data models. For that job, SQLAlchemy recommends using the Alembic tool. To start using Alembic in async mode, you should run the following command in your terminal:

$ alembic init -t async alembic

Running the above command will generate the alembic directory. Inside this directory, we have to modify the env.py file with the connection string to your database and import your database models. Let’s say we have the following database.py file with one SQLAlchemy ORM:

from sqlalchemy import (
    Column,
    Integer,
    String,
)
from sqlalchemy.orm import declarative_base

Base = declarative_base()


class Tutorial(Base):
    __tablename__ = "tutorials"

    tutorial_id = Column(Integer, primary_key=True)
    name = Column(String(255), nullable=False)

With such configuration, we need to add the following code to the env.py file inside alembic :

from . import database

# Set target metadata so Alembic knows where the models are
target_metadata = database.Base.metadata

# Set URL to the database
config.set_main_option("sqlalchemy.url",  <your_connection_string>)

You can look at the whole file here.

Now, we can create the migration file that will generate the database table corresponding to the Tutorial model. In order to do that, you should you use the following command:

# Generate new migration file
$ alembic revision --autogenerate -m "Add Tutorial model"

# Apply the migration
$ alembic upgrade head

Managing session with contextmanager

To save, read or delete data from the database, we require a session. To create an AsyncSession instance, we need to add a session generator first. To create one, we should use the sessionmaker function from SQLAlchemy ORM:

from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import sessionmaker

def async_session_generator():
    return sessionmaker(
        engine, class_=AsyncSession
    )

Now we can create the AsyncSession instance and use it in our code. One of the best options is to create a contextmanager that will close the session for us when our code stops using it. An important thing to note is that contextmanager won’t work in the async environment. Instead, we should use the asynccontextmanager:

from contextlib import asynccontextmanager

@asynccontextmanager
async def get_session():
    try:
        async_session = async_session_generator()

        async with async_session() as session:
            yield session
    except:
        await session.rollback()
        raise
    finally:
        await session.close()

Using the asynccontextmanager

In order to utilize the asynccontextmanager, we will use Python’s async with statement:

from .database import get_session
from .dto import Tutorial

async save_tutorial(tutorial: Tutorial) -> Tutorial:
    async with get_session() as session:
        …
        await session.commit()
        …

Conclusion

With the increasing popularity of libraries like Starlette or FastAPI, the demand for async/await support has grown a lot. Nowadays, database integration is a fundamental operation that should cause no difficulties even in the async/await world. A world that is slowly replacing synchronous Python applications.

You can find the entire example here on GitHub.

If you need a dedicated development team keeping up with the Python ecosystem…

Let’s talk!

Kamil Kucharski is a dedicated Backend Developer at Makimo, constantly exploring the dynamic terrain of DevOps, AWS, and Software Architecture. A fearless hero in the tech world, he confidently navigates his way through complex challenges, often sharing his insightful discoveries through articles on Makimo’s blog. Guided by his passion for Python and Clojure, he continually seeks the right tool for every unique challenge. Outside of his professional journey, Kamil channels his energy into mastering the art of Kendo, embodying the fearlessness and heroism he expresses at work.