Complete Learning Guide

Python Mastery —
Zero to Web Dev

A structured, practical course covering Python fundamentals, OOP, web development with Flask, and database integration — with real-world code throughout.

5 Chapters 40+ Code Examples 1 Full Project Beginner → Advanced
1
Python Basics
Build a rock-solid foundation: syntax, data types, control flow, functions, and more
Introduction to Python

Python is a high-level, interpreted programming language created by Guido van Rossum in 1991. Its design philosophy emphasises code readability, making it ideal for beginners and powerful enough for professionals.

Key Features

Simple syntax · Dynamically typed · Garbage collected · Vast standard library · Cross-platform

Use Cases

Web development · Data science · AI/ML · Automation · Scripting · Scientific computing

Interpreted

Python runs line-by-line. No compilation step needed — great for rapid development and testing.

Community

330,000+ packages on PyPI. One of the world's most popular languages with massive community support.

💡 Why Python First?
Python's clear, English-like syntax lets you focus on learning programming concepts rather than fighting with language complexity. Once you know Python, picking up other languages is much easier.
Installation & Environment Setup

Before writing code, you need Python installed and a good editor configured.

  • 1Download Python: Visit python.org/downloads and install Python 3.11+. Always check "Add Python to PATH" on Windows.
  • 2Install VS Code: Download from code.visualstudio.com and install the Python extension by Microsoft.
  • 3Verify installation: Open a terminal and run python --version. You should see Python 3.x.x.
  • 4Run your first script: Create hello.py, type print("Hello, World!"), and run it with python hello.py.
python · hello.py
# Your very first Python program
# The print() function outputs text to the terminal
print("Hello, World!")

# Python uses indentation instead of curly braces
# This is valid Python — clean and readable
name = "Alice"
print(f"Hello, {name}!")  # Output: Hello, Alice!
Variables & Data Types

Variables are named containers that store data. Python is dynamically typed — you don't declare types explicitly; Python figures it out automatically.

python · data_types.py
# ── INTEGER: whole numbers ──────────────────────────
age = 25
year = 2024
print(type(age))  # <class 'int'>

# ── FLOAT: decimal numbers ──────────────────────────
price = 19.99
pi    = 3.14159
print(type(price))  # <class 'float'>

# ── STRING: text (single or double quotes) ──────────
name    = "Alice Mugisha"
city    = 'Kigali'
message = f"{name} lives in {city}"  # f-string
print(message)  # Alice Mugisha lives in Kigali

# String methods
print(name.upper())       # ALICE MUGISHA
print(name.split(" "))   # ['Alice', 'Mugisha']
print(len(name))          # 13

# ── BOOLEAN: True or False ──────────────────────────
is_student  = True
is_employed = False
print(type(is_student))  # <class 'bool'>

# ── NONE: absence of a value ────────────────────────
result = None
print(result is None)  # True

# ── TYPE CONVERSION ─────────────────────────────────
num_str = "42"
num_int = int(num_str)   # string → int
num_flt = float(num_str) # string → float
back_str = str(num_int)  # int → string
TypeExampleUse Case
intage = 25Counting, indexing, IDs
floatprice = 9.99Measurements, money, decimals
strname = "Alice"Text, names, messages
boolactive = TrueFlags, conditions, switches
NoneTyperesult = NoneMissing/optional values
Operators

Operators perform operations on values. Python has arithmetic, comparison, and logical operators — the building blocks of all logic.

