You know Python. Now learn the one skill that makes you twice as valuable.
Why Python Developers Need SQL
When I started learning backend development I thought Python was enough.
Build the API. Connect to MongoDB. Done.
Then I started looking at real job descriptions.
"Experience with SQL required."
"PostgreSQL knowledge preferred."
"Must be comfortable writing complex queries."
SQL was everywhere. And I had been avoiding it.
The truth is — most real world data lives in relational databases. Not MongoDB. Not JSON files. SQL databases.
MySQL. PostgreSQL. SQLite. They power banks, hospitals, e-commerce platforms, government systems, and almost every enterprise application you've ever used.
As a Python developer — SQL is not optional. It's the difference between being good and being genuinely valuable.
This is everything you actually need to know. No fluff. Just the real stuff. 🐍
Setting Up — Python + SQLite (Zero Installation)
SQLite comes built into Python. No installation needed. Perfect for learning.
import sqlite3
# Connect to database (creates it if doesn't exist)
conn = sqlite3.connect('mydb.db')
cursor = conn.cursor()
print("Database connected successfully!")
# Always close when done
conn.close()
That's it. You have a working database in 5 lines of Python.
Part 1 — Creating Tables
Tables are where your data lives. Think of them like Excel spreadsheets with strict rules.
import sqlite3
conn = sqlite3.connect('company.db')
cursor = conn.cursor()
# Create employees table
cursor.execute('''
CREATE TABLE IF NOT EXISTS employees (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
department TEXT,
salary REAL,
hired_date TEXT,
is_active INTEGER DEFAULT 1
)
''')
# Create departments table
cursor.execute('''
CREATE TABLE IF NOT EXISTS departments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
budget REAL,
manager_id INTEGER
)
''')
conn.commit()
print("Tables created successfully!")
conn.close()
Data types you need to know:
| SQL Type | Python equivalent | Use for |
|---|---|---|
| INTEGER | int | Numbers, IDs, counts |
| REAL | float | Decimals, prices |
| TEXT | str | Names, emails, descriptions |
| BLOB | bytes | Files, images |
| NULL | None | Missing values |
Part 2 — Inserting Data
import sqlite3
conn = sqlite3.connect('company.db')
cursor = conn.cursor()
# Insert single record
cursor.execute('''
INSERT INTO employees (name, email, department, salary, hired_date)
VALUES (?, ?, ?, ?, ?)
''', ('Prashik Besekar', 'prashik@company.com', 'Engineering', 45000, '2026-01-15'))
# Insert multiple records at once
employees = [
('Rahul Sharma', 'rahul@company.com', 'Engineering', 50000, '2025-06-01'),
('Priya Patel', 'priya@company.com', 'Marketing', 42000, '2025-08-15'),
('Amit Kumar', 'amit@company.com', 'Engineering', 55000, '2024-12-01'),
('Sneha Joshi', 'sneha@company.com', 'HR', 38000, '2026-02-01'),
('Vikram Singh', 'vikram@company.com', 'Marketing', 48000, '2025-03-20'),
]
cursor.executemany('''
INSERT INTO employees (name, email, department, salary, hired_date)
VALUES (?, ?, ?, ?, ?)
''', employees)
conn.commit()
print(f"Inserted {cursor.rowcount} records")
conn.close()
Important: Always use ? placeholders for values. NEVER use string formatting — it creates SQL injection vulnerabilities.
# WRONG - SQL injection risk
name = "Prashik"
cursor.execute(f"SELECT * FROM employees WHERE name = '{name}'")
# RIGHT - Safe
cursor.execute("SELECT * FROM employees WHERE name = ?", (name,))
Part 3 — Selecting Data (The Most Important Part)
Basic SELECT
import sqlite3
conn = sqlite3.connect('company.db')
cursor = conn.cursor()
# Get all employees
cursor.execute("SELECT * FROM employees")
all_employees = cursor.fetchall()
for emp in all_employees:
print(emp)
# Get specific columns only
cursor.execute("SELECT name, salary, department FROM employees")
results = cursor.fetchall()
for name, salary, dept in results:
print(f"{name} | {dept} | ₹{salary:,.0f}")
conn.close()
WHERE — Filtering Results
# Single condition
cursor.execute("SELECT * FROM employees WHERE department = ?", ('Engineering',))
# Multiple conditions
cursor.execute("""
SELECT name, salary
FROM employees
WHERE department = ? AND salary > ?
""", ('Engineering', 45000))
# IN operator — multiple values
cursor.execute("""
SELECT name, department
FROM employees
WHERE department IN (?, ?)
""", ('Engineering', 'Marketing'))
# LIKE — pattern matching
cursor.execute("""
SELECT name, email
FROM employees
WHERE email LIKE ?
""", ('%@company.com',))
# NULL check
cursor.execute("SELECT * FROM employees WHERE department IS NOT NULL")
# Range with BETWEEN
cursor.execute("""
SELECT name, salary
FROM employees
WHERE salary BETWEEN ? AND ?
""", (40000, 55000))
ORDER BY — Sorting
# Sort by salary highest to lowest
cursor.execute("""
SELECT name, salary, department
FROM employees
ORDER BY salary DESC
""")
# Sort by multiple columns
cursor.execute("""
SELECT name, department, salary
FROM employees
ORDER BY department ASC, salary DESC
""")
LIMIT — Control How Many Results
# Get top 3 highest paid
cursor.execute("""
SELECT name, salary
FROM employees
ORDER BY salary DESC
LIMIT 3
""")
# Pagination — skip first 5, get next 5
cursor.execute("""
SELECT name, salary
FROM employees
ORDER BY salary DESC
LIMIT 5 OFFSET 5
""")
Part 4 — Aggregations (Making Data Tell Stories)
This is where SQL becomes truly powerful.
# COUNT — how many records
cursor.execute("SELECT COUNT(*) FROM employees")
total = cursor.fetchone()[0]
print(f"Total employees: {total}")
# SUM — add up values
cursor.execute("SELECT SUM(salary) FROM employees")
total_salary = cursor.fetchone()[0]
print(f"Total salary budget: ₹{total_salary:,.0f}")
# AVG — average value
cursor.execute("SELECT AVG(salary) FROM employees")
avg_salary = cursor.fetchone()[0]
print(f"Average salary: ₹{avg_salary:,.0f}")
# MIN and MAX
cursor.execute("SELECT MIN(salary), MAX(salary) FROM employees")
min_sal, max_sal = cursor.fetchone()
print(f"Salary range: ₹{min_sal:,.0f} — ₹{max_sal:,.0f}")
GROUP BY — Aggregations by Category
# Average salary by department
cursor.execute("""
SELECT
department,
COUNT(*) as employee_count,
AVG(salary) as avg_salary,
SUM(salary) as total_salary
FROM employees
GROUP BY department
ORDER BY avg_salary DESC
""")
results = cursor.fetchall()
print("\nDepartment Summary:")
print("-" * 60)
for dept, count, avg, total in results:
print(f"{dept:<15} | {count} employees | Avg: ₹{avg:,.0f} | Total: ₹{total:,.0f}")
HAVING — Filter After Grouping
# Departments with more than 2 employees
cursor.execute("""
SELECT department, COUNT(*) as count
FROM employees
GROUP BY department
HAVING COUNT(*) > 2
""")
# Departments where average salary > 45000
cursor.execute("""
SELECT department, AVG(salary) as avg_sal
FROM employees
GROUP BY department
HAVING AVG(salary) > 45000
ORDER BY avg_sal DESC
""")
Part 5 — JOINs (Combining Tables)
JOINs are the most powerful SQL feature. They combine data from multiple tables.
# First let's add some orders data
cursor.execute('''
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
employee_id INTEGER,
product TEXT,
amount REAL,
order_date TEXT,
FOREIGN KEY (employee_id) REFERENCES employees(id)
)
''')
orders = [
(1, 'Laptop', 85000, '2026-03-01'),
(2, 'Monitor', 25000, '2026-03-05'),
(1, 'Keyboard', 3000, '2026-03-10'),
(3, 'Chair', 15000, '2026-03-15'),
]
cursor.executemany(
"INSERT INTO orders (employee_id, product, amount, order_date) VALUES (?, ?, ?, ?)",
orders
)
conn.commit()
INNER JOIN — Only Matching Records
# Get employees with their orders
cursor.execute("""
SELECT
e.name,
e.department,
o.product,
o.amount,
o.order_date
FROM employees e
INNER JOIN orders o ON e.id = o.employee_id
ORDER BY e.name
""")
results = cursor.fetchall()
for row in results:
print(row)
LEFT JOIN — All Records From Left Table
# Get ALL employees even if they have no orders
cursor.execute("""
SELECT
e.name,
e.department,
COALESCE(o.product, 'No orders') as product,
COALESCE(o.amount, 0) as amount
FROM employees e
LEFT JOIN orders o ON e.id = o.employee_id
ORDER BY e.name
""")
Part 6 — Updating and Deleting
# Update single record
cursor.execute("""
UPDATE employees
SET salary = salary * 1.10
WHERE name = ?
""", ('Prashik Besekar',))
print(f"Updated {cursor.rowcount} records")
# Update multiple records
cursor.execute("""
UPDATE employees
SET salary = salary * 1.05
WHERE department = ? AND salary < ?
""", ('Engineering', 50000))
# Delete specific records
cursor.execute("""
DELETE FROM employees
WHERE is_active = 0
""")
# Safe delete with confirmation
def safe_delete(cursor, employee_id):
cursor.execute("SELECT name FROM employees WHERE id = ?", (employee_id,))
employee = cursor.fetchone()
if not employee:
print(f"Employee {employee_id} not found")
return
confirm = input(f"Delete {employee[0]}? (yes/no): ")
if confirm.lower() == 'yes':
cursor.execute("DELETE FROM employees WHERE id = ?", (employee_id,))
print(f"Deleted {employee[0]}")
else:
print("Cancelled")
conn.commit()
Part 7 — Python + SQL Best Practices
Use Context Managers
# Clean way to handle connections
with sqlite3.connect('company.db') as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM employees")
results = cursor.fetchall()
# Connection automatically closed here
Use Pandas for Analysis
import pandas as pd
import sqlite3
conn = sqlite3.connect('company.db')
# Load SQL query directly into DataFrame
df = pd.read_sql_query("""
SELECT
department,
COUNT(*) as count,
AVG(salary) as avg_salary
FROM employees
GROUP BY department
""", conn)
print(df)
print(df.describe())
# Save DataFrame back to SQL
new_data = pd.DataFrame({
'name': ['New Employee'],
'email': ['new@company.com'],
'department': ['Engineering'],
'salary': [45000],
'hired_date': ['2026-05-01']
})
new_data.to_sql('employees', conn, if_exists='append', index=False)
conn.close()
Build a Reusable Database Class
import sqlite3
import pandas as pd
from contextlib import contextmanager
class Database:
def __init__(self, db_path):
self.db_path = db_path
@contextmanager
def get_connection(self):
conn = sqlite3.connect(self.db_path)
try:
yield conn
conn.commit()
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
def execute(self, query, params=None):
with self.get_connection() as conn:
cursor = conn.cursor()
cursor.execute(query, params or ())
return cursor.fetchall()
def query_df(self, query, params=None):
with self.get_connection() as conn:
return pd.read_sql_query(query, conn, params=params)
# Use it
db = Database('company.db')
# Simple query
results = db.execute("SELECT * FROM employees WHERE department = ?", ('Engineering',))
# Query as DataFrame
df = db.query_df("SELECT department, AVG(salary) as avg FROM employees GROUP BY department")
print(df)
Part 8 — PostgreSQL for Production
SQLite is great for learning. Production apps use PostgreSQL.
pip install psycopg2-binary
import psycopg2
import os
# Connect to PostgreSQL
conn = psycopg2.connect(
host=os.environ.get('DB_HOST', 'localhost'),
database=os.environ.get('DB_NAME', 'myapp'),
user=os.environ.get('DB_USER', 'postgres'),
password=os.environ.get('DB_PASSWORD'),
port=5432
)
cursor = conn.cursor()
# Same SQL syntax as SQLite!
cursor.execute("""
SELECT name, salary
FROM employees
WHERE department = %s
ORDER BY salary DESC
""", ('Engineering',)) # Note: %s instead of ? for PostgreSQL
results = cursor.fetchall()
for row in results:
print(row)
conn.close()
The SQL syntax is almost identical. The main difference — PostgreSQL uses %s instead of ? for parameters.
The SQL Cheat Sheet Every Python Developer Needs
-- SELECT
SELECT column1, column2 FROM table WHERE condition ORDER BY column LIMIT 10;
-- INSERT
INSERT INTO table (col1, col2) VALUES (val1, val2);
-- UPDATE
UPDATE table SET column = value WHERE condition;
-- DELETE
DELETE FROM table WHERE condition;
-- AGGREGATIONS
SELECT COUNT(*), SUM(col), AVG(col), MIN(col), MAX(col) FROM table;
-- GROUP BY
SELECT column, COUNT(*) FROM table GROUP BY column HAVING COUNT(*) > 1;
-- JOINS
SELECT * FROM table1 t1
INNER JOIN table2 t2 ON t1.id = t2.foreign_id;
SELECT * FROM table1 t1
LEFT JOIN table2 t2 ON t1.id = t2.foreign_id;
-- SUBQUERY
SELECT * FROM employees WHERE salary > (SELECT AVG(salary) FROM employees);
Final Thoughts
SQL is not complicated. It reads almost like English.
SELECT the data I want FROM this table WHERE these conditions are true.
Once it clicks — and it clicks fast — you will use it every single day as a Python developer.
Every data engineering role. Every backend role. Every analytics role. SQL is there.
Learn it properly once. Use it forever.
Start with SQLite today. Build the employee database from this article. Run every query. Break things. Fix them.
By the end of the day you'll have a skill that makes you twice as valuable. 💪
Follow LearnWithPrashik for more practical Python and data engineering content.
Connect with me:
LinkedIn: linkedin.com/in/prashik-besekar
GitHub: github.com/prashikBesekar
Top comments (0)