SQLAlchemy Engine, Connection, Session, and Sessionmaker Explained
python sqlalchemy engine connection session and sessionmaker: Understand the distinct roles of engine, connection, session, and sessionmaker in SQLAlchemy, and learn w...
python sqlalchemy engine connection session and sessionmaker requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you start working with SQLAlchemy, the terms engine, connection, session, and sessionmaker appear everywhere. They are related but serve different purposes, and confusing them leads to subtle bugs, leaked connections, or unexpected transaction behavior. This article explains what each object does, how they interact, and how to choose the right one for your code.
What an Engine Actually Does
An Engine is the starting point for any SQLAlchemy application. It holds the database URL, the connection pool configuration, and the dialect-specific behavior. Creating an engine does not open a connection to the database; it only prepares the infrastructure needed to create connections later.
from sqlalchemy import create_engine engine = create_engine("postgresql+psycopg2://user:pass@localhost/mydb")
The engine is a factory for connections. It also manages the connection pool, which reuses database connections to avoid the overhead of establishing a new TCP connection and authentication handshake for every operation. The pool is created lazily; the first time you ask for a connection, the engine initializes the pool and opens the first connection.
Connection: The Direct Database Link
A Connection represents an actual DBAPI connection wrapped by SQLAlchemy. You obtain one from the engine using engine.connect(). This is a low-level object that lets you execute raw SQL statements and manage transactions manually.
with engine.connect() as conn: result = conn.execute(text("SELECT * FROM users")) for row in result: print(row)
The with block ensures the connection is returned to the pool after the block exits, even if an exception occurs. You can also use conn.begin() to start an explicit transaction, but the connection itself does not automatically wrap every statement in a transaction. In SQLAlchemy 2.0, the Connection uses a transactional model where you explicitly commit or rollback.
Session: The ORM Workhorse
A Session is a higher-level abstraction built on top of connections. It tracks changes to objects, manages identity maps, and provides a unit-of-work pattern. When you use the ORM, you interact with a Session rather than a Connection directly.
from sqlalchemy.orm import Session with Session(engine) as session: user = session.get(User, 1) user.name = "Alice" session.commit()
The session uses a connection from the engine only when it needs to execute a query or flush changes. It holds that connection for the duration of a transaction, which typically begins when the first database operation is issued. The session also maintains an identity map, so multiple calls to session.get(User, 1) return the same Python object instance, preventing inconsistent state within a single session.
Sessionmaker: A Factory for Sessions
sessionmaker is a configurable factory that creates Session instances. Instead of repeating the same configuration (like binding to an engine, autoflush settings, expire_on_commit) every time you need a session, you define it once.
from sqlalchemy.orm import sessionmaker SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False) # Later in code session = SessionLocal()
Using a sessionmaker is especially useful in web applications where you need to create a new session per request. It centralizes session configuration and makes it easy to swap the engine or change behavior in one place. The sessionmaker itself is thread-safe in the sense that it can be called from multiple threads to produce independent sessions, but the sessions it creates are not thread-safe by default.
Choosing Between Connection and Session
Use a Connection when you need fine-grained control over SQL execution, such as running raw SQL, executing multi-statement transactions, or working with database-specific features that the ORM does not expose. Use a Session when you are working with ORM-mapped objects and want automatic tracking of changes, cascading saves, and identity map behavior.
A common mistake is using a session to execute raw SQL when a connection would be simpler, or using a connection when you actually need ORM features. The decision comes down to whether you are operating at the relational level or the object level.
| Use Case | Recommended Object |
|---|---|
| Raw SQL with manual transaction control | Connection |
| ORM object persistence and querying | Session |
| Bulk inserts or updates via SQL | Connection |
| Complex object graphs with relationships | Session |
| Executing stored procedures | Connection |
Session Lifecycle and Scoping
A session should be used for a single unit of work and then closed. In a web request, that usually means one session per request, created at the beginning and closed at the end. The sessionmaker pattern fits this well.
# In a FastAPI or Flask route def get_db(): db = SessionLocal() try: yield db finally: db.close()
For desktop or long-running applications, you might use a scoped session to ensure thread-local sessions. scoped_session wraps a sessionmaker and returns the same session within the same thread, which helps avoid accidentally sharing a session across threads.
from sqlalchemy.orm import scoped_session session_factory = sessionmaker(bind=engine) Session = scoped_session(session_factory) ```n This is not a substitute for proper session management; it only provides a convenient thread-local storage. You still need to close the session when the thread ends. ## Common Pitfalls and How to Avoid Them One frequent error is using a session after it has been closed. The session's `close()` method releases the underlying connection and rolls back any pending transaction. Accessing objects after close can raise `DetachedInstanceError` if the objects are not loaded and `expire_on_commit` is True. Another pitfall is sharing a session across threads. A `Session` is not thread-safe; it assumes a single thread of execution. Use separate sessions per thread or per request. If you need to share data across threads, detach the objects and reattach them to a new session. A third issue is forgetting to commit or rollback. If you leave a transaction open, the connection remains checked out from the pool, which can exhaust the pool under load. Always use the session as a context manager or explicitly close it in a `finally` block. ## Performance and Connection Pooling The engine's connection pool is a key performance factor. By default, SQLAlchemy uses a `QueuePool` that holds a fixed number of connections. When you create a session, it does not immediately acquire a connection; it does so lazily when the first SQL statement is executed. This means you can create many sessions without exhausting the pool, as long as they do not all execute statements simultaneously. For read-heavy workloads, consider using `pool_pre_ping=True` to verify connections before using them, which avoids stale connections after a database restart. For write-heavy workloads, the transaction isolation level and commit behavior matter more than the pool size. Session overhead is generally small compared to the database round-trip, but creating and closing sessions rapidly can add up. Reusing a sessionmaker and letting the pool handle connection reuse is the standard approach. Avoid creating a new engine for every operation; an engine is meant to be created once per process. ## Session vs. Connection: A Practical Example Suppose you need to insert a list of users and then update their status based on a calculation. With a session, you can do this in an ORM-friendly way: ```python with Session(engine) as session: users = [User(name=f"user{i}") for i in range(10)] session.add_all(users) session.flush() # assign IDs for user in users: user.status = "active" if user.id % 2 == 0 else "inactive" session.commit()
With a connection, you would write raw SQL:
with engine.connect() as conn: with conn.begin(): result = conn.execute(text("INSERT INTO users (name) VALUES (:name) RETURNING id"), [{"name": f"user{i}"} for i in range(10)]) ids = [row[0] for row in result] for uid in ids: status = "active" if uid % 2 == 0 else "inactive" conn.execute(text("UPDATE users SET status = :status WHERE id = :id"), {"status": status, "id": uid})
The session version is more concise and leverages the identity map, but it also hides the transaction boundaries. The connection version gives you explicit control over the transaction and can be more efficient for bulk operations because you can combine statements. Choose based on whether you value ORM convenience or SQL-level control.
Handling Detached Objects and Expiration
When a session is closed, the objects that were loaded become detached. They still hold their data, but any lazy-loaded relationships will fail if accessed. To avoid this, either access all needed attributes before closing the session or configure expire_on_commit=False so objects do not expire after commit.
SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
With this setting, objects remain usable after the session is closed, as long as they were fully loaded. This is useful for returning objects from a request handler after the session has been closed. However, it means you might read stale data if another process updates the same row. For most web applications, it is safer to keep the default expire_on_commit=True and re-query if you need fresh data.
Thread Safety and Scoped Sessions
As mentioned, a Session is not thread-safe. If you are using a multi-threaded web server, create a new session per request. The scoped_session helper provides a thread-local session, but it does not make the session itself thread-safe; it only ensures each thread gets its own instance. You still need to close the session when the thread finishes.
For asynchronous applications, SQLAlchemy provides an async variant (AsyncSession) that works with async engines. The same principles apply, but you must use await when executing operations. The sessionmaker can be used with an async engine to create async sessions.
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb") AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
Async sessions are not interchangeable with sync sessions; choose one based on your application's concurrency model.
Final Code Pattern for a Web Application
A robust pattern for a web application is to create an engine once, define a sessionmaker, and use a dependency to provide a session per request. This keeps the session lifecycle tied to the request and avoids leaks.
# db.py from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker engine = create_engine("postgresql+psycopg2://user:pass@localhost/mydb", pool_pre_ping=True) SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False) # main.py (FastAPI example) from fastapi import FastAPI, Depends from sqlalchemy.orm import Session from .db import SessionLocal app = FastAPI() def get_db(): db = SessionLocal() try: yield db finally: db.close() @app.get("/users/{user_id}") def get_user(user_id: int, db: Session = Depends(get_db)): return db.get(User, user_id)
This pattern ensures that every request gets a fresh session, the session is closed after the response is sent, and the engine's connection pool is reused across requests. It also makes it easy to replace the engine configuration without touching the route handlers.
The distinction between engine, connection, session, and sessionmaker becomes clearer once you see them as layers: engine manages the pool, connection is a pooled DBAPI link, session uses a connection to track ORM objects, and sessionmaker is a factory for sessions with consistent settings. Choosing the right layer for the job prevents resource leaks and keeps your database code maintainable.