python · operators.py
# ── ARITHMETIC OPERATORS ────────────────────────────
print(10 + 3)   # 13  — addition
print(10 - 3)   # 7   — subtraction
print(10 * 3)   # 30  — multiplication
print(10 / 3)   # 3.33 — division (always float)
print(10 // 3)  # 3   — floor division (int)
print(10 % 3)   # 1   — modulus (remainder)
print(2 ** 8)   # 256 — exponentiation

# ── COMPARISON OPERATORS ────────────────────────────
x, y = 10, 20
print(x == y)   # False — equal to
print(x != y)   # True  — not equal
print(x < y)    # True  — less than
print(x > y)    # False — greater than
print(x <= 10)  # True  — less or equal

# ── LOGICAL OPERATORS ───────────────────────────────
a, b = True, False
print(a and b)   # False — both must be True
print(a or b)    # True  — at least one True
print(not a)     # False — inverts the value

# Real-world example: check eligibility
age = 20
has_id = True
can_vote = age >= 18 and has_id
print(f"Can vote: {can_vote}")  # Can vote: True
Control Flow — if, elif, else & Loops

Control flow directs which code runs and when. Use if/elif/else for decisions and loops for repetition.

python · control_flow.py
# ── IF / ELIF / ELSE ────────────────────────────────
score = 75

if score >= 90:
    grade = "A"
elif score >= 80:
    grade = "B"
elif score >= 70:
    grade = "C"
else:
    grade = "F"

print(f"Grade: {grade}")  # Grade: C

# ── FOR LOOP: iterate over a sequence ───────────────
students = ["Alice", "Bob", "Claire"]
for student in students:
    print(f"Welcome, {student}!")

# range() generates a sequence of numbers
for i in range(1, 6):  # 1 to 5
    print(f"{i} x 3 = {i * 3}")

# enumerate() gives index + value
for idx, name in enumerate(students, start=1):
    print(f"{idx}. {name}")

# ── WHILE LOOP: repeat while condition is True ──────
countdown = 5
while countdown > 0:
    print(f"T-minus {countdown}...")
    countdown -= 1
print("Liftoff! 🚀")

# ── BREAK & CONTINUE ────────────────────────────────
for num in range(10):
    if num == 3:
        continue  # skip 3
    if num == 7:
        break     # stop at 7
    print(num)
📌 Indentation is Syntax
Python uses 4 spaces of indentation to define code blocks — not curly braces. Getting indentation wrong causes IndentationError. Configure your editor to insert 4 spaces when you press Tab.
Functions

Functions are reusable blocks of code that accept inputs (parameters), do something, and optionally return a result. Good functions do one thing well.

python · functions.py
# ── BASIC FUNCTION ──────────────────────────────────
def greet(name):
    """Return a greeting string for the given name."""
    return f"Hello, {name}! Welcome aboard."

message = greet("Alice")
print(message)  # Hello, Alice! Welcome aboard.

# ── DEFAULT PARAMETERS ──────────────────────────────
def calculate_tax(amount, rate=0.18):
    """Calculate tax. Default rate is 18% (VAT)."""
    return amount * rate

print(calculate_tax(10000))          # 1800.0
print(calculate_tax(10000, 0.15))   # 1500.0

# ── MULTIPLE RETURN VALUES ──────────────────────────
def get_min_max(numbers):
    """Return the smallest and largest values."""
    return min(numbers), max(numbers)

low, high = get_min_max([3, 1, 9, 4, 7])
print(f"Min: {low}, Max: {high}")  # Min: 1, Max: 9

# ── *ARGS: variable number of arguments ─────────────
def add_all(*numbers):
    """Sum any number of values passed in."""
    return sum(numbers)

print(add_all(1, 2, 3, 4, 5))  # 15

# ── **KWARGS: keyword arguments ─────────────────────
def create_profile(**info):
    """Build a profile dict from keyword arguments."""
    return info

profile = create_profile(name="Alice", age=20, city="Kigali")
print(profile)  # {'name': 'Alice', 'age': 20, 'city': 'Kigali'}

# ── LAMBDA: anonymous one-line function ─────────────
square = lambda x: x ** 2
print(square(9))  # 81

numbers = [5, 2, 8, 1]
numbers.sort(key=lambda x: x)
print(numbers)  # [1, 2, 5, 8]
Data Structures

Python has four built-in collection types. Choosing the right one matters for performance and correctness.

python · data_structures.py
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# LIST — ordered, mutable, allows duplicates
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
courses = ["Python", "Web Dev", "Databases"]

courses.append("DevOps")       # add to end
courses.insert(1, "OOP")       # insert at index
courses.remove("Web Dev")      # remove by value
last = courses.pop()           # remove & return last
print(courses[0])              # Python (index access)
print(courses[1:3])           # slicing

# List comprehension — concise and Pythonic
squares = [x ** 2 for x in range(1, 6)]
print(squares)  # [1, 4, 9, 16, 25]

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# TUPLE — ordered, IMMUTABLE, allows duplicates
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
coordinates = (1.9441, 30.0619)  # Kigali lat/lng
lat, lng = coordinates             # unpacking
print(f"Lat: {lat}, Lng: {lng}")

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# SET — unordered, NO duplicates, mutable
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
tags = {"python", "flask", "python", "api"}
print(tags)  # {'python', 'flask', 'api'} — no duplicate

set_a = {1, 2, 3}
set_b = {2, 3, 4}
print(set_a & set_b)   # {2, 3}   — intersection
print(set_a | set_b)   # {1,2,3,4}— union
print(set_a - set_b)   # {1}      — difference

# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# DICTIONARY — key-value pairs, ordered (3.7+)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
student = {
    "name":   "Alice Mugisha",
    "age":    20,
    "grade":  "A",
    "active": True
}

print(student["name"])           # Alice Mugisha
print(student.get("phone", "N/A")) # N/A (safe access)
student["email"] = "alice@school.rw"  # add new key

for key, value in student.items():
    print(f"  {key}: {value}")
TypeOrderedMutableDuplicatesBest For
list []Sequences, collections that change
tuple ()Fixed data, coordinates, records
set {}Unique items, membership testing
dict {k:v}keys: ✗Key-value lookups, JSON-like data
Error Handling

Errors happen. Good code handles them gracefully instead of crashing. Python uses try/except blocks to catch and respond to errors.

python · error_handling.py
# ── BASIC TRY / EXCEPT ──────────────────────────────
try:
    age = int(input("Enter your age: "))
    print(f"In 10 years you'll be {age + 10}")
except ValueError:
    print("Please enter a valid number!")

# ── MULTIPLE EXCEPT BLOCKS ──────────────────────────
def divide(a, b):
    try:
        result = a / b
    except ZeroDivisionError:
        print("Error: Cannot divide by zero!")
        return None
    except TypeError:
        print("Error: Both values must be numbers!")
        return None
    else:
        print(f"Result: {result}")  # runs if no error
        return result
    finally:
        print("Division attempt complete.")  # always runs

divide(10, 2)  # Result: 5.0
divide(10, 0)  # Error: Cannot divide by zero!

# ── CUSTOM EXCEPTIONS ───────────────────────────────
class InsufficientFundsError(Exception):
    """Raised when account balance is too low."""
    pass

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(
            f"Need {amount} but only have {balance}"
        )
    return balance - amount

try:
    withdraw(500, 1000)
except InsufficientFundsError as e:
    print(f"Transaction failed: {e}")
Modules & Packages

A module is a Python file. A package is a directory of modules. They let you organise code and reuse it across projects.

python · modules.py
# ── IMPORTING STANDARD LIBRARY MODULES ──────────────
import math
print(math.sqrt(144))     # 12.0
print(math.pi)             # 3.141592653589793

import datetime
today = datetime.date.today()
print(f"Today: {today}")

import random
print(random.randint(1, 100))  # random int 1–100

# ── IMPORT SPECIFIC ITEMS ───────────────────────────
from math import pi, ceil, floor
print(ceil(4.2))    # 5
print(floor(4.9))   # 4

# ── YOUR OWN MODULE ─────────────────────────────────
# File: utils.py
def format_currency(amount, currency="RWF"):
    return f"{currency} {amount:,.0f}"

# File: main.py
from utils import format_currency
print(format_currency(15000))  # RWF 15,000
2
Intermediate Python
OOP, file handling, virtual environments, and package management
Object-Oriented Programming (OOP)

OOP models real-world entities as objects with data (attributes) and behaviour (methods). It makes code modular, reusable, and easier to maintain.

python · oop.py
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# CLASS & OBJECTS
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class Student:
    """Represents a student in the management system."""

    school_name = "Tech Academy"  # class attribute (shared)

    def __init__(self, name, age, grade):
        # instance attributes (unique per object)
        self.name  = name
        self.age   = age
        self.grade = grade

    def introduce(self):
        """Return a brief introduction string."""
        return f"Hi! I'm {self.name}, age {self.age}, grade {self.grade}."

    def is_passing(self):
        """Check if the student has a passing grade."""
        return self.grade not in ["F", "D"]

    def __repr__(self):
        return f"Student({self.name!r}, {self.grade!r})"


# Creating objects (instances)
alice = Student("Alice", 20, "A")
bob   = Student("Bob",   22, "C")

print(alice.introduce())   # Hi! I'm Alice, age 20, grade A.
print(bob.is_passing())    # True


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# INHERITANCE — reuse and extend existing classes
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class GraduateStudent(Student):  # inherits from Student
    """A student who is also doing research."""

    def __init__(self, name, age, grade, thesis_topic):
        super().__init__(name, age, grade)  # call parent
        self.thesis_topic = thesis_topic

    def introduce(self):  # POLYMORPHISM — override parent method
        base = super().introduce()
        return f"{base} Thesis: {self.thesis_topic}"


grad = GraduateStudent("Claire", 25, "A", "AI in Healthcare")
print(grad.introduce())
print(grad.is_passing())  # inherited method still works


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ENCAPSULATION — hide internal details
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner     = owner
        self.__balance = balance  # __ makes it private

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount

    def get_balance(self):  # controlled access
        return self.__balance

account = BankAccount("Alice", 5000)
account.deposit(2000)
print(account.get_balance())  # 7000
File Handling

Python makes reading and writing files simple. Always use the with statement — it automatically closes the file even if an error occurs.

python · file_handling.py
import json

# ── WRITE a text file ───────────────────────────────
with open("students.txt", "w") as f:
    f.write("Alice Mugisha, A\n")
    f.write("Bob Nkurunziza, C\n")

# ── READ a text file ────────────────────────────────
with open("students.txt", "r") as f:
    for line in f:
        print(line.strip())

# ── APPEND to a file ────────────────────────────────
with open("students.txt", "a") as f:
    f.write("Claire Uwimana, A\n")

# ── JSON files (most common for web data) ───────────
data = {
    "students": [
        {"name": "Alice", "grade": "A"},
        {"name": "Bob",   "grade": "C"}
    ]
}

# Write JSON
with open("data.json", "w") as f:
    json.dump(data, f, indent=2)

# Read JSON
with open("data.json", "r") as f:
    loaded = json.load(f)
    print(loaded["students"][0]["name"])  # Alice
Virtual Environments & pip

A virtual environment is an isolated Python installation for each project. This prevents package version conflicts between projects.

bash · terminal
# Create a virtual environment named 'venv'
python -m venv venv

# Activate it
source venv/bin/activate      # macOS / Linux
venv\Scripts\activate         # Windows

# Install packages
pip install flask
pip install requests
pip install sqlalchemy

# Save dependencies to a file
pip freeze > requirements.txt

# Install from requirements.txt (for teammates)
pip install -r requirements.txt

# Deactivate when done
deactivate
⚠️ Always Use a Virtual Environment
Never install packages globally for a project. Always activate your venv first. Add venv/ to your .gitignore so it's not committed to version control.
3
Python for Web Development
Build web apps with Flask: routing, templates, forms, and REST APIs
Web Concepts

Before writing web code, understand the client-server model and HTTP — the language of the web.

HTTP Methods

GET (read) · POST (create) · PUT (update) · DELETE (remove) — the four actions of REST APIs

Client–Server

Browser (client) sends requests → Server processes and returns responses (HTML, JSON, etc.)

Status Codes

200 OK · 201 Created · 400 Bad Request · 404 Not Found · 500 Server Error

JSON

JavaScript Object Notation — the standard format for API data exchange. Python dicts convert directly to JSON.

Setting Up Flask

Flask is a lightweight Python web framework. It gives you routing, templating, and request handling without imposing a strict structure.

bash · setup
pip install flask flask-sqlalchemy
python · app.py — minimal flask app
from flask import Flask, render_template, request, jsonify

app = Flask(__name__)  # __name__ tells Flask where to look for files

# ── HOME ROUTE ──────────────────────────────────────
@app.route("/")
def home():
    """Render the home page."""
    return render_template("index.html")

# ── ROUTE WITH URL PARAMETER ────────────────────────
@app.route("/student/<int:student_id>")
def get_student(student_id):
    """Return student info as JSON."""
    # In a real app, query the database here
    student = {"id": student_id, "name": "Alice", "grade": "A"}
    return jsonify(student)

if __name__ == "__main__":
    app.run(debug=True)  # debug=True for auto-reload
Routing & Views

Routes map URLs to Python functions. Each function is called a view and returns a response.

python · routes.py
from flask import Flask, request, redirect, url_for, flash

app = Flask(__name__)
app.secret_key = "your-secret-key"  # needed for flash messages

# ── MULTIPLE HTTP METHODS on one route ──────────────
@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get("username")
        password = request.form.get("password")

        if username == "admin" and password == "pass123":
            flash("Login successful!", "success")
            return redirect(url_for("dashboard"))
        else:
            flash("Invalid credentials.", "error")

    return render_template("login.html")


@app.route("/dashboard")
def dashboard():
    return render_template("dashboard.html")


# ── QUERY PARAMETERS: /search?name=alice ────────────
@app.route("/search")
def search():
    query = request.args.get("name", "")
    # Filter students whose name contains query
    results = [s for s in students if query.lower() in s["name"].lower()]
    return jsonify(results)
Templates (Jinja2)

Flask uses Jinja2 templates — HTML files with special tags that let you inject Python data, loop, and apply conditions.

html · templates/students.html
<!-- Base template structure -->
<!DOCTYPE html>
<html lang="en">
<head>
  <title>{{ title }}</title>  <!-- {{ }} for variables -->
</head>
<body>
  <h1>Student List</h1>

  <!-- {% %} for logic -->
  {% if students %}
    <ul>
    {% for student in students %}
      <li>
        <strong>{{ student.name }}</strong> —
        Grade: {{ student.grade }}
        {% if student.grade == "A" %}
          <span>⭐ Top Performer</span>
        {% endif %}
      </li>
    {% endfor %}
    </ul>
  {% else %}
    <p>No students found.</p>
  {% endif %}
</body>
</html>
python · passing data to template
@app.route("/students")
def student_list():
    students = [
        {"name": "Alice", "grade": "A"},
        {"name": "Bob",   "grade": "C"},
    ]
    return render_template(
        "students.html",
        title="All Students",
        students=students
    )
Building a REST API

A REST API is a web service that returns JSON data instead of HTML. It powers mobile apps, frontends, and third-party integrations.

python · api.py — full CRUD REST API
from flask import Flask, jsonify, request, abort

app = Flask(__name__)

# In-memory storage (replace with DB in production)
students = [
    {"id": 1, "name": "Alice", "grade": "A"},
    {"id": 2, "name": "Bob",   "grade": "C"},
]

# ── GET /api/students — list all ────────────────────
@app.route("/api/students", methods=["GET"])
def get_students():
    return jsonify({"students": students, "count": len(students)})

# ── GET /api/students/1 — get one ───────────────────
@app.route("/api/students/<int:sid>", methods=["GET"])
def get_student(sid):
    student = next((s for s in students if s["id"] == sid), None)
    if not student:
        abort(404)  # triggers 404 response
    return jsonify(student)

# ── POST /api/students — create ─────────────────────
@app.route("/api/students", methods=["POST"])
def create_student():
    data = request.get_json()
    if not data or "name" not in data:
        return jsonify({"error": "name is required"}), 400

    new_student = {
        "id":    len(students) + 1,
        "name":  data["name"],
        "grade": data.get("grade", "N/A")
    }
    students.append(new_student)
    return jsonify(new_student), 201  # 201 = Created

# ── PUT /api/students/1 — update ────────────────────
@app.route("/api/students/<int:sid>", methods=["PUT"])
def update_student(sid):
    student = next((s for s in students if s["id"] == sid), None)
    if not student:
        abort(404)
    data = request.get_json()
    student.update(data)
    return jsonify(student)

# ── DELETE /api/students/1 — delete ─────────────────
@app.route("/api/students/<int:sid>", methods=["DELETE"])
def delete_student(sid):
    global students
    students = [s for s in students if s["id"] != sid]
    return jsonify({"message": "Student deleted"}), 200

# ── Error handlers ──────────────────────────────────
@app.errorhandler(404)
def not_found(e):
    return jsonify({"error": "Resource not found"}), 404
4
Database Integration
Connect Python to SQLite and MySQL, perform CRUD, and use SQLAlchemy ORM
Introduction to Databases

A database is an organised system for storing, retrieving, and managing data. Relational databases (SQL) store data in structured tables with relationships.

SQLite

Serverless, file-based DB. Built into Python. Perfect for development, small apps, and prototyping.

MySQL / PostgreSQL

Full client-server databases. Production-ready, handles millions of records and concurrent connections.

sqlite3

Python's built-in module to work with SQLite databases. No installation required.

SQLAlchemy

ORM (Object-Relational Mapper) — write Python code instead of raw SQL. Works with all major DBs.

SQLite — CRUD with sqlite3

The sqlite3 module is built into Python. No installation required — perfect for learning and small projects.

python · database.py — sqlite3 CRUD
import sqlite3
from contextlib import contextmanager

# ── CONNECTION HELPER ───────────────────────────────
@contextmanager
def get_db():
    """Context manager for safe DB connections."""
    conn = sqlite3.connect("school.db")
    conn.row_factory = sqlite3.Row  # rows as dicts
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()

# ── CREATE TABLE ────────────────────────────────────
def init_db():
    """Create tables if they don't exist."""
    with get_db() as conn:
        conn.execute("""
            CREATE TABLE IF NOT EXISTS students (
                id        INTEGER PRIMARY KEY AUTOINCREMENT,
                name      TEXT    NOT NULL,
                email     TEXT    UNIQUE NOT NULL,
                grade     TEXT    DEFAULT 'N/A',
                created   TEXT    DEFAULT CURRENT_TIMESTAMP
            )
        """)

# ── INSERT ──────────────────────────────────────────
def add_student(name, email, grade="N/A"):
    """Add a new student. Uses parameterised query to prevent SQL injection."""
    with get_db() as conn:
        cursor = conn.execute(
            "INSERT INTO students (name, email, grade) VALUES (?, ?, ?)",
            (name, email, grade)  # ← NEVER use string formatting here
        )
        return cursor.lastrowid  # return new ID

# ── SELECT ALL ──────────────────────────────────────
def get_all_students():
    """Return all students as a list of dicts."""
    with get_db() as conn:
        rows = conn.execute("SELECT * FROM students ORDER BY name").fetchall()
        return [dict(row) for row in rows]

# ── SELECT ONE ──────────────────────────────────────
def get_student(student_id):
    """Return a single student by ID."""
    with get_db() as conn:
        row = conn.execute(
            "SELECT * FROM students WHERE id = ?",
            (student_id,)
        ).fetchone()
        return dict(row) if row else None

# ── UPDATE ──────────────────────────────────────────
def update_student(student_id, grade):
    """Update a student's grade."""
    with get_db() as conn:
        conn.execute(
            "UPDATE students SET grade = ? WHERE id = ?",
            (grade, student_id)
        )

# ── DELETE ──────────────────────────────────────────
def delete_student(student_id):
    """Remove a student from the database."""
    with get_db() as conn:
        conn.execute(
            "DELETE FROM students WHERE id = ?",
            (student_id,)
        )

# ── USAGE EXAMPLE ───────────────────────────────────
if __name__ == "__main__":
    init_db()
    add_student("Alice Mugisha", "alice@school.rw", "A")
    add_student("Bob Nkurunziza", "bob@school.rw",   "C")
    students = get_all_students()
    for s in students:
        print(s)
MySQL with mysql-connector

For production applications, MySQL offers better performance, concurrency, and scalability than SQLite.

python · mysql_db.py
pip install mysql-connector-python
python · mysql_db.py
import mysql.connector
from mysql.connector import Error

# ── CONNECTION CONFIG ────────────────────────────────
DB_CONFIG = {
    "host":     "localhost",
    "user":     "root",
    "password": "yourpassword",
    "database": "school_db"
}

def get_connection():
    """Create and return a MySQL connection."""
    return mysql.connector.connect(**DB_CONFIG)

# ── INSERT with parameterised query ─────────────────
def add_student(name, email, grade):
    sql = "INSERT INTO students (name, email, grade) VALUES (%s, %s, %s)"
    try:
        conn = get_connection()
        cursor = conn.cursor()
        cursor.execute(sql, (name, email, grade))  # %s = placeholder
        conn.commit()
        print(f"Student added with ID: {cursor.lastrowid}")
    except Error as e:
        print(f"Database error: {e}")
    finally:
        if conn.is_connected():
            cursor.close()
            conn.close()

# ── SELECT with filtering ────────────────────────────
def search_students(grade):
    sql = "SELECT * FROM students WHERE grade = %s"
    try:
        conn = get_connection()
        cursor = conn.cursor(dictionary=True)
        cursor.execute(sql, (grade,))
        return cursor.fetchall()
    except Error as e:
        print(f"Error: {e}")
        return []
    finally:
        conn.close()
SQLAlchemy ORM

SQLAlchemy lets you define database tables as Python classes. No raw SQL required — it generates queries for you.

python · models.py — SQLAlchemy ORM
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///school.db"
# For MySQL: "mysql+mysqlconnector://user:pass@localhost/school_db"

db = SQLAlchemy(app)

# ── MODEL = TABLE DEFINITION ─────────────────────────
class Student(db.Model):
    """ORM model — each attribute maps to a DB column."""
    __tablename__ = "students"

    id         = db.Column(db.Integer,     primary_key=True)
    name       = db.Column(db.String(100), nullable=False)
    email      = db.Column(db.String(150), unique=True, nullable=False)
    grade      = db.Column(db.String(5),   default="N/A")
    created_at = db.Column(db.DateTime,    default=datetime.utcnow)

    def to_dict(self):
        """Serialise for JSON responses."""
        return {
            "id":    self.id,
            "name":  self.name,
            "email": self.email,
            "grade": self.grade
        }

# ── CRUD with SQLAlchemy ────────────────────────────

# CREATE
with app.app_context():
    db.create_all()  # create tables from models
    student = Student(name="Alice", email="alice@school.rw", grade="A")
    db.session.add(student)
    db.session.commit()

# READ
all_students = Student.query.all()
top_students = Student.query.filter_by(grade="A").all()
one_student  = Student.query.get(1)

# UPDATE
student = Student.query.get(1)
student.grade = "A+"
db.session.commit()

# DELETE
student = Student.query.get(1)
db.session.delete(student)
db.session.commit()
SQL Injection Prevention

SQL injection is one of the most dangerous web vulnerabilities. Always use parameterised queries — never build SQL strings with user input.

python · sql_safety.py
# ❌ DANGEROUS — never do this!
# An attacker could input: ' OR '1'='1
user_input = request.form.get("name")
sql = f"SELECT * FROM students WHERE name = '{user_input}'"
# This could become: SELECT * FROM students WHERE name = '' OR '1'='1'
# Which returns ALL rows — a major security breach!

# ✅ SAFE — use parameterised queries always
cursor.execute(
    "SELECT * FROM students WHERE name = ?",
    (user_input,)  # DB driver escapes the value safely
)

# ✅ SAFE with SQLAlchemy ORM (automatically parameterised)
Student.query.filter_by(name=user_input).first()

# ✅ SAFE with SQLAlchemy text() for raw SQL
from sqlalchemy import text
db.session.execute(
    text("SELECT * FROM students WHERE name = :name"),
    {"name": user_input}
)
🔐 Security Rule #1
Never concatenate user input into SQL strings. Always use placeholders (? for SQLite, %s for MySQL) or the ORM. This is non-negotiable.
5
Mini Project
Student Management System — Flask + SQLite + HTML/CSS
🎯 Capstone Project

Student Management System

A fully functional web app with Flask backend, SQLite database, CRUD operations, and a clean HTML/CSS UI — everything you've learned, integrated.

Project Structure
bash · project layout
student_manager/
├── app.py              # Flask application & routes
├── database.py         # DB connection & CRUD functions
├── requirements.txt    # flask, flask-sqlalchemy
├── templates/
│   ├── base.html       # shared layout
│   ├── index.html      # student list
│   ├── add.html        # add student form
│   └── edit.html       # edit student form
└── static/
    └── style.css       # stylesheet
Database Layer — database.py
python · database.py
import sqlite3
from contextlib import contextmanager

DATABASE = "students.db"

@contextmanager
def get_db():
    conn = sqlite3.connect(DATABASE)
    conn.row_factory = sqlite3.Row
    try:
        yield conn
        conn.commit()
    except:
        conn.rollback()
        raise
    finally:
        conn.close()

def init_db():
    with get_db() as db:
        db.execute("""
            CREATE TABLE IF NOT EXISTS students (
                id      INTEGER PRIMARY KEY AUTOINCREMENT,
                name    TEXT    NOT NULL,
                email   TEXT    UNIQUE NOT NULL,
                age     INTEGER,
                grade   TEXT    DEFAULT 'N/A'
            )
        """)

def get_all_students():
    with get_db() as db:
        rows = db.execute("SELECT * FROM students ORDER BY name").fetchall()
        return [dict(r) for r in rows]

def get_student_by_id(sid):
    with get_db() as db:
        row = db.execute("SELECT * FROM students WHERE id=?", (sid,)).fetchone()
        return dict(row) if row else None

def add_student(name, email, age, grade):
    with get_db() as db:
        db.execute(
            "INSERT INTO students (name, email, age, grade) VALUES (?,?,?,?)",
            (name, email, age, grade)
        )

def update_student(sid, name, email, age, grade):
    with get_db() as db:
        db.execute(
            "UPDATE students SET name=?, email=?, age=?, grade=? WHERE id=?",
            (name, email, age, grade, sid)
        )

def delete_student(sid):
    with get_db() as db:
        db.execute("DELETE FROM students WHERE id=?", (sid,))
Flask App & Routes — app.py
python · app.py — full application
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
from database import init_db, get_all_students, get_student_by_id
from database import add_student, update_student, delete_student

app = Flask(__name__)
app.secret_key = "sms-secret-2024"

# Initialise the database when app starts
with app.app_context():
    init_db()

# ── WEB ROUTES (HTML pages) ─────────────────────────

@app.route("/")
def index():
    """Home page — list all students."""
    students = get_all_students()
    return render_template("index.html", students=students)


@app.route("/add", methods=["GET", "POST"])
def add():
    """Show add form (GET) or process submission (POST)."""
    if request.method == "POST":
        name  = request.form["name"].strip()
        email = request.form["email"].strip()
        age   = request.form.get("age", None)
        grade = request.form.get("grade", "N/A")

        if not name or not email:
            flash("Name and email are required.", "error")
            return redirect(url_for("add"))

        add_student(name, email, age, grade)
        flash(f"{name} added successfully!", "success")
        return redirect(url_for("index"))

    return render_template("add.html")


@app.route("/edit/<int:sid>", methods=["GET", "POST"])
def edit(sid):
    """Edit an existing student record."""
    student = get_student_by_id(sid)
    if not student:
        flash("Student not found.", "error")
        return redirect(url_for("index"))

    if request.method == "POST":
        update_student(
            sid,
            request.form["name"],
            request.form["email"],
            request.form.get("age"),
            request.form.get("grade", "N/A")
        )
        flash("Student updated.", "success")
        return redirect(url_for("index"))

    return render_template("edit.html", student=student)


@app.route("/delete/<int:sid>", methods=["POST"])
def delete(sid):
    """Delete a student (POST only for safety)."""
    delete_student(sid)
    flash("Student deleted.", "info")
    return redirect(url_for("index"))


# ── API ROUTES (JSON responses) ─────────────────────

@app.route("/api/students")
def api_students():
    return jsonify(get_all_students())

if __name__ == "__main__":
    app.run(debug=True, port=5000)
HTML Templates & CSS UI
html · templates/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Student Manager</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
  <div class="container">
    <header>
      <h1>🎓 Student Manager</h1>
      <a href="/add" class="btn btn-primary">+ Add Student</a>
    </header>

    <!-- Flash messages -->
    {% for category, message in get_flashed_messages(with_categories=true) %}
      <div class="alert alert-{{ category }}">{{ message }}</div>
    {% endfor %}

    <!-- Student table -->
    {% if students %}
    <table class="table">
      <thead>
        <tr>
          <th>#</th><th>Name</th><th>Email</th><th>Age</th><th>Grade</th><th>Actions</th>
        </tr>
      </thead>
      <tbody>
      {% for s in students %}
        <tr>
          <td>{{ s.id }}</td>
          <td>{{ s.name }}</td>
          <td>{{ s.email }}</td>
          <td>{{ s.age or '—' }}</td>
          <td>
            <span class="badge {% if s.grade == 'A' %}badge-green{% endif %}">
              {{ s.grade }}
            </span>
          </td>
          <td>
            <a href="/edit/{{ s.id }}" class="btn btn-sm">Edit</a>
            <form method="POST" action="/delete/{{ s.id }}" style="display:inline">
              <button type="submit" class="btn btn-danger btn-sm"
                onclick="return confirm('Delete {{ s.name }}?')">
                Delete
              </button>
            </form>
          </td>
        </tr>
      {% endfor %}
      </tbody>
    </table>
    {% else %}
      <p class="empty">No students yet. <a href="/add">Add one!</a></p>
    {% endif %}
  </div>
</body>
</html>
css · static/style.css
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: system-ui, sans-serif;
  background: #f8f9fa;
  color: #1a1a2e;
}

