SQLAlchemy is the most widely used ORM (Object Relational Mapper) in the Python ecosystem. If you work with relational databases, mastering SQLAlchemy is an essential skill that separates beginner developers from professionals. In this complete guide, you'll learn everything from fundamental concepts to advanced data modeling and query techniques.
According to the official SQLAlchemy documentation, the library provides a full set of high-performance database access patterns tailored for Python. Whether you're a web developer, data analyst, or software engineer, SQLAlchemy will transform how you interact with databases.
What Is an ORM and Why Use SQLAlchemy?
An ORM (Object Relational Mapper) is a technique that lets you work with relational databases using objects and methods from your programming language instead of writing raw SQL. SQLAlchemy takes this concept to a professional level by offering two distinct usage modes:
- SQLAlchemy Core: A focused abstraction built around the SQL expression language, ideal for those who prefer more control over queries
- SQLAlchemy ORM: A complete object-relational mapping layer that turns table rows into Python objects
The Real Python SQLAlchemy tutorial highlights that the library is widely adopted by companies like Reddit, Dropbox, and Yelp precisely for its flexibility and performance.
SQLAlchemy Advantages
- Database independence: The same code works with SQLite, PostgreSQL, MySQL, Oracle, and more
- Security: Built-in protection against SQL injection through parameterized queries
- Productivity: Less boilerplate code compared to raw SQL
- Migrations: Integration with Alembic for schema versioning
- Performance: Lazy and eager loading optimize queries
Installation and Setup
Installing SQLAlchemy is straightforward with pip:
pip install sqlalchemy
To install with support for a specific database:
# For PostgreSQL
pip install sqlalchemy psycopg2
For MySQL
pip install sqlalchemy pymysql
SQLite only (already included in Python)
pip install sqlalchemy
You can verify the installation on the official SQLAlchemy PyPI repository to ensure you have the latest version.
Connecting to the Database
The first step is to create a connection engine. SQLAlchemy uses a connection string called a database URL:
from sqlalchemy import create_engine
SQLite (local database)
engine = create_engine('sqlite:///my_database.db', echo=True)
PostgreSQL
engine = create_engine('postgresql://user:password@localhost:5432/mydb')
MySQL
engine = create_engine('mysql+pymysql://user:password@localhost:3306/mydb')
The echo=True parameter displays all executed SQL queries, essential for debugging and learning.
Defining Models with Declarative Base
With SQLAlchemy ORM, you define your tables as Python classes. The most modern approach uses the declarative system:
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Base = declarative_base()
class Product(Base):
tablename = 'products'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(200), nullable=False)
price = Column(Float, nullable=False)
category = Column(String(100))
created_at = Column(DateTime, default=datetime.utcnow)
def __repr__(self):
return f"<Product(name='{self.name}', price={self.price})>"
Creating Tables
# Create all defined tables
Base.metadata.create_all(engine)
SQLAlchemy automatically generates CREATE TABLE statements based on your classes. Check the declarative tables documentation for more details.
Sessions: The Heart of Operations
The Session is the central mechanism for all database operations in SQLAlchemy ORM. It manages transactions and maintains an object cache:
from sqlalchemy.orm import sessionmaker
Session = sessionmaker(bind=engine)
session = Session()
Always use sessions with context managers to ensure connections are properly closed:
with Session() as session:
# your operations here
pass
CRUD Operations with SQLAlchemy
CREATE — Inserting Data
with Session() as session:
product = Product(
name='Dell XPS Notebook',
price=8999.90,
category='Electronics'
)
session.add(product)
session.commit()
print(f"Product created with ID: {product.id}")
To insert multiple records:
products = [
Product(name='Gaming Mouse', price=249.90, category='Peripherals'),
Product(name='Mechanical Keyboard', price=599.90, category='Peripherals'),
Product(name='27" 4K Monitor', price=3499.90, category='Monitors'),
]
session.add_all(products)
session.commit()
READ — Querying Data
SQLAlchemy offers a rich API for queries. Here are the most common patterns:
# Fetch all records
all_products = session.query(Product).all()
Fetch by ID
product = session.query(Product).get(1)
First record matching the filter
product = session.query(Product).filter_by(name='Gaming Mouse').first()
Filter with conditions
expensive_products = session.query(Product).filter(
Product.price >= 1000
).all()
UPDATE — Updating Data
with Session() as session:
product = session.query(Product).get(1)
product.price = 8499.90
product.name = 'Dell XPS 15 Notebook'
session.commit()
DELETE — Removing Data
with Session() as session:
product = session.query(Product).get(2)
session.delete(product)
session.commit()
Table Relationships
One of SQLAlchemy's most powerful features is managing relationships between tables. Let's build a complete example with customers and orders.
One-to-Many
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship
class Customer(Base):
tablename = 'customers'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
email = Column(String(200), unique=True, nullable=False)
orders = relationship('Order', back_populates='customer')
class Order(Base):
tablename = 'orders'
id = Column(Integer, primary_key=True)
date = Column(DateTime, default=datetime.utcnow)
total_amount = Column(Float)
customer_id = Column(Integer, ForeignKey('customers.id'))
customer = relationship('Customer', back_populates='orders')
Now you can navigate relationships naturally:
# Access orders from a customer
customer = session.query(Customer).get(1)
for order in customer.orders:
print(f"Order #{order.id} - ${order.total_amount}")
Access customer from an order
order = session.query(Order).get(1)
print(f"Customer: {order.customer.name}")
Many-to-Many
For many-to-many relationships, use an association table:
from sqlalchemy import Table, Text
Association table
order_product_table = Table(
'order_products', Base.metadata,
Column('order_id', Integer, ForeignKey('orders.id')),
Column('product_id', Integer, ForeignKey('products.id')),
)
class Product(Base):
tablename = 'products'
id = Column(Integer, primary_key=True)
name = Column(String(200))
price = Column(Float)
description = Column(Text)
orders = relationship('Order', secondary=order_product_table, back_populates='products')
class Order(Base):
tablename = 'orders'
id = Column(Integer, primary_key=True)
date = Column(DateTime, default=datetime.utcnow)
total_amount = Column(Float)
customer_id = Column(Integer, ForeignKey('customers.id'))
customer = relationship('Customer', back_populates='orders')
products = relationship('Product', secondary=order_product_table, back_populates='orders')
For more on relationships, visit the official SQLAlchemy relationships documentation.
Advanced Queries
Complex Filters
from sqlalchemy import and_, or_, not_
Using AND, OR, NOT
products = session.query(Product).filter(
and(
Product.price >= 100,
Product.price <= 5000,
or(
Product.category == 'Electronics',
Product.category == 'Peripherals'
)
)
).all()
Explicit Joins
# JOIN between tables
results = session.query(Order, Customer).join(
Customer, Order.customer_id == Customer.id
).all()
for order, customer in results:
print(f"Order {order.id} - {customer.name} - ${order.total_amount}")
Aggregations and Group By
from sqlalchemy import func
Total sales per customer
results = session.query(
Customer.name,
func.count(Order.id).label('total_orders'),
func.sum(Order.total_amount).label('total_value')
).join(Order).group_by(Customer.id).all()
for name, total, value in results:
print(f"{name}: {total} orders - ${value:.2f}")
SQLAlchemy supports all standard SQL functions through func. Check the SQLAlchemy SQL functions documentation for the complete list.
Subqueries
# Subquery: customers with orders above average
subquery = session.query(
func.avg(Order.total_amount)
).scalar_subquery()
above_avg = session.query(Customer).join(Order).filter(
Order.total_amount > subquery
).distinct().all()
Lazy Loading vs Eager Loading
A crucial performance concept is understanding when and how related data is loaded:
- Lazy Loading (default): Relationships are loaded only when accessed. Can cause N+1 queries if used carelessly
- Eager Loading: Loads everything in a single query using
joinedloadorsubqueryload
from sqlalchemy.orm import joinedload
Eager loading with JOIN
customers = session.query(Customer).options(
joinedload(Customer.orders)
).all()
Now all orders are loaded in the same query
for customer in customers:
print(f"{customer.name}: {len(customer.orders)} orders")
Migrations with Alembic
Alembic is the official migration tool for SQLAlchemy, created by the same author. It allows you to version your database schema:
pip install alembic
alembic init alembic
Configure the alembic.ini file with your database URL and start creating migrations:
# Create a new automatic migration
alembic revision --autogenerate -m "create_products_table"
Apply the migration
alembic upgrade head
For a complete guide, visit the official Alembic documentation.
SQLAlchemy Best Practices
1. Always Use Context Managers for Sessions
# Correct
with Session() as session:
session.add(product)
session.commit()
Incorrect (may leak connection)
session = Session()
session.add(product)
session.commit()
2. Indexes for Performance
class Product(Base):
__tablename__ = 'products'
id = Column(Integer, primary_key=True)
name = Column(String(200), index=True) # Index for name searches
category = Column(String(100), index=True)
3. Use Enum for Fixed Fields
from enum import Enum as PyEnum
from sqlalchemy import Enum as SAEnum
class OrderStatus(PyEnum):
PENDING = 'pending'
PAID = 'paid'
SHIPPED = 'shipped'
DELIVERED = 'delivered'
class Order(Base):
tablename = 'orders'
status = Column(SAEnum(OrderStatus), default=OrderStatus.PENDING)
4. Avoid N+1 Queries
The N+1 problem occurs when you load N records and then make N more queries for each relationship. Use joinedload or subqueryload to prevent this.
SQLAlchemy with FastAPI
Integrating SQLAlchemy with FastAPI is a powerful combination for building high-performance REST APIs. The recommended structure uses dependency injection to manage sessions:
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
app = FastAPI()
def get_db():
db = Session()
try:
yield db
finally:
db.close()
@app.get("/products/")
def list_products(db: Session = Depends(get_db)):
products = db.query(Product).all()
return products
To dive deeper into API creation, check our guide on FastAPI Python: Building RESTful APIs.
SQLAlchemy vs Other Tools
| Feature | SQLAlchemy | Django ORM | Peewee |
|---|---|---|---|
| Flexibility | High (Core + ORM) | Medium | Medium |
| Learning curve | Steep | Moderate | Gentle |
| Database support | SQLite, PG, MySQL, Oracle, MSSQL | SQLite, PG, MySQL, Oracle | SQLite, PG, MySQL |
| Performance | Excellent | Good | Good |
| Migrations | Alembic | Built-in | Playhouse |
| Async support | Yes (asyncio) | Yes (3.1+) | Limited |
Async Support with SQLAlchemy
Starting from version 1.4, SQLAlchemy offers native async/await support, enabling non-blocking database operations:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
import asyncio
async_engine = create_async_engine('sqlite+aiosqlite:///my_database.db')
AsyncSessionLocal = sessionmaker(
asyncengine, class=AsyncSession
)
async def get_products():
async with AsyncSessionLocal() as session:
result = await session.execute('SELECT * FROM products')
return result.fetchall()
asyncio.run(get_products())
Asynchronous programming is an advanced topic worth exploring. Check our guide on Python Async/Await: Asynchronous Programming.
Conclusion
SQLAlchemy is far more than a simple ORM — it's a complete ecosystem for working with relational databases in Python. From small SQLite projects to enterprise PostgreSQL systems, the library provides the tools needed to build robust, secure, and performant data layers.
What we covered in this guide:
- Installation and configuration with different databases
- Model definition using the declarative approach
- Complete CRUD operations
- One-to-many and many-to-many relationships
- Advanced queries with joins, subqueries, and aggregations
- Loading strategies (lazy vs eager)
- Migrations with Alembic
- Integration with FastAPI
- Async support
To continue your studies, I highly recommend reading the official SQLAlchemy 2.0 documentation, one of the best technical documentation sets in the Python ecosystem. Practice by creating models, experiment with different query types, and most importantly, build real projects.
SQLAlchemy also integrates seamlessly with tools like Pytest for automated testing and mypy for type checking, ensuring even more robust code.
Start using SQLAlchemy in your Python projects today and elevate your data layer to a professional level!