Sitemap

Mastering Relational Databases in Python: From Psycopg2 to SQLAlchemy with a Real Project

4 min readMay 6, 2025

๐Ÿ“œ Prelude: A Tale of Two Libraries

Once upon a repo in the kingdom of Python, a developer (you!) stood at the crossroads of working with PostgreSQL. Two powerful paths appeared:

  • Psycopg2 โ€” the sharp sword of raw SQL, mighty but manual.
  • SQLAlchemy โ€” a magical staff, harnessing the arcane arts of ORM.

In this tale, we will walk both paths, one after another, using the Student Enrollment System โ€” a classic yet evergreen RDBMS example.

๐Ÿ—๏ธ Project: Student Enrollment System

We have three mighty tables:

  • students: Stores each learner's details.
  • courses: Holds course information.
  • enrollments: A bridge table showing who enrolled in what.

Letโ€™s first create this project using psycopg2, then recreate it with SQLAlchemy ORM.

โš™๏ธ Setup

๐Ÿงฑ Install PostgreSQL 16 (already done!)

Create a database named school.

๐Ÿงช Install Requirements

pip install psycopg2 sqlalchemy

๐Ÿ“‚ Folder Structure

Student-Enrollment-System/
โ”œโ”€โ”€ psycopg2_version/
โ”‚ โ”œโ”€โ”€ create_tables.py
โ”‚ โ”œโ”€โ”€ insert_data.py
โ”‚ โ””โ”€โ”€ delete_data.py
โ”œโ”€โ”€ sqlalchemy_version/
โ”‚ โ”œโ”€โ”€ models.py
โ”‚ โ”œโ”€โ”€ database.py
โ”‚ โ”œโ”€โ”€ insert_data.py
โ”‚ โ”œโ”€โ”€ delete_data.py
โ”‚ โ””โ”€โ”€ create_tables.py

๐Ÿ˜ Psycopg2: Low-level SQL, High-level Control

๐Ÿ”ง create_tables.py

import psycopg2

conn = psycopg2.connect("dbname=school user=postgres password=yourpass")
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS students (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT NOT NULL,
grade VARCHAR(10)
);
CREATE TABLE IF NOT EXISTS courses (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT
);
CREATE TABLE IF NOT EXISTS enrollments (
id SERIAL PRIMARY KEY,
student_id INT REFERENCES students(id),
course_id INT REFERENCES courses(id)
);
''')
conn.commit()
cursor.close()
conn.close()

โœ๏ธ insert_data.py

conn = psycopg2.connect("dbname=school user=postgres password=yourpass")
cursor = conn.cursor()

cursor.execute("INSERT INTO students (name, age, grade) VALUES (%s, %s, %s)", ('Ravi', 16, '10th'))
cursor.execute("INSERT INTO courses (name, description) VALUES (%s, %s)", ('Math', 'Algebra & Geometry'))
cursor.execute("INSERT INTO enrollments (student_id, course_id) VALUES (%s, %s)", (1, 1))
conn.commit()
cursor.close()
conn.close()

โŒ delete_data.py โ€“ Delete enrollment where student name is Ravi

conn = psycopg2.connect("dbname=school user=postgres password=yourpass")
cursor = conn.cursor()
cursor.execute('''
DELETE FROM enrollments
WHERE student_id = (SELECT id FROM students WHERE name = %s)
''', ('Ravi',))
conn.commit()
cursor.close()
conn.close()

So far, so SQL. But thereโ€™s a lot of manual wiring. Letโ€™s step into the realm of SQLAlchemy!

๐Ÿง™ SQLAlchemy ORM: Pythonic Magic

๐Ÿ”ฎ database.py

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "postgresql://postgres:yourpass@localhost/school"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine)

๐Ÿงฑ models.py

from sqlalchemy import Column, Integer, String, ForeignKey, Text
from sqlalchemy.orm import declarative_base, relationship

Base = declarative_base()
class Student(Base):
__tablename__ = 'students'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
age = Column(Integer, nullable=False)
grade = Column(String(10))
enrollments = relationship("Enrollment", back_populates="student")
class Course(Base):
__tablename__ = 'courses'
id = Column(Integer, primary_key=True)
name = Column(String(100), nullable=False)
description = Column(Text)
enrollments = relationship("Enrollment", back_populates="course")
class Enrollment(Base):
__tablename__ = 'enrollments'
id = Column(Integer, primary_key=True)
student_id = Column(Integer, ForeignKey('students.id'))
course_id = Column(Integer, ForeignKey('courses.id'))
student = relationship("Student", back_populates="enrollments")
course = relationship("Course", back_populates="enrollments")

๐Ÿ—๏ธ create_tables.py

from models import Base
from database import engine

if __name__ == "__main__":
Base.metadata.create_all(engine)
print("All tables created successfully.")

โœ๏ธ insert_data.py

from database import SessionLocal
from models import Student, Course, Enrollment

session = SessionLocal()
student1 = Student(name="Ravi", age=16, grade="10th")
course1 = Course(name="Math", description="Algebra & Geometry")
session.add_all([student1, course1])
session.commit()
enrollment1 = Enrollment(student=student1, course=course1)
session.add(enrollment1)
session.commit()
session.close()
print("Sample data inserted.")

โŒ delete_data.py

from database import SessionLocal
from models import Enrollment, Student

session = SessionLocal()
student = session.query(Student).filter(Student.name == "Ravi").first()
if student:
session.query(Enrollment).filter(Enrollment.student_id == student.id).delete()
session.commit()
print("Enrollment deleted for Ravi.")
session.close()

โš”๏ธ Psycopg2 vs SQLAlchemy โ€” Code Duel

๐Ÿ“š Final Thoughts: Which Should You Choose?

  • Use Psycopg2 if you want fine-grained SQL control or are doing performance tuning.
  • Use SQLAlchemy ORM for 90% of applications โ€” itโ€™s readable, maintainable, and Pythonic.

But best of all, know both, like we did! Start with psycopg2 to understand SQL, then level up to ORM for speed and style.

๐Ÿ The End (Or the Beginning?)

May your databases always be normalized, your queries optimized, and your models beautifully mapped.

If this blog helped you, share it, star it, fork it โ€” and remember:

โ€œThe best code is not just written; itโ€™s woven like poetry.โ€

--

--

Aditya Mangal
Aditya Mangal

Written by Aditya Mangal

Tech enthusiast weaving stories of code and life. Writing about innovation, reflection, and the timeless dance between mind and heart.

No responses yet