.container {
  max-width: 960px;
  margin: 40px auto;
  padding: 0 20px;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 28px;
}

.btn {
  padding: 8px 18px;
  border-radius: 6px;
  border: none;
  cursor: pointer;
  font-size: 14px;
  text-decoration: none;
  display: inline-block;
  background: #e5e7eb;
  color: #374151;
  transition: background .2s;
}
.btn-primary { background: #1d6b44; color: white; }
.btn-danger  { background: #dc2626; color: white; }
.btn-sm      { padding: 4px 12px; font-size: 12px; }

.table {
  width: 100%; border-collapse: collapse;
  background: white; border-radius: 12px;
  overflow: hidden; box-shadow: 0 1px 6px rgba(0,0,0,.08);
}
.table th {
  background: #1a1a2e; color: white;
  padding: 12px 16px; text-align: left; font-size: 13px;
}
.table td { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; }
.table tr:last-child td { border-bottom: none; }
.table tr:hover td { background: #f9fafb; }

.badge { padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; }
.badge-green { background: #d1fae5; color: #065f46; }

.alert {
  padding: 12px 18px; border-radius: 8px; margin-bottom: 16px;
}
.alert-success { background: #d1fae5; color: #065f46; }
.alert-error   { background: #fee2e2; color: #991b1b; }
.alert-info    { background: #dbeafe; color: #1e3a5f; }

.empty { text-align: center; padding: 40px; color: #6b7280; }
▶ Run the Project
cd student_managerpython -m venv venv → activate → pip install flaskpython app.py → open http://localhost:5000