SQL from Python
Where the SQL in this section meets the Python already covered elsewhere on this site.
Intermediate
import psycopg2
conn = psycopg2.connect(host="localhost", dbname="mydb", user="alice", password="...")
cur = conn.cursor()
cur.execute("SELECT id, name FROM users WHERE active = %s", (True,))
rows = cur.fetchall()
conn.commit() # required for any write — psycopg2 opens an implicit transaction per connection
cur.close()
conn.close()
psycopg2 (or its newer async-friendly sibling, psycopg3) is the standard low-level PostgreSQL driver for Python — almost everything else, including SQLAlchemy, uses one of these underneath.# NEVER do this — string formatting builds SQL injection directly into the query
cur.execute(f"SELECT * FROM users WHERE name = '{user_input}'")
# if user_input is: ' OR '1'='1
# the query becomes: SELECT * FROM users WHERE name = '' OR '1'='1' — returns every row
# ALWAYS do this — the driver sends the value separately from the SQL text
cur.execute("SELECT * FROM users WHERE name = %s", (user_input,))
# user_input is never interpreted as SQL, no matter what it contains
# Core: write SQL-shaped Python, close to the metal
from sqlalchemy import create_engine, text
engine = create_engine("postgresql+psycopg2://alice:...@localhost/mydb")
with engine.connect() as conn:
result = conn.execute(text("SELECT id, name FROM users WHERE active = :active"), {"active": True})
# ORM: map rows to objects
from sqlalchemy.orm import Session
from models import User
with Session(engine) as session:
users = session.query(User).filter(User.active == True).all()
| Core | ORM | |
|---|---|---|
| Mental model | SQL, expressed as Python objects | Python objects, backed by SQL |
| Best for | reports, bulk operations, anything performance-sensitive where you want to see exactly what SQL runs | typical CRUD application code, where object identity and relationships matter more than query-shape control |
| Common pitfall | — | the N+1 problem — see Query Optimization |
| Layer | What it does |
|---|---|
| SQLAlchemy's built-in pool | reuses connections within a single application process |
| PgBouncer | an external pooler sitting between many application processes/servers and PostgreSQL, multiplexing a large number of client connections onto a much smaller number of actual database connections |
max_connections purely from pool overhead, long before real query load is the bottleneck. In transaction pooling mode specifically, session-level features like SET and advisory locks (see Locking & Concurrency) don't behave the same as on a direct connection — worth checking before assuming PgBouncer is a drop-in, zero-behavior-change layer.