Databases with Python — The Complete Notebook
Comprehensive guide on Databases with Python — The Complete Notebook.
Databases with Python
1. Overview#
Most real applications need to persist data. Python talks to databases through DB-API drivers (like psycopg2 for PostgreSQL) directly, or through an ORM (Object-Relational Mapper) like SQLAlchemy, which lets you work with Python classes instead of writing raw SQL everywhere. This note covers both, plus async database access — relevant when paired with FastAPI.
2. Raw SQL with a DB-API Driver#
🐍 PythonInteractive WebAssemblyimport sqlite3
conn = sqlite3.connect("app.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
)
""")
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("Kamal", "kamal@example.com")
)
conn.commit()
cursor.execute("SELECT * FROM users WHERE name = ?", ("Kamal",))
print(cursor.fetchone())
conn.close()
Always use parameterized queries (
?placeholders, or%sfor PostgreSQL drivers) — never format SQL strings with f-strings or.format(). String-built SQL is the classic entry point for SQL injection attacks.
🐍 PythonInteractive WebAssembly# INCORRECT: Never do this
name = "Kamal"
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'") # vulnerable to injection
# CORRECT: Always do this
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))
3. SQLAlchemy — The ORM Approach#
3.1 Installation#
bashpip install sqlalchemy
3.2 Defining Models#
🐍 PythonInteractive WebAssemblyfrom sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.orm import declarative_base, sessionmaker
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
email = Column(String, unique=True, nullable=False)
def __repr__(self):
return f"User(id={self.id}, name={self.name!r})"
engine = create_engine("sqlite:///app.db")
Base.metadata.create_all(engine) # creates tables if they don't exist
Session = sessionmaker(bind=engine)
3.3 CRUD Operations#
🐍 PythonInteractive WebAssemblysession = Session()
# Create
new_user = User(name="Asha", email="asha@example.com")
session.add(new_user)
session.commit()
# Read
user = session.query(User).filter_by(name="Asha").first()
print(user)
all_users = session.query(User).all()
# Update
user.email = "asha.new@example.com"
session.commit()
# Delete
session.delete(user)
session.commit()
session.close()
3.4 Using a Session as a Context Manager#
🐍 PythonInteractive WebAssemblyfrom sqlalchemy.orm import Session as SQLASession
with SQLASession(engine) as session:
session.add(User(name="Ravi", email="ravi@example.com"))
session.commit()
# Session automatically closed here
4. Relationships Between Tables#
🐍 PythonInteractive WebAssemblyfrom sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship
class Author(Base):
__tablename__ = "authors"
id = Column(Integer, primary_key=True)
name = Column(String)
books = relationship("Book", back_populates="author")
class Book(Base):
__tablename__ = "books"
id = Column(Integer, primary_key=True)
title = Column(String)
author_id = Column(Integer, ForeignKey("authors.id"))
author = relationship("Author", back_populates="books")
# Usage
author = Author(name="R.K. Narayan")
author.books.append(Book(title="Swami and Friends"))
session.add(author)
session.commit()
print(author.books[0].title) # Swami and Friends
print(author.books[0].author.name) # R.K. Narayan
| Relationship Type | Example |
|---|---|
| One-to-Many | One Author has many Books |
| Many-to-Many | Book and Tag via an association table |
| One-to-One | User and Profile |
5. Querying in Depth#
🐍 PythonInteractive WebAssemblyfrom sqlalchemy import and_, or_
# Filtering
session.query(User).filter(User.name == "Kamal").all()
session.query(User).filter(User.name.like("%amal%")).all()
session.query(User).filter(and_(User.name == "Kamal", User.id > 1)).all()
session.query(User).filter(or_(User.name == "Kamal", User.name == "Asha")).all()
# Ordering & limiting
session.query(User).order_by(User.name.desc()).limit(10).all()
# Counting
session.query(User).count()
6. Async Database Access#
For high-concurrency apps (especially with FastAPI), synchronous DB calls block the event loop. SQLAlchemy supports async drivers for this.
bashpip install sqlalchemy[asyncio] asyncpg # asyncpg for PostgreSQL
🐍 PythonInteractive WebAssemblyfrom sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
async def get_user_by_id(user_id: int):
async with AsyncSessionLocal() as session:
result = await session.get(User, user_id)
return result
6.1 Using It Inside FastAPI#
🐍 PythonInteractive WebAssemblyfrom fastapi import FastAPI, Depends
app = FastAPI()
async def get_db():
async with AsyncSessionLocal() as session:
yield session
@app.get("/users/{user_id}")
async def read_user(user_id: int, db: AsyncSession = Depends(get_db)):
user = await db.get(User, user_id)
return user
7. Connection Pooling#
Opening a new database connection per request is slow — pooling reuses a set of open connections across requests.
🐍 PythonInteractive WebAssemblyfrom sqlalchemy import create_engine
engine = create_engine(
"postgresql://user:pass@localhost/mydb",
pool_size=10, # number of connections kept open
max_overflow=5, # extra connections allowed under heavy load
pool_timeout=30, # seconds to wait for a free connection before erroring
pool_recycle=1800, # recycle connections after 30 min to avoid stale ones
)
8. Migrations with Alembic#
Schema changes (adding a column, renaming a table) need to be tracked and applied consistently across environments — that's what Alembic (SQLAlchemy's migration tool) is for.
bashpip install alembic
alembic init migrations
alembic revision --autogenerate -m "add email column to users"
alembic upgrade head
9. Common Pitfalls#
INCORRECT: Building SQL Queries with String Formatting#
🐍 PythonInteractive WebAssemblycursor.execute(f"SELECT * FROM users WHERE id = {user_id}") # SQL injection risk
CORRECT: Fix — Always Parameterize#
🐍 PythonInteractive WebAssemblycursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
INCORRECT: Not Closing Sessions/Connections#
Leaked connections eventually exhaust the connection pool, causing the whole app to hang under load.
CORRECT: Fix — Use Context Managers or Dependency Injection#
🐍 PythonInteractive WebAssemblywith Session(engine) as session:
...
# or, in FastAPI, a dependency with yield handles cleanup automatically
INCORRECT: N+1 Query Problem#
🐍 PythonInteractive WebAssemblyauthors = session.query(Author).all()
for author in authors:
print(author.books) # triggers a SEPARATE query for every author!
CORRECT: Fix — Eager Loading#
🐍 PythonInteractive WebAssemblyfrom sqlalchemy.orm import joinedload
authors = session.query(Author).options(joinedload(Author.books)).all()
# Now books are fetched in a single JOIN query instead of one query per author
10. Summary & Best Practices Checklist#
- Always use parameterized queries — never build SQL with string interpolation.
- Use SQLAlchemy (or another ORM) for anything beyond trivial scripts — it prevents whole classes of bugs.
- Close sessions/connections properly — use
withblocks or dependency injection. - Watch for the N+1 query problem; use
joinedload/selectinloadfor related data. - Use async SQLAlchemy + an async driver (
asyncpg) when paired with an async framework like FastAPI. - Configure connection pooling appropriately for your expected load.
- Use Alembic (or an equivalent) to version-control schema changes — never edit production schemas by hand.
Databases & SQLAlchemy Checkpoint
Finished studying this notebook?
Mark this guide as completed to update your course progress roadmap.