Home / Viva prep / MAD1 questions
258 questions with model answers, grouped by topic. Answer out loud first, then open the answer. Tick the ones you can answer; the count is saved in your browser.
HTTP is the application-layer request-response protocol the browser uses to talk to a web server. It is stateless because the server treats every request as independent and does not remember earlier ones. To remember a logged-in user we add state on top using cookies and sessions.
The browser is the client: it sends an HTTP request such as GET /dashboard. The Flask server receives it, runs the matching view, talks to the database and returns an HTML response. The client only renders; all data and business rules live on the server.
GET asks for a resource, puts its parameters in the URL query string, should not change server state and is safe to bookmark or refresh. POST sends data in the request body and is used to create or change something, such as submitting a registration form. Never use GET for actions that modify data, and never put passwords in a URL.
A method is idempotent if repeating the same request leaves the server in the same state as sending it once. GET, PUT, DELETE, HEAD and OPTIONS are idempotent; POST is not, since two POSTs can create two records. This is why a double-click on a booking button needs separate protection.
200 OK, 201 Created, 204 No Content, 301/302 redirects, 400 Bad Request, 401 Unauthorized (not authenticated), 403 Forbidden (authenticated but not allowed), 404 Not Found, 405 Method Not Allowed, 409 Conflict and 500 Internal Server Error. The first digit gives the class: 2xx success, 3xx redirect, 4xx client error, 5xx server error.
The browser resolves the domain to an IP address via DNS, opens a TCP connection (plus a TLS handshake for HTTPS) and sends an HTTP request. The server routes it, runs code, and returns a response with a status line, headers and body. The browser parses the HTML, fetches linked CSS, JS and images, and renders the page.
In https://example.com:8080/books/5?sort=asc#reviews, https is the scheme, example.com the host, 8080 the port, /books/5 the path, sort=asc the query string and reviews the fragment. The fragment is handled only by the browser and is never sent to the server.
A cookie is a small piece of data the server asks the browser to store and send back with every request to that site. A session is the per-user state kept between requests; in Flask the session dict is stored in a signed cookie, while server-side sessions keep only an id in the cookie. Cookies are the transport, sessions are the idea.
Query parameters come after ? in the URL and are read with request.args. Path parameters are part of the route, like /book/<int:id>, and arrive as function arguments. Form data is in the POST body and is read with request.form.
After a successful POST the server replies with a redirect (302/303) to a GET page instead of rendering directly. If the user refreshes, only the harmless GET is repeated, so the form is not resubmitted and no duplicate record is created.
@app.route("/add", methods=["POST"])
def add():
db.session.add(Item(name=request.form["name"]))
db.session.commit()
return redirect(url_for("list_items"))HTTPS is HTTP carried over TLS. It encrypts traffic so passwords and cookies cannot be read on the network, protects against tampering, and lets the browser verify the server's identity through a certificate. Plain HTTP sends everything readable.
A static site serves fixed files that are the same for every visitor. A dynamic site generates the HTML per request using code and a database, for example showing your own bookings. Flask with Jinja2 builds dynamic pages.
In server-side rendering the server builds the final HTML, as Jinja2 does, and the browser just displays it. In client-side rendering the server sends mostly data (JSON) and JavaScript builds the DOM in the browser. MAD1 mainly uses server-side rendering, with JavaScript used for extras.
Headers are key-value metadata sent with requests and responses. Examples are Content-Type (format of the body), Set-Cookie, Cookie, Location (for redirects), Accept and Authorization. They let client and server agree on formats, caching and identity.
GET puts fields in the query string, which ends up in history, logs and referrers. Change the form to POST and make the route accept POST.
<form method="post" action="{{ url_for('login') }}">
@app.route("/login", methods=["GET", "POST"])Typing a URL makes a GET request, and the route does not allow GET, so Flask returns 405 Method Not Allowed. Add "GET" to methods and branch on request.method, or serve the form on a separate GET route. 405 is the correct response when the verb is not supported.
Rendering in the POST leaves the browser URL as /login and a refresh re-sends the credentials. Redirecting to the dashboard sets a clean URL and a GET page, so refresh is safe and the logic of building the dashboard stays in one place.
Return 401 when the caller is not authenticated, meaning there is no valid login or token. Return 403 when the caller is identified but lacks permission, for example a normal user opening an admin route. Mixing them up gives clients the wrong signal.
Open the dev tools Network tab, check whether the CSS request is served from cache, and do a hard reload with the cache disabled. To fix it permanently, change the file name or add a version query like style.css?v=2 so the browser treats it as a new resource.
Semantic HTML uses tags that describe meaning, such as header, nav, main, section, article and footer, rather than generic divs. It helps screen readers, search engines and other developers understand the page structure, and makes code easier to maintain.
Block elements like div, p and h1 start on a new line and take the full width. Inline elements like span, a and strong flow within text and only take their content's width. div is a generic block container and span a generic inline container.
The form tag has action (URL) and method (get/post). On submit, the browser collects every named control as name=value pairs and sends them in the query string or the request body. Controls without a name attribute are not sent.
The name is the key under which the value reaches the server, for example request.form['email']. The id is only used by CSS, JavaScript and label for. A field without a name is silently dropped.
An id must be unique in a page and identifies one element; a class can be reused on many elements and one element can have several. In CSS, ids are selected with # and classes with .. Classes are preferred for styling, ids for anchors and JavaScript hooks.
Every element is a box made of content, padding, border and margin from inside out. Width and height apply to the content box by default, so padding and border add to the total size. box-sizing: border-box makes width include padding and border.
Inline CSS uses the style attribute on a tag, internal CSS sits in a <style> block in the head, and external CSS is a separate .css file linked with <link rel="stylesheet">. External is best for real apps: one file styles many pages and the browser caches it.
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">A selector chooses which elements a rule applies to. p is element, .card class, #main id, nav a descendant (any a inside nav) and ul > li child (direct children only). Pseudo-classes such as a:hover target states.
Inline styles beat ids, ids beat classes (and attributes and pseudo-classes), and classes beat element selectors. If specificity ties, the rule that appears later wins. !important overrides these but should be avoided.
Flexbox lays items out in one dimension, a row or a column, and is great for navbars and centring. Grid is two-dimensional, controlling rows and columns together, and suits page layouts and card galleries. They can be combined.
Responsive design makes a page adapt to different screen sizes. Use the viewport meta tag, flexible units like percentages and rem, media queries such as @media (max-width: 768px), and a grid system like Bootstrap's.
<meta name="viewport" content="width=device-width, initial-scale=1"> tells mobile browsers to use the device width instead of pretending to be a wide desktop. Without it, responsive CSS and Bootstrap breakpoints do not work properly on phones.
Types include text, email, password, number, date, checkbox, radio and file. Attributes like required, minlength, max and pattern give browser-side validation. This only improves user experience; the server must validate again.
Put templates in a templates folder and render them with render_template. Put CSS, JS and images in a static folder and refer to them with url_for('static', filename=...). Flask serves /static/<path> automatically.
Right-click and choose Inspect. The Elements panel shows the DOM and the CSS applied, and lets you edit styles live; the Console shows JS errors; the Network tab shows each request, its status, headers and timing. They are the first stop for debugging layout and request problems.
Use flexbox on the parent with a height.
.parent { display: flex; justify-content: center; align-items: center; height: 100vh; }
For horizontal centring alone, a block with a fixed width can use margin: 0 auto.
Inspect the element in dev tools; the Styles pane shows crossed-out overridden rules. Check specificity, order of stylesheets and typos in the selector or file path. Fix it by making the intended selector more specific or reordering, rather than adding !important.
Use nth-child and :hover, which need no JavaScript.
tr:nth-child(even) { background: #f2f2f2; }
tr:hover { background: #ddd; }Use target="_blank" together with rel="noopener noreferrer" so the new page cannot access window.opener or see the referrer.
<a href="https://example.com" target="_blank" rel="noopener noreferrer">Docs</a>Browsers only send checked checkboxes. An unchecked one has no entry, so request.form['agree'] raises a 400 error. Use request.form.get('agree') or 'agree' in request.form to read it safely.
Jinja2 is a templating engine that mixes HTML with placeholders and simple logic. render_template('page.html', items=items) loads the file from templates, fills in the variables and returns the final HTML string to the browser.
{{ ... }} prints an expression, {% ... %} holds statements like for, if and block, and {# ... #} is a comment that does not appear in the output.
{% for b in books %}
<li>{{ loop.index }}. {{ b.title }}{% if b.available %} (in stock){% endif %}</li>
{% else %}
<li>No books</li>
{% endfor %}
loop.index starts at 1 and the else branch runs when the list is empty.
A base template defines the common layout with named blocks; child templates use {% extends 'base.html' %} and fill the blocks. Anything outside blocks in a child is ignored. This avoids repeating the navbar and CSS links on every page.
{% extends "base.html" %}
{% block content %}<h1>Home</h1>{% endblock %}extends makes the current page a child of a layout and overrides its blocks. include simply inserts another template's rendered output at that spot, which is good for reusable fragments like a footer or a card.
A macro is a reusable template function with parameters. Define it once and call it wherever needed, for example to render a form field or a card.
{% macro card(title, body) %}<div class="card"><h5>{{ title }}</h5><p>{{ body }}</p></div>{% endmacro %}
{{ card("Hi", "Welcome") }}Filters transform a value using a pipe: {{ name|upper }}, {{ items|length }}, {{ x|default('N/A') }}, {{ price|round(2) }}, {{ text|truncate(50) }}. They can be chained. Custom filters can be registered with @app.template_filter.
Flask auto-escapes HTML in variables, turning < into <, so user input cannot inject scripts (XSS). The |safe filter marks text as trusted and turns that protection off, so use it only for content you generated yourself, never user input.
url_for('endpoint', **values) builds a URL from the view function name. If you later change the route path, links update automatically. It also builds static file URLs: url_for('static', filename='style.css').
flash('Saved') in a view stores a message in the session; the next rendered template reads it with get_flashed_messages() and shows it once.
{% with msgs = get_flashed_messages(with_categories=true) %}
{% for cat, m in msgs %}<div class="alert alert-{{ cat }}">{{ m }}</div>{% endfor %}
{% endwith %}Bootstrap is a CSS and JS framework with a responsive grid and ready components such as navbar, cards, buttons and modals. Include it from a CDN with a link and script tag in the base template, or serve local copies from the static folder.
A container holds a row, and a row holds up to 12 columns. Classes like col-12 col-md-6 col-lg-4 set how many columns an element spans at each breakpoint, so layout changes between phone, tablet and desktop.
<div class="row"><div class="col-12 col-md-6">A</div><div class="col-12 col-md-6">B</div></div>A CDN is quick to set up and often cached, but needs internet and depends on a third party. Local files work offline and are under your control but you must ship and update them. For a demo that might run offline, local files are safer.
Pass keyword arguments to render_template; they become variables. Objects work too, so {{ user.name }} accesses attributes. A context processor can add variables to every template.
Put the navbar in base.html and compare request.endpoint to mark the active link.
<a class="nav-link {% if request.endpoint == 'home' %}active{% endif %}" href="{{ url_for('home') }}">Home</a>Register a function with the template_filter decorator.
@app.template_filter("dmy")
def dmy(value):
return value.strftime("%d-%m-%Y")
Use it as {{ booking.date|dmy }}.
Yes, that is autoescaping working: the text is shown as characters instead of running. If it executed, the template must be using |safe or building HTML by string concatenation, which is an XSS bug to fix.
Pass a list of (label, url) pairs from the view, or define a block in the base template that children fill.
<nav><ol class="breadcrumb">
{% for label, url in crumbs %}<li class="breadcrumb-item"><a href="{{ url }}">{{ label }}</a></li>{% endfor %}
</ol></nav>Use loop.cycle to alternate a class name.
{% for r in rows %}<tr class="{{ loop.cycle('odd', 'even') }}">...</tr>{% endfor %}Check that the viewport meta tag is present, that the Bootstrap CSS link loaded (the Network tab shows no 404), and that column classes include a small-screen class such as col-12. A missing viewport tag is the commonest cause.
A database gives structured storage, fast queries with indexes, constraints that keep data valid, transactions for safe concurrent updates and a standard query language. Plain files make searching, relating and protecting data much harder.
SQLite is a serverless, file-based SQL database embedded in the application process. It needs no installation or configuration, comes with Python, and is perfect for a single-machine project. It is less suited to many concurrent writers or large multi-server deployments.
A primary key uniquely identifies each row and cannot be null. A foreign key is a column that refers to the primary key of another table and enforces that the referenced row exists, which is how relationships are stored.
One-to-many: one user has many bookings, so the foreign key user_id sits on the bookings table. Many-to-many: students and courses; you need an association table holding (student_id, course_id) with both as foreign keys, optionally with extra columns like enrolled date.
An entity-relationship diagram shows entities (tables), their attributes, and the relationships between them with cardinality such as 1:N or M:N. It is the design blueprint you draw before creating tables.
Normalization organises tables to reduce redundancy and update anomalies. 1NF: each cell holds one atomic value. 2NF: no partial dependence of non-key columns on part of a composite key. 3NF: no non-key column depends on another non-key column.
Atomicity: a transaction fully happens or not at all. Consistency: it moves the database from one valid state to another. Isolation: concurrent transactions do not see each other's partial work. Durability: committed data survives crashes.
NOT NULL, UNIQUE, PRIMARY KEY and REFERENCES are constraints enforced by the database.
CREATE TABLE booking (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES user(id),
slot TEXT NOT NULL,
UNIQUE (user_id, slot)
);INNER JOIN returns only rows that match in both tables; LEFT JOIN returns all rows from the left table with NULLs when there is no match. The query below lists every user including those with zero bookings.
SELECT u.name, COUNT(b.id)
FROM user u LEFT JOIN booking b ON b.user_id = u.id
GROUP BY u.id;WHERE filters rows before grouping, HAVING filters groups after aggregation. For example GROUP BY user_id HAVING COUNT(*) > 3 keeps only users with more than three rows.
DELETE removes selected rows and can be rolled back in a transaction. DROP removes the whole table and its structure. TRUNCATE empties a table quickly in other databases, but SQLite has no TRUNCATE; you use DELETE FROM table.
A transaction groups several statements so they commit together or roll back together. You need it when steps depend on each other, for example reducing stock and inserting an order; if one fails, both must be undone.
An index is a sorted structure (a B-tree in SQLite) on one or more columns that makes lookups and sorting faster. The cost is extra disk space and slower inserts and updates, since the index must be maintained.
It automatically deletes child rows when the parent row is deleted. SQLite does not enforce foreign keys unless you run PRAGMA foreign_keys = ON for each connection, so cascades silently do nothing without it.
Underline the nouns to get entities, decide attributes and a primary key for each, then find relationships between entities and their cardinality. Resolve M:N with an association table, then check for redundancy using normalization.
Group by user, count, sort descending and limit.
SELECT user_id, COUNT(*) AS n
FROM booking
GROUP BY user_id
ORDER BY n DESC
LIMIT 3;A comma separated value breaks first normal form: you cannot index, search or join on individual tags, and constraints cannot be applied. Use a separate tag table and a post_tag association table.
Use a LEFT JOIN and filter NULLs, or NOT EXISTS.
SELECT a.* FROM a LEFT JOIN b ON b.a_id = a.id WHERE b.id IS NULL;Use ALTER TABLE. Note that create_all() will not alter existing tables, so in a real app use a migration tool such as Flask-Migrate.
ALTER TABLE user ADD COLUMN phone TEXT;An ORM maps Python classes to tables and instances to rows, so you write Python instead of SQL strings. SQLAlchemy also builds parameterised queries, which prevents SQL injection, and makes switching databases easier.
The engine manages the connection to the database. A session is a workspace that tracks objects, collects changes and sends them in a transaction on commit. A model is a class mapped to a table whose attributes are columns.
engine = create_engine("sqlite:///app.db")
Session = sessionmaker(bind=engine)Subclass db.Model and declare columns with db.Column; relationships are declared with db.relationship.
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(120), unique=True, nullable=False)
bookings = db.relationship("Booking", backref="user")Set SQLALCHEMY_DATABASE_URI, create db = SQLAlchemy(app) and call db.create_all() inside an app context. create_all creates only missing tables and never alters existing ones.
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///app.db"
db = SQLAlchemy(app)
with app.app_context():
db.create_all()flush sends pending SQL to the database inside the open transaction without ending it. commit flushes and makes the transaction permanent. rollback discards uncommitted changes, and is needed after an error.
first() returns None when nothing matches, while first_or_404() aborts with 404.
db.session.get(User, 1)
User.query.filter_by(email=e).first()
Booking.query.filter(Booking.price > 100).order_by(Booking.date.desc()).all()
Booking.query.filter_by(user_id=1).count()relationship creates a Python-level link between models using the foreign key. backref automatically adds the reverse attribute on the other class, while back_populates declares both sides explicitly. The foreign key column is still required.
Cascade controls what happens to related objects when the parent changes. cascade='all, delete-orphan' deletes children when the parent is deleted or when a child is removed from the parent's collection. It acts in Python; database-level ON DELETE is separate.
By default related rows load lazily on first access, so looping over 100 users and reading user.bookings issues 1 query plus 100 more. Fix it with eager loading using joinedload or selectinload.
from sqlalchemy.orm import joinedload
User.query.options(joinedload(User.bookings)).all()func.count, func.sum and func.avg map to the SQL aggregates.
from sqlalchemy import func
rows = (db.session.query(User.name, func.count(Booking.id))
.join(Booking).group_by(User.id).all())It sends user values as bound parameters, separate from the SQL text, so input is never interpreted as SQL. The danger returns if you build raw strings with f-strings; for raw SQL use text() with bind parameters.
Add unique=True on the column or a UniqueConstraint for a combination. The database then rejects duplicates with an IntegrityError even under race conditions, which a check in Python alone cannot guarantee.
Flask-Migrate wraps Alembic to version-control schema changes. flask db init creates the migrations folder, flask db migrate autogenerates a script from model changes, flask db upgrade applies it and flask db downgrade reverts the latest one. It changes the schema without dropping your data.
Edit the model, then generate and apply a migration. Check the generated script first; for SQLite a non-null new column needs a default.
flask db migrate -m "add phone"
flask db upgradeCatch the IntegrityError, roll back and tell the user.
from sqlalchemy.exc import IntegrityError
try:
db.session.add(User(email=email)); db.session.commit()
except IntegrityError:
db.session.rollback(); flash("Email already used")Add a boolean column and filter by it. Deleting becomes item.is_deleted = True followed by commit.
is_deleted = db.Column(db.Boolean, default=False)
Item.query.filter_by(is_deleted=False).all()A previous flush failed and the session is still in a failed state. Call db.session.rollback() in the except block so the session can be used again. Always rollback after a failed commit.
Flask-SQLAlchemy needs the app context to know which app and database to use. Wrap the call in with app.app_context(): or run it from the Flask shell.
create_all will not change existing tables, and SQLite supports limited ALTER. Use a migration with batch mode (render_as_batch=True), which recreates the table and copies data, and back up the database first.
Flask is a lightweight Python web microframework built on Werkzeug (WSGI and routing) and Jinja2 (templates). It provides routing, request and response objects, sessions and templates, and leaves choices like database and forms to extensions.
Model holds data and rules (SQLAlchemy classes), View is what the user sees (Jinja templates), Controller handles requests and ties them together (Flask route functions). Flask itself calls the controller a view function, so some people describe it as MVT; the separation of concerns is the same.
A route maps a URL pattern to a function with a decorator. Converters like int, string and path validate and convert the URL part.
@app.route("/book/<int:book_id>", methods=["GET", "POST"])
def book(book_id):
...request.form has POST form fields, request.args has query-string parameters and request.get_json() parses a JSON body. Use .get() to avoid a 400 when a key is missing.
url_for('endpoint') builds the URL and redirect(url) returns a 302 response with a Location header. Together they let you move to another page without hard-coding paths.
session is a dict-like object stored in a cookie that Flask signs with SECRET_KEY, so the user cannot alter it undetected. It is signed, not encrypted, so never store secrets in it. Without a key Flask raises an error when you use the session.
A Blueprint groups related routes, templates and static files into a module that is registered on the app. It keeps larger projects organised, for example separate auth, admin and api blueprints.
A function create_app(config) that builds and returns the app and initialises extensions. It allows different configs for testing and production and avoids circular imports.
abort(404) raises an HTTP exception immediately. @app.errorhandler(404) registers a function that returns a custom page for that error.
@app.errorhandler(404)
def not_found(e):
return render_template("404.html"), 404before_request functions run before each view, for example to load the current user. after_request runs after the view and can modify the response. g stores data for the duration of one request only.
A decorator is a function that wraps another function to add behaviour.
from functools import wraps
def login_required(f):
@wraps(f)
def wrapper(*a, **kw):
if "user_id" not in session:
return redirect(url_for("login"))
return f(*a, **kw)
return wrapperA top-level app.py or package with models.py, routes (or blueprints), a templates folder with base.html, a static folder for css, js and images, config.py, requirements.txt and the SQLite database file. Separate files keep models, logic and views apart.
Use a form with enctype="multipart/form-data", read request.files['file'], sanitise the name with secure_filename, check the extension and size, and save to an upload folder.
It enables the auto-reloader and the interactive debugger. The debugger lets anyone who reaches an error page execute code on the server, so it must be off in production.
Create templates/about.html extending the base, add a route, and link to it in the navbar with {{ url_for('about') }}.
@app.route("/about")
def about():
return render_template("about.html")Flask treats a rule defined with a trailing slash (/items/) as a folder and redirects /items to it, while a rule defined without the slash does not match /items/. Choose one form consistently, or set strict_slashes=False.
Make a decorator that checks the role and aborts with 403.
def admin_required(f):
@wraps(f)
def w(*a, **k):
if session.get("role") != "admin":
abort(403)
return f(*a, **k)
return wFlask uses the function name as the endpoint and endpoints must be unique. Rename one function or give an explicit endpoint= argument. This also bites decorators that forget functools.wraps, because every wrapped function then gets the name wrapper.
Filter the query by the session id rather than any id given in the URL.
uid = session["user_id"]
bookings = Booking.query.filter_by(user_id=uid).all()REST is an architectural style where resources are identified by URLs and manipulated with standard HTTP methods, with stateless requests and usually JSON representations. For example GET /api/books/5 returns book 5.
Create is POST (201 Created), Read is GET (200), Update is PUT for full replacement or PATCH for partial (200), and Delete is DELETE (204 or 200). Errors use 400, 404 or 409 as appropriate.
Use plural nouns for resources, not verbs, and nest only when meaningful: /api/users/5/bookings rather than /getBookings. Use query strings for filtering and sorting, and version the API like /api/v1/.
OpenAPI is a machine-readable standard to describe a REST API: servers, paths, methods, parameters, request bodies, responses and schemas. Tools like Swagger UI render it as interactive docs and can generate client code and tests.
paths:
/api/books/{id}:
get:
responses:
"200": {description: A book}
"404": {description: Not found}Flask-RESTful lets you write a class per resource with methods named get, post, put and delete; the HTTP method picks the function. You register it with api.add_resource.
class BookAPI(Resource):
def get(self, id):
...
api.add_resource(BookAPI, "/api/books/<int:id>")reqparse.RequestParser declares expected request arguments with type, required flag and help message and validates them, returning 400 with an error message if they are wrong.
parser = reqparse.RequestParser()
parser.add_argument("title", type=str, required=True, help="title needed")
args = parser.parse_args()It formats the returned object according to a dict of fields, converting SQLAlchemy objects into JSON with only the chosen fields.
book_book_fields = {"id": fields.Integer, "title": fields.String}
@marshal_with(book_fields)
def get(self, id):
return Book.query.get_or_404(id)Return a tuple of data and status, or raise abort(404, message="Not found") from flask_restful.
return {"message": "created"}, 201PUT replaces the whole resource and sends all fields; PATCH changes only the supplied fields. Both are normally idempotent when used correctly.
jsonify creates a JSON response with the right Content-Type header. Newer Flask versions also convert a returned dict to JSON automatically, and Flask-RESTful serialises returned dicts for you.
It lets other clients such as JavaScript, a mobile app or scripts use the same data in JSON, separates backend from frontend and is easy to test with curl or Postman.
Use curl, Postman or Python requests, or the Swagger UI from the OpenAPI spec. For automated checks use the Flask test client in pytest.
curl -X POST http://localhost:5000/api/books -H "Content-Type: application/json" -d '{"title":"X"}'def delete(self, id):
book = db.session.get(Book, id)
if not book:
abort(404, message="Book not found")
db.session.delete(book); db.session.commit()
return "", 204Validate with reqparse (required, type, choices) or manual checks and return 400 with a message before touching the database. A 500 means an unhandled exception on the server; a client mistake should be a 4xx.
Read the argument and filter.
q = request.args.get("q", "")
Book.query.filter(Book.title.ilike(f"%{q}%")).all()When the request is valid but conflicts with current state, such as creating a user whose unique email already exists or booking a slot already taken. Return 409 with a message rather than a generic 400 or 500.
Use fields.List(fields.Nested(...)).
book_fields = {"id": fields.Integer, "reviews": fields.List(fields.Nested(review_fields))}An index is a separate sorted structure that lets the database find rows without scanning the whole table. Create it for columns you often filter, join or sort on.
CREATE INDEX idx_booking_user ON booking(user_id);
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True)Run EXPLAIN QUERY PLAN before the query in SQLite. SEARCH ... USING INDEX means the index is used, while SCAN means a full table scan.
EXPLAIN QUERY PLAN SELECT * FROM booking WHERE user_id = 5;On tiny tables, on columns with very few distinct values such as a boolean, when the query wraps the column in a function or uses a leading wildcard like LIKE '%abc', or on write-heavy tables where the maintenance cost outweighs the gain.
A composite index covers several columns, for example (user_id, date). Order matters: it helps queries filtering on the leftmost column or on both, but not on date alone.
WHERE title LIKE '%word%' matches a substring and ilike in SQLAlchemy is case-insensitive. A leading wildcard cannot use a normal index, so it scans every row, and there is no ranking or word-based matching.
FTS5 is a SQLite extension for full-text search. You create a virtual table with the columns to search and query it with MATCH, which gives fast word-based search and ranking, unlike LIKE.
CREATE VIRTUAL TABLE docs USING fts5(title, body);
SELECT * FROM docs WHERE docs MATCH 'flask' ORDER BY rank;Caching stores the result of expensive work so repeated requests are served faster. It can be in the browser (HTTP cache headers), in the app (Flask-Caching, memoised functions), in a store like Redis, or at a proxy or CDN. The trade-off is stale data.
Configure a cache type, then decorate a view or function with a timeout.
cache = Cache(app, config={"CACHE_TYPE": "SimpleCache"})
@app.route("/stats")
@cache.cached(timeout=60)
def stats(): ...
SimpleCache lives in process memory; use Redis for multiple workers.
Invalidation means removing or updating cached data when the source changes. It is hard because you must know every write that affects the cached value. Simple strategies are short timeouts or deleting the key on updates with cache.delete.
From low to high: DEBUG, INFO, WARNING, ERROR, CRITICAL. A logger prints messages at or above its level.
import logging
logging.basicConfig(level=logging.INFO, filename="app.log",
format="%(asctime)s %(levelname)s %(message)s")
app.logger.info("User %s logged in", user_id)Logging has levels, timestamps, destinations such as files, and can be switched off or tuned without editing code. In production print output may be lost, while logs support diagnosing problems later.
Read the traceback first, add log lines, and use breakpoint() to open pdb where commands like n, s, c, p var and l step through code. The flask shell command opens a Python shell with the app context so you can try queries interactively.
Pagination returns results in chunks instead of loading everything. It reduces memory and response time.
page = Item.query.paginate(page=1, per_page=10)
page.items, page.has_nextLog or count queries; this looks like an N+1 problem. Use a join or eager loading, add an index on the foreign key and paginate. Confirm with EXPLAIN QUERY PLAN and compare timings.
Log the username and time, never the secret.
app.logger.warning("Failed login for %s from %s", email, request.remote_addr)Use logging.exception or app.logger.exception inside an except block, which records the message at ERROR level together with the traceback.
try: risky()
except Exception:
app.logger.exception("risky failed")Delete or refresh the cache key after the write, for example cache.delete('stats') after commit, or shorten the timeout. Decide how much staleness the page can tolerate.
The Document Object Model is the browser's tree representation of the HTML page. JavaScript can read and change it, for example changing text, adding elements or reacting to clicks.
const el = document.getElementById("msg");
el.textContent = "Saved";
document.querySelector(".btn").classList.add("active");
querySelector accepts any CSS selector; textContent is safer than innerHTML for user text.
Attach a listener with addEventListener.
document.getElementById("btn").addEventListener("click", () => {
alert("clicked");
});
event.preventDefault() stops default behaviour such as a form submit.
fetch makes HTTP requests from JavaScript and returns a Promise.
fetch("/api/books")
.then(r => r.json())
.then(data => console.log(data));fetch("/api/books", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({title: "X"})
}).then(r => r.json());
Without the Content-Type header, request.get_json() on the server fails.
A Promise represents a value that will arrive later. async/await is syntax to write asynchronous code in a sequential style.
async function load() {
const r = await fetch("/api/books");
const data = await r.json();
}No. It rejects only on network failure. Check response.ok or response.status yourself and handle errors explicitly.
const cannot be reassigned, let is block-scoped and reassignable, var is function-scoped and hoisted, which causes surprises, so prefer const and let.
== converts types before comparing, so 0 == '0' is true. === compares without conversion, so it is false. Prefer ===.
For small enhancements: client-side validation, confirming deletes, live search filter, fetching JSON from your API to update part of a page without reload, or drawing charts. The core logic and validation remain on the server.
<form method="post" action="/delete/5" onsubmit="return confirm('Delete this item?');">
<button class="btn btn-danger">Delete</button>
</form>document.getElementById("q").addEventListener("input", e => {
const q = e.target.value.toLowerCase();
document.querySelectorAll("tbody tr").forEach(tr => {
tr.style.display = tr.textContent.toLowerCase().includes(q) ? "" : "none";
});
});Open the Network tab, click the request and check the request payload, Content-Type header and the response body, which usually contains the error message. Also check the Console for JavaScript errors.
The script may run before the element exists, so place it at the end of body or use defer, or the id or selector may be misspelled. Check the Elements panel for the real id.
Authentication verifies who you are, for example by password. Authorization decides what you are allowed to do, for example whether you can open the admin page. Both are needed.
Only as salted hashes produced by a slow hashing function, never plain text. Werkzeug provides this.
from werkzeug.security import generate_password_hash, check_password_hash
h = generate_password_hash("secret")
check_password_hash(h, "secret")A hash is a one-way function, so you verify by hashing the input again and comparing. A random salt per password makes identical passwords produce different hashes and defeats precomputed rainbow tables.
After checking credentials you store the user id in session; later requests carry the signed cookie so the server knows who the user is. Logout clears the session with session.clear().
Users are assigned roles such as admin, customer or sponsor, and each route or action is allowed only for particular roles. It is enforced in the backend with decorators, not by merely hiding links.
Flask-Security (Flask-Security-Too) adds registration, login, logout, password hashing, roles and role-protected routes on top of Flask-Login. You define User and Role models and use decorators like @login_required and @roles_required('admin').
Cross-site scripting is injecting script into a page so it runs in another user's browser. Prevent it by escaping output, which Jinja2 does by default, avoiding |safe on user input and avoiding innerHTML with user text.
Cross-site request forgery makes a logged-in user's browser send an unwanted request to your site. Defend with a secret per-form token (Flask-WTF CSRFProtect), SameSite cookies and using POST for state changes.
It is when user input is concatenated into SQL and changes the query, for example ' OR 1=1 --. Use the ORM or parameterised queries so input is only data.
Insecure direct object reference means a user changes an id in the URL to access someone else's record. Always check ownership on the server by filtering with the logged-in user's id, and return 403 or 404 otherwise.
Anyone can bypass the browser using dev tools, curl or a script. The server must validate every input for type, length, uniqueness and permission.
HttpOnly stops JavaScript reading the cookie, Secure sends it only over HTTPS and SameSite limits cross-site sending. In Flask: SESSION_COOKIE_HTTPONLY, SESSION_COOKIE_SECURE, SESSION_COOKIE_SAMESITE.
Read them from environment variables or an untracked config file and add that file to .gitignore.
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]The route lacks a server-side role check. Add a decorator that verifies the logged-in role and aborts with 403, and also test it manually and in pytest.
@app.route("/admin")
@login_required
@roles_required("admin")
def admin(): ...Hash on registration with the new method and verify with check_password_hash, which reads the method from the stored hash. For stronger scheme migration, rehash the password on the next successful login.
The Flask session cookie is signed, so tampering invalidates it, while a plain id cookie could be edited to impersonate someone. Still, the session is only signed, so do not store sensitive data in it.
A unit test checks one small piece such as a function in isolation. A functional (integration) test exercises a feature through several layers, for example posting a form through the test client and checking the response and database.
pytest finds functions named test_* and uses plain assert statements.
def test_add():
assert 1 + 2 == 3
Run with pytest -v.
A fixture is reusable setup and teardown code injected into tests by name. A typical one creates an app and an empty test database.
@pytest.fixture
def client():
app = create_app({"TESTING": True, "SQLALCHEMY_DATABASE_URI": "sqlite://"})
with app.app_context():
db.create_all()
yield app.test_client()Use the test client and assert on status code and content.
def test_home(client):
r = client.get("/")
assert r.status_code == 200
assert b"Welcome" in r.datadef test_login(client):
r = client.post("/login", data={"email": "a@b.com", "password": "x"},
follow_redirects=True)
assert r.status_code == 200
follow_redirects=True follows the 302 to the final page.
Tests must not damage real data and must start from a known state so they are repeatable. In-memory SQLite (sqlite://) or a temporary file is created and thrown away for each test.
Coverage is the percentage of lines your tests execute. Run coverage run -m pytest then coverage report, or use pytest --cov=app. High coverage does not prove correctness but low coverage shows untested code.
Duplicate registration, empty or invalid fields, nonexistent ids, double booking the same slot, unauthorised access to another user's data and admin routes, and deleting a parent with children.
Send requests with the test client and check status code and JSON.
r = client.post("/api/books", json={"title": "X"})
assert r.status_code == 201
assert r.get_json()["title"] == "X"Manual testing means trying scenarios yourself in a browser. It finds layout and usability problems that automated tests miss, but is slow and not repeatable, so combine with automated tests.
Tests share state such as the database. Make each test use a fresh fixture with its own app and tables, and avoid depending on test order.
def test_admin_forbidden(client, login_user):
r = client.get("/admin")
assert r.status_code == 403The code touching the database needs an app context. Create the app in the fixture and use with app.app_context(): around setup and queries.
<!DOCTYPE html> tells the browser to use standards mode for HTML5. Without it browsers may render in quirks mode with older, inconsistent behaviour.
Semantic elements include header, nav, main, section, article, aside and footer. Forms gained types like email, date, number, range and attributes such as required, placeholder and pattern. HTML5 also has audio, video and canvas.
Accessibility means people with disabilities, for example screen reader or keyboard-only users, can use the site. It matters for fairness, legal reasons and also improves usability and SEO for everyone.
Use semantic tags, give images alt text, connect labels to inputs, keep sufficient colour contrast, ensure everything works via keyboard with visible focus, and use ARIA only when native elements are not enough.
Use for matching the input's id, or wrap the input in the label.
<label for="email">Email</label>
<input type="email" id="email" name="email">
Clicking the label focuses the input, and screen readers announce it.
It provides text for an image when it cannot be seen, read out by screen readers and shown if the image fails to load. Decorative images get an empty alt="".
Encoding maps characters to bytes. UTF-8 can represent every Unicode character, is backward-compatible with ASCII and is the web standard. Declare it with <meta charset="utf-8"> so non-English text displays correctly.
ASCII has 128 characters, mainly English letters and symbols. Unicode assigns a code point to characters of all scripts and emoji; UTF-8 is one way to encode those code points into bytes.
Entities represent reserved or special characters, for example < for <, >, & for & and ". They stop the browser treating text as markup and are what Jinja2 autoescaping outputs.
ARIA attributes such as role, aria-label and aria-live add accessibility information to elements where plain HTML cannot express it. The first rule of ARIA is to use a native element like button if one exists.
Special characters in URLs are replaced with percent codes, for example a space becomes %20 and & becomes %26. Query values should be encoded, and Flask decodes them in request.args.
Give the button an accessible name with aria-label or visible hidden text.
<button aria-label="Delete item"><i class="bi bi-trash"></i></button>Ensure the HTML has <meta charset="utf-8">, the file is saved as UTF-8, and the response Content-Type charset is UTF-8. Also check that the database and any file reads use UTF-8.
Colour-blind users or screen readers may miss it. Add text such as 'Error: email required', an icon, and link it to the field with aria-describedby.
WSGI (Web Server Gateway Interface) is the standard Python interface between web servers and applications. Flask is a WSGI app, so servers like gunicorn can call it. Flask's built-in server is only for development.
The development server is single-purpose, not hardened and weak under load. Use a production WSGI server such as gunicorn with several workers, normally behind nginx.
gunicorn -w 4 -b 127.0.0.1:8000 app:appIt acts as a reverse proxy: handles HTTPS, serves static files efficiently, buffers slow clients and forwards dynamic requests to gunicorn. This improves security and performance.
A virtual environment is an isolated Python installation per project so package versions do not clash.
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txtIt lists the project's dependencies so anyone can reproduce the environment. Generate it with pip freeze > requirements.txt and install with pip install -r requirements.txt.
Docker packages the app with its dependencies into an image that runs the same on any machine as a container. It removes 'works on my machine' problems and simplifies deployment.
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]An image is the read-only template built from the Dockerfile; a container is a running instance of an image.
git init
git add .
git commit -m "message"
git branch feature
git checkout feature
git push origin main
Git tracks changes so you can see history, work on branches and undo mistakes.
Files that should not be versioned: the virtual environment, __pycache__, .db files, uploaded files, .env and secrets, and editor settings.
Use separate config classes or environment variables for debug flag, database URI and secret key, and select by environment. Never commit production secrets.
Copy the code, create a virtualenv, install requirements, set environment variables, run migrations, start gunicorn under a process manager such as systemd, and configure nginx as the reverse proxy with HTTPS.
Missing or version-mismatched dependencies (update requirements.txt), hard-coded absolute paths, a database that is not created at startup, a missing environment variable, or OS-specific path separators. Provide clear run instructions and test them in a fresh virtualenv.
Remove the file from tracking with git rm --cached, add it to .gitignore and commit. A secret that was pushed must be treated as leaked: rotate it, since history still contains it.
flask run --port 5001 --host 0.0.0.0
Or app.run(port=5001). Binding to 0.0.0.0 exposes the dev server to the network, so do it only deliberately.
State the problem it automates, name the roles such as admin and user, and list the main features in one breath, for example registration, browsing, booking and an admin dashboard. Then name the stack: Flask, Jinja2, SQLite with SQLAlchemy, Bootstrap. Keep it under thirty seconds and let them ask for detail.
SQLite is serverless, needs no setup, ships with Python and is a single file, so the examiner can run the project anywhere. For the scale of this project, with modest concurrent writes, it is enough. If it grew I would move to PostgreSQL by changing the connection URI, as SQLAlchemy abstracts the database.
Flask is lightweight and explicit, so I understand every part of the request flow: routing, templates and database. It suits a small to medium project and the course teaches it. Django offers an admin and ORM out of the box, but that hides things I wanted to learn.
Name each table, its primary key and the relationships: for example User 1-N Booking N-1 Slot. Explain that repeated information is stored once and referenced by foreign keys, which follows third normal form, and mention the constraints you added like unique and not null.
Every protected route requires login and every query is filtered by the logged-in user's id from the session, never by an id trusted from the URL alone. For a record opened by id, I check record.user_id == current_user.id and abort with 403 or 404 otherwise. I tested this by trying another user's URLs.
b = Booking.query.get_or_404(id)
if b.user_id != session["user_id"]:
abort(403)At two levels. In the database a UniqueConstraint on the columns that must be unique, such as (user_id, slot_id) or email, guarantees it even with two simultaneous requests. In code I check first for a friendly message and also catch IntegrityError and roll back.
The admin is created programmatically at first startup if none exists, so there is no public admin signup. Admin routes are guarded by a role check that returns 403 for others, in the backend, not just by hiding menu links.
In the browser with required, types and patterns for quick feedback, and on the server for every field: presence, type, length, uniqueness and permission. The server is the authority because browser checks can be bypassed.
Describe your real implementation: typically a filter with ilike on name or category using a query parameter, optionally ordered and paginated. Say that if data grew, I would add an index or use SQLite FTS5.
Pick one, for example booking: the user clicks a button which is a form POST to a route; the route checks login, validates input, creates a Booking through SQLAlchemy, commits and redirects to the bookings page; the template loops over the user's bookings. Name the template, route function and model at each step.
Either cascade the delete to children with cascade='all, delete-orphan', block deletion while children exist with a friendly message, or soft delete with a flag so history is kept. I chose whichever fits the business rule, for example soft delete for items that appear in past bookings.
Invalid input is caught by validation, which re-renders the form with a flash message. A database error is caught, the session rolled back so nothing half-saved remains, and a friendly message or error page is shown while the details go to the log.
Pagination and caching for large lists, automated pytest tests, a proper token-based REST API with documentation, CSRF protection if not present, migrations with Flask-Migrate, and deployment with gunicorn and Docker.
Give a specific example, such as modelling relationships or preventing double booking. Describe the symptom, how you debugged it using the traceback, logging or the Flask shell, and the fix. A concrete story shows you wrote the code.
Answer honestly. Say what you took from documentation, tutorials or tools, and that you reviewed, tested and understand all code. Then be ready to explain any file you are shown, because that is what the viva checks.
Open the login route, point to the credentials check and the session assignment, then edit the redirect target.
return redirect(url_for("user_dashboard")) # was "home"
Save, restart if needed, and demonstrate the new behaviour.
Add the column to the model, apply the schema change (migration, or recreate the dev database), add the input to the form, read it with request.form, and display it in the template. Mention that existing rows will have a default or NULL.
Without protection both could succeed. A unique constraint or a conditional update inside one transaction makes the database accept only one and raise IntegrityError for the other, which I catch and report as slot taken. Checking in Python alone is not enough because of the race.
@app.route("/hello/<name>")
def hello(name):
return f"Hello, {name}!"
The <name> part of the URL arrives as the function argument. In real pages use a template so the name is escaped.
phone = db.Column(db.String(15))
Run a migration or recreate the database in development, add an input named phone in the form, save it with user.phone = request.form['phone'], and show {{ user.phone }} in the template.
items = Item.query.order_by(Item.created_at.desc()).limit(5).all()
order_by(...desc()) puts the newest first and limit(5) keeps five.
q = request.args.get("q", "")
items = Item.query.filter(Item.name.ilike(f"%{q}%")).all()
<form method="get"><input name="q" value="{{ request.args.get('q','') }}"><button>Search</button></form>A link makes a GET, which should not change data. Use a form.
<form method="post" action="{{ url_for('delete_item', id=item.id) }}"><button>Delete</button></form>
@app.route("/delete/<int:id>", methods=["POST"])Add is_active = db.Column(db.Boolean, default=True) and check it in the login route.
if not user.is_active:
flash("Account blocked"); return redirect(url_for("login"))
Blocking sets the flag to False and commits.
rows = (db.session.query(User.name, func.count(Booking.id))
.outerjoin(Booking).group_by(User.id).all())
In the template loop {% for name, n in rows %}.
__table_args__ = (db.UniqueConstraint("user_id", "slot_id"),)
Then catch IntegrityError around commit, call db.session.rollback() and flash a message.
<div class="row">
{% for c in cards %}
<div class="col-12 col-lg-4 mb-3"><div class="card">...</div></div>
{% endfor %}
</div>flash("Saved successfully", "success")
return redirect(url_for("list_items"))
Show it in base.html with a loop over get_flashed_messages(with_categories=true) and a Bootstrap alert.
page = request.args.get("page", 1, type=int)
p = Item.query.paginate(page=page, per_page=10)
In the template loop p.items and link to url_for('items', page=p.next_num) when p.has_next.
@app.route("/api/items")
def api_items():
return jsonify([{"id": i.id, "name": i.name} for i in Item.query.all()])Pass ?sort=price and map it to a column.
sort = request.args.get("sort", "name")
col = {"name": Item.name, "price": Item.price}.get(sort, Item.name)
items = Item.query.order_by(col).all()
Using a whitelist prevents arbitrary column names.
@app.errorhandler(404)
def nf(e):
return render_template("404.html"), 404
Return the status code 404 with the page so browsers and tools see a real not found.
user = User(email=email, password=generate_password_hash(pw))
...
if user and check_password_hash(user.password, pw): session["user_id"] = user.id
The password column must be long enough, for example 255 characters.
Clear the session and redirect, and add no-store headers on protected pages.
session.clear()
@app.after_request
def nocache(r): r.headers["Cache-Control"] = "no-store"; return rfrom datetime import datetime, timezone
created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
Pass a function so the time is computed per row, not once at import.
Validate on the server.
d = datetime.strptime(request.form["date"], "%Y-%m-%d").date()
if d < date.today():
flash("Date must be today or later"); return redirect(url_for("book"))
Add min on the date input too for the browser.
name = db.Column(db.String(80), index=True)
Then run EXPLAIN QUERY PLAN SELECT * FROM item WHERE name = 'x'; and look for USING INDEX.