Python · SQL for Python Developers
How SQL Helps You Land a Python Developer Job Faster
Quick summary — SQL for Python developer jobs
SQL helps you land a Python developer job faster because most Python work reads or writes a database, and interviews test it directly. Learn joins, grouping and window functions, then use them from Python through the DB-API, always with parameterised queries. Add SQLAlchemy for ORM work, understand transactions and the N+1 problem, and show it all in one project.
In this guide you will learn:
- SQL basics through Python — sqlite3, cursors and rows.
- Safe queries — placeholders, injection and transactions.
- ORMs vs raw SQL — SQLAlchemy 2.0 style and when to drop down.
- Templates — practice schema, query drills and a project checklist.
- Interview questions — what Python developer interviews ask about databases.
SECTION 01Why SQL matters in Python developer jobs
Python developer roles in India cover many kinds of work: backend APIs with Django, Flask or FastAPI, automation scripts, internal tools and data pipelines. What they share is a database. User accounts, orders, logs and reports almost always live in a relational database such as PostgreSQL, MySQL or SQLite, and the Python code is only as good as the queries behind it.
That is why SQL shortens a job search. Many screening rounds include a SQL question or a take-home task that stores data, and interviewers quickly notice whether a candidate understands joins, indexes and transactions or only knows how to call an ORM method. Being comfortable in both — writing SQL by hand and using it well from Python — makes you useful from your first week.
| Python job type | Where SQL appears | Skill tested | Typical task |
|---|---|---|---|
| Backend / API developer | Models, queries behind endpoints | Joins, indexes, ORM use | Return a user's recent orders |
| Automation / scripting | Reading and updating records in bulk | executemany, transactions | Import a CSV into a table safely |
| Data / ETL work | Extracting and loading data | GROUP BY, window functions | Daily totals per customer |
| Internal tools | Admin pages and reports | Filtering, pagination | Search users by city and date |
import sqlite3
conn = sqlite3.connect("shop.db")
conn.row_factory = sqlite3.Row # access columns by name
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT
)""")
cur.executemany(
"INSERT INTO customers (name, city) VALUES (?, ?)",
[("Asha", "Noida"), ("Ravi", "Delhi"), ("Meera", "Noida")],
)
conn.commit()
city = "Noida"
for row in cur.execute("SELECT id, name FROM customers WHERE city = ?", (city,)):
print(row["id"], row["name"])
conn.close()
$ python3 basics.py
1 Asha
3 Meera
# sqlite3 ships with Python, so you can practise
# SQL without installing a database server.
# The same DB-API pattern (connect, cursor,
# execute, commit) works with psycopg for
# PostgreSQL and other drivers, though the
# placeholder style may differ (%s instead of ?).
SECTION 02Safe queries and transactions from Python
The most common mistake in beginner code is building SQL with f-strings or string concatenation. If any part of that string comes from a user, a form or a file, an attacker can change the meaning of the query — this is SQL injection. The fix is simple: use parameterised queries, where the SQL text contains placeholders such as ?, %s or :name and the values are passed separately to execute().
The second thing interviewers look for is transactions. When two changes must succeed or fail together — debiting one account and crediting another, or inserting an order and its items — they belong in one transaction. In sqlite3, using the connection as a context manager commits when the block succeeds and rolls back if an exception is raised.
| Driver or library | Placeholder style | Example | Notes |
|---|---|---|---|
| sqlite3 (built in) | qmark ? | WHERE email = ? | Also supports :name |
| psycopg (PostgreSQL) | format %s | WHERE email = %s | Never use Python % to fill it |
| SQLAlchemy text() | named :name | WHERE email = :email | Pass a dict of values |
| Django ORM raw() | %s | raw("... = %s", [email]) | Prefer the ORM filters first |
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT, is_admin INTEGER)")
conn.executemany("INSERT INTO users (email, is_admin) VALUES (?, ?)",
[("a@x.com", 0), ("boss@x.com", 1)])
user_input = "nobody' OR '1'='1"
# UNSAFE: input is pasted into the SQL text
unsafe = f"SELECT email FROM users WHERE email = '{user_input}'"
print("unsafe:", conn.execute(unsafe).fetchall())
# SAFE: the driver sends the value separately
safe = "SELECT email FROM users WHERE email = ?"
print("safe: ", conn.execute(safe, (user_input,)).fetchall())
# Output:
# unsafe: [('a@x.com',), ('boss@x.com',)]
# safe: []
import sqlite3
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE accounts (id INTEGER PRIMARY KEY, "
"balance INTEGER CHECK (balance >= 0))")
conn.executemany("INSERT INTO accounts (id, balance) VALUES (?, ?)", [(1, 500), (2, 100)])
conn.commit()
def transfer(conn, src, dst, amount):
try:
with conn: # commit on success, rollback on exception
conn.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (amount, src))
conn.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (amount, dst))
return True
except sqlite3.IntegrityError:
return False
print(transfer(conn, 1, 2, 200)) # True
print(transfer(conn, 2, 1, 9999)) # False: CHECK fails, nothing changes
print(conn.execute("SELECT id, balance FROM accounts").fetchall())
# [(1, 300), (2, 300)]
SECTION 03ORMs vs raw SQL: SQLAlchemy in practice
Most Python teams use an ORM — the Django ORM or SQLAlchemy — so you write Python classes and queries instead of SQL strings. ORMs save time, keep code consistent and handle parameters safely. But they still generate SQL, and a developer who does not understand that SQL can write code that looks fine and runs one query per row. This is the N+1 problem, and it is a favourite interview topic.
A sensible rule: use the ORM for everyday create, read, update and delete work, and drop to raw SQL (through text() or a cursor) for complex reports, window functions or performance-critical queries. Either way, turn on query logging while you develop so you can see exactly what reaches the database.
| Approach | Strength | Weakness | Use it for |
|---|---|---|---|
| ORM models and select() | Readable, safe, portable | Can hide inefficient queries | Everyday CRUD in apps |
| ORM with eager loading | Avoids N+1 queries | Must choose strategy deliberately | Lists with related data |
| SQLAlchemy text() | Full SQL power, still parameterised | Database-specific syntax | Reports, window functions |
| Plain DB-API cursor | No extra dependency | More manual code | Scripts and small tools |
# pip install sqlalchemy (2.0 style)
from sqlalchemy import create_engine, ForeignKey, String, select, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
class Base(DeclarativeBase):
pass
class Customer(Base):
__tablename__ = "customers"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(100))
orders: Mapped[list["Order"]] = relationship(back_populates="customer")
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
amount: Mapped[int]
customer_id: Mapped[int] = mapped_column(ForeignKey("customers.id"))
customer: Mapped["Customer"] = relationship(back_populates="orders")
engine = create_engine("sqlite:///shop_orm.db", echo=True) # echo prints SQL
Base.metadata.create_all(engine)
with Session(engine) as session:
session.add(Customer(name="Asha", orders=[Order(amount=1200), Order(amount=800)]))
session.commit()
stmt = (select(Customer.name, func.sum(Order.amount).label("total"))
.join(Customer.orders)
.group_by(Customer.name))
for name, total in session.execute(stmt):
print(name, total) # Asha 2000
from sqlalchemy import select, text
from sqlalchemy.orm import Session, selectinload
# (continues from the ORM Models tab)
# N+1: one query for customers, then one per customer for orders
with Session(engine) as s:
for c in s.scalars(select(Customer)):
print(c.name, len(c.orders))
# Fix: load all orders in one extra query
with Session(engine) as s:
stmt = select(Customer).options(selectinload(Customer.orders))
for c in s.scalars(stmt):
print(c.name, len(c.orders))
# Raw SQL when it is clearer, still parameterised
report = text("""
SELECT c.name, COUNT(o.id) AS order_count, SUM(o.amount) AS total
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.name
HAVING SUM(o.amount) > :min_total
ORDER BY total DESC
""")
with engine.connect() as conn:
for row in conn.execute(report, {"min_total": 500}):
print(row.name, row.order_count, row.total)
SECTION 04Templates: practice schema, query drills, project checklist
Use the schema to practise, work through the drills until you can write each query without looking it up, then use the checklist to turn the practice into one portfolio project that shows Python and SQL working together.
-- schema.sql (works in SQLite and PostgreSQL)
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT,
joined DATE
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
amount INTEGER NOT NULL,
created DATE NOT NULL
);
CREATE INDEX idx_orders_customer ON orders (customer_id);
-- Load it from Python:
-- conn.executescript(open("schema.sql").read())
QUERY DRILLS (write each in SQL, then run from Python)
1. Customers from Noida, newest first
2. Total order amount per customer (GROUP BY)
3. Customers with no orders (LEFT JOIN ... IS NULL)
4. Customers whose total is above a value (HAVING)
5. Latest order per customer (ROW_NUMBER() OVER
(PARTITION BY customer_id ORDER BY created DESC))
6. Monthly revenue (strftime / date_trunc)
7. Top 3 customers by spend (ORDER BY ... LIMIT 3)
8. Same as 2, but written with SQLAlchemy select()
9. Insert 1,000 rows with executemany in one transaction
10. Explain query 2 with EXPLAIN QUERY PLAN / EXPLAIN
PYTHON + SQL PORTFOLIO PROJECT CHECKLIST
[ ] Small API (Flask or FastAPI) over 2-3 related tables
[ ] Schema with primary keys, foreign keys, one index
[ ] All queries parameterised - no f-strings in SQL
[ ] At least one multi-step write in a transaction
[ ] One endpoint using a JOIN or eager loading
[ ] One report query with GROUP BY or a window function
[ ] Migrations (Alembic or Django migrations)
[ ] Tests that use a temporary SQLite database
[ ] README: schema diagram + example queries
[ ] Pushed to GitHub with clear commits
SECTION 05Tips for learning SQL as a Python developer
Spend your time on the parts of SQL that show up in real Python code:
- Write SQL by hand first — practise joins and grouping in a SQL console before hiding them behind an ORM.
- Never format values into SQL — use placeholders every time, even in quick scripts, so the habit is automatic.
- Watch the generated SQL — turn on echo or Django query logging and count the queries each page runs.
- Learn one real server — after SQLite, set up PostgreSQL locally and connect with psycopg or SQLAlchemy.
- Test against a database — use a temporary SQLite or test PostgreSQL database in your pytest suite.
SECTION 06Interview Q&A — SQL for Python developers
Q1How do you prevent SQL injection in Python?
I never build SQL by inserting values into strings. I use parameterised queries, passing values separately to execute() with the driver's placeholder style, or I use an ORM that does this for me. For dynamic column names I validate against an allow-list.
Q2What is the N+1 query problem?
It happens when code runs one query to fetch a list and then one more query for each item to load related data. With an ORM I fix it using eager loading, such as selectinload or joinedload in SQLAlchemy, or select_related and prefetch_related in Django.
Q3When would you use raw SQL instead of an ORM?
For complex reports, window functions, bulk operations or a query the ORM generates poorly. I still parameterise it, keep it in one place, and cover it with tests so changes to the schema are caught.
Q4What does commit and rollback do?
Commit makes all changes in the current transaction permanent. Rollback undoes them. Grouping related writes in one transaction means either all of them are saved or none are, which keeps data consistent when something fails.
Q5What is the difference between INNER JOIN and LEFT JOIN?
INNER JOIN returns only rows that match in both tables. LEFT JOIN returns every row from the left table and fills the right-hand columns with NULL where there is no match, which is useful for finding customers without orders.
SECTION 07Test yourself — Python and SQL quiz
Five questions. No sign-up.
0 / 5Pick an answer to see why it is right or wrong.
SECTION 08Frequently asked questions
Do Python developers need to know SQL?
Yes, for most roles. Backend, automation and data-focused Python jobs all read or write databases. Even with an ORM, you need SQL to debug slow pages, write reports and answer the database questions that appear in many interviews.
Is SQLAlchemy or Django ORM better to learn first?
Learn the one your target jobs use. Django ORM comes with Django projects; SQLAlchemy is common with Flask, FastAPI and data scripts. The concepts — models, relationships, eager loading and transactions — transfer between them.
How much SQL should a fresher Python developer know?
Enough to design two or three related tables, write joins, GROUP BY and HAVING, filter and paginate, use transactions, and read a basic query plan. Window functions are a useful bonus for data-related roles.
Which database should I practise SQL with for Python?
Start with SQLite because it ships with Python and needs no server. Then install PostgreSQL locally, since it is widely used in production Python applications, and connect to it with psycopg or SQLAlchemy.
What is the Python DB-API?
It is a standard interface, described in PEP 249, that most Python database drivers follow. It defines connections, cursors, execute, fetch methods, commit and rollback, so code looks similar whether you use SQLite, PostgreSQL or MySQL.
SECTION 09Related reads
Classroom & online · Noida
Become a Python developer who knows the database
Our Python Training Course covers core Python, working with databases and SQL, APIs and projects, with classroom batches in Noida and Delhi and live online batches.
Talk to a counsellor for current fees- Core Python & OOP
- Databases & SQL from Python
- Resume rebuild & mock interviews
- Weekday & weekend batches