Open ConfirmTkt or the IRCTC app a week before Diwali travel season and search a waitlisted train. Next to your ticket you will see something like "Confirmation Chance: High" or a percentage. Nobody at ConfirmTkt is sitting there personally calculating your odds. A model, trained once on years of past bookings, produces a number. A website then takes that number, wraps it in HTML, and shows it to you the moment you hit search. That combination — a trained prediction model on one side, a web page that serves its answers on the other — is exactly what this chapter teaches you to build. We will build a much smaller, fully transparent version of the same idea: a website that predicts a student's exam marks from the number of hours they studied, so you can see every single number the "AI" is using, with nothing hidden.
By the end of this chapter you will have written a real, runnable Flask application that (1) computes a prediction using a formula you derived by hand from real data, (2) accepts input from a user through a URL or a form, and (3) sends back an answer as a web page or as JSON. This is precisely the skeleton that sits underneath ConfirmTkt-style prediction features, recommendation widgets, and "smart" chatbots on Indian e-commerce sites — the machine learning model does the thinking, and a web framework like Flask does the talking.
What a web framework actually does
Before touching machine learning, you need to understand what problem Flask solves, because it is easy to confuse "Flask" with "a website" when they are not the same thing. A website, at the network level, is a conversation between two programs. Your browser is one program. Somewhere on the internet (or, while you are learning, on your own laptop) a second program is running, listening for messages. Your browser sends a small text message called an HTTP request — it typically says something like "GET me the page at this address." The listening program reads that message, decides what to do, and sends back an HTTP response — usually a chunk of HTML text, which your browser then paints on screen as a page.
Flask is a Python library that writes the "listening program" for you. Without Flask, you would have to handle raw network sockets, parse text-based HTTP messages by hand, and manage many simultaneous visitors — a genuinely hard systems-programming problem. Flask hides all of that behind a simple idea: you write ordinary Python functions, and you tell Flask which web address should trigger which function. Flask created by Armin Ronacher, a German developer, and first released in 2010 as a deliberately small "microframework" — it does routing and request/response handling well, and leaves you free to add only the extra pieces (databases, authentication, and so on) that your particular project actually needs, rather than forcing a huge structure on you from day one.
Here is the smallest possible Flask application:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello from Flask!"
if __name__ == "__main__":
app.run(debug=True)
Read this line by line, because every later example builds on it. Flask(__name__) creates one Flask application object; __name__ just tells Flask where this file lives on disk, so it can find related files later. The line @app.route("/") is a decorator — it registers the function immediately below it, home(), as the code that should run whenever a browser requests the address / (the site's root page). When you run this file and visit http://127.0.0.1:5000/ in a browser, Flask receives the request, matches it to the / route, calls home(), gets back the string "Hello from Flask!", and sends that string back as the HTTP response. The browser displays it as plain text. This single decorator-plus-function pattern — one URL, one Python function, one return value — is the entire mental model you need for routing in Flask.
From a table of numbers to a prediction formula
Now for the part that makes this an ML-powered app rather than a static page. Suppose five students recorded how many hours they studied for a Class 9 mathematics test and the marks (out of 100) they scored:
Hours studied (x): 1 2 3 4 5
Marks scored (y): 37 53 58 65 77
A pattern is visible immediately: more hours generally means more marks, and the increase looks roughly steady. Our goal is to turn that visible pattern into a formula: a straight line y = m·x + c that lets us predict marks for a new student given only their study hours, even for hours-values that never appeared in the table. This is the simplest possible machine learning model — linear regression — and working through it by hand once is the fastest way to stop treating "the model" as a magic black box.
The method: find the line that passes as close as possible to all five points. The standard way to compute the best-fitting m (the slope) and c (the intercept) is:
m = sum((x - mean_x) * (y - mean_y)) / sum((x - mean_x)^2)
c = mean_y - m * mean_x
Do not just accept that formula — trace it with the actual numbers. First, the two means:
mean_x = (1+2+3+4+5) / 5 = 15/5 = 3
mean_y = (37+53+58+65+77) / 5 = 290/5 = 58
Next, for every student, find how far their hours and marks sit from those two averages — the "deviations":
x: 1 2 3 4 5
x - 3: -2 -1 0 1 2
y: 37 53 58 65 77
y - 58: -21 -5 0 7 19
Now multiply each pair of deviations together, and separately square each x-deviation:
(x-3)*(y-58): (-2)(-21)=42 (-1)(-5)=5 (0)(0)=0 (1)(7)=7 (2)(19)=38
sum = 42 + 5 + 0 + 7 + 38 = 92
(x-3)^2: 4 1 0 1 4
sum = 4 + 1 + 0 + 1 + 4 = 10
Now the formula is just arithmetic:
m = 92 / 10 = 9.2
c = 58 - (9.2 * 3) = 58 - 27.6 = 30.4
So our trained model is marks = 9.2 × hours + 30.4. Notice what "training" actually meant here: nothing mysterious happened, we simply used the five known (hours, marks) pairs to solve for the two numbers, 9.2 and 30.4, that make a straight line fit the data as closely as possible. Those two numbers, and nothing else, are "the model." Every prediction the finished website makes will just be this line evaluated at a new x.
Let's use it. A student who studied 6 hours:
marks = 9.2 * 6 + 30.4 = 55.2 + 30.4 = 85.6
A student who studied 6.5 hours:
marks = 9.2 * 6.5 + 30.4 = 59.8 + 30.4 = 90.2
Both feel reasonable given the pattern in the table. But watch what happens for 10 hours:
marks = 9.2 * 10 + 30.4 = 92 + 30.4 = 122.4
The formula confidently predicts 122.4 marks out of 100 — an impossible score. This is not a bug in the arithmetic; it is a real and important limitation of the model itself. Our line was fitted using students who studied between 1 and 5 hours. Beyond that range, we are extrapolating — asking the line to make promises about territory it never saw. A real deployed app should clamp predictions to a sensible range (say, 0 to 100) and, more importantly, should be honest that predictions far outside the training data are unreliable. Keep this example in mind: it is the single most common way beginners get burned by their first "working" ML model — it looks fine everywhere they tested it, then produces nonsense the moment a user enters a value outside the data it learned from.
Turning the formula into a Python function
The formula becomes a plain function, completely independent of Flask:
def predict_marks(hours):
m = 9.2
c = 30.4
predicted = m * hours + c
return round(predicted, 1)
Call predict_marks(6) in a Python shell and you get 85.6, matching the hand calculation exactly. This separation matters: predict_marks knows nothing about the web, HTTP, or browsers — it is a pure calculation. Flask's only job will be to connect a URL to this function and format whatever it returns as a response. Keeping the "thinking" part and the "web serving" part as separate pieces of code is good practice in every real ML web app, including large production systems: the model can be tested, reused, and even swapped out (for a proper library-trained model, using something like scikit-learn) without touching a single line of the web-facing code.
Wiring the function into a Flask route
Now connect the two. We want a visitor to go to an address like /predict?hours=6 and see a predicted-marks page.
from flask import Flask, request
app = Flask(__name__)
def predict_marks(hours):
m = 9.2
c = 30.4
return round(m * hours + c, 1)
@app.route("/predict")
def predict():
hours = float(request.args.get("hours", 0))
marks = predict_marks(hours)
return f"<h1>Predicted marks: {marks}</h1>"
if __name__ == "__main__":
app.run(debug=True)
Two new ideas appear here. First, request is a special Flask object that represents the incoming HTTP request currently being handled — Flask fills it in automatically for you before your function runs. Second, request.args is a dictionary-like object holding everything after the ? in the URL, called the query string. When a browser visits /predict?hours=6, request.args.get("hours", 0) reads the text "6" (the second argument, 0, is only used as a fallback if hours is missing entirely). Notice it comes back as text, not a number — everything in a URL is text — so we must explicitly convert it with float(...) before doing arithmetic. Skip that conversion and Python will try to multiply a string by 9.2 and crash with a TypeError.
There is a second, often cleaner way to accept the same input: build it into the URL path itself, using a Flask converter.
@app.route("/predict/<float:hours>")
def predict_path(hours):
marks = predict_marks(hours)
return f"<h1>Predicted marks: {marks}</h1>"
Here <float:hours> tells Flask: "the segment of the URL in this position must look like a decimal number; convert it to a Python float automatically, and pass it into the function as the argument named hours." Visit /predict/6.5 and Flask hands your function hours = 6.5 as a real float, no manual conversion needed. This is where a common mistake shows up: if you had instead written <int:hours> and a visitor requested /predict/6.5, Flask would not pass in a rounded or truncated number — the URL simply fails to match the route at all, because "6.5" is not a valid integer, and Flask automatically returns a 404 Not Found page before your function ever runs. Converters validate the URL shape before your code executes; they do not silently coerce bad input into something your function can limp along with.
The full round trip
It helps to see the entire journey of one request as a single picture, from the moment a student clicks a button to the moment marks appear on their screen.
Trace the loop with your finger: the browser sends a GET request carrying hours=6 as text; the /predict route reads it, converts it to 6.0, and calls predict_marks(6.0); that function returns the number 85.6; the route builds an HTML string containing that number; Flask packages the HTML into an HTTP response with status code 200 OK; and the browser receives that response and renders it, which is what the student actually sees. Every one of the earlier code snippets is one labeled step in this diagram — there is no hidden step and nothing else is happening.
GET, POST, and why forms don't use query strings
So far every input arrived through the URL — either as a query string (?hours=6) or as part of the path (/predict/6.5). Both of those are examples of the HTTP GET method, meant for requests that only fetch data and cause no side effects. GET requests are visible in the address bar, can be bookmarked, and get logged in browser history and server logs — all fine for a marks prediction, but a poor choice once forms start carrying anything sensitive, such as a login password or a long block of typed text, because URLs have length limits and get saved in plaintext in many places.
For a proper HTML form, the standard method is POST, which sends the data inside the body of the HTTP request instead of the URL:
<form action="/predict" method="POST">
<label>Hours studied:</label>
<input type="number" step="0.5" name="hours">
<button type="submit">Predict my marks</button>
</form>
On the Flask side, a route must explicitly opt in to accepting POST requests (Flask only allows GET by default), and the submitted value is read from request.form instead of request.args:
@app.route("/predict", methods=["POST"])
def predict_from_form():
hours = float(request.form["hours"])
marks = predict_marks(hours)
return f"<h1>Predicted marks: {marks}</h1>"
The distinction between request.args (query string, GET) and request.form (form body, POST) trips up almost every beginner at least once — write request.form for a GET-only route, or request.args when the form actually submits as POST, and Flask raises a KeyError because the field you asked for genuinely is not present in that dictionary. When your form breaks with a "missing key" error, check the method on the <form> tag before anything else.
Returning JSON instead of HTML
Not every consumer of your prediction is a human staring at a browser. If another program — say, a mobile app, or a second website — wants to use your prediction, sending back a full HTML page is unhelpful; the other program would have to tear the page apart just to find one number. The standard solution is to return JSON (JavaScript Object Notation), a compact, structured text format that both humans and programs can parse easily. Flask provides a helper called jsonify for exactly this:
from flask import Flask, request, jsonify
app = Flask(__name__)
def predict_marks(hours):
return round(9.2 * hours + 30.4, 1)
@app.route("/api/predict")
def api_predict():
hours = float(request.args.get("hours", 0))
marks = predict_marks(hours)
return jsonify({"hours": hours, "predicted_marks": marks})
Visiting /api/predict?hours=6 now returns not an HTML page but the text {"hours": 6.0, "predicted_marks": 85.6}, along with an HTTP header telling the receiving program "this body is JSON, parse it accordingly." This single change — HTML versus JSON — is the entire practical difference between a route meant for a human's browser and a route meant to be an API endpoint that other software calls. Real-world sites frequently expose both from the same underlying prediction function, exactly as we just did by keeping predict_marks shared between the two routes.
A misconception worth correcting directly: is the website "learning"?
A very natural but incorrect assumption is that every time a student requests a prediction, the Flask app is somehow "doing machine learning" on the spot — analyzing data, learning, getting smarter. It is not. Look back at predict_marks: the numbers 9.2 and 30.4 are fixed constants, computed once, by us, by hand, from the five-student table. Nothing in the request-handling code recalculates them, and nothing about handling one more request changes them for the next visitor.
Machine learning systems have two genuinely separate phases, and confusing them is one of the most common errors beginners make when reasoning about "AI-powered" apps. Training is the expensive, one-time (or occasional) process of looking at historical data and solving for the best parameters — what we did by hand with the deviation-and-sum arithmetic above; in a real project this step usually happens offline, often using a library such as scikit-learn, and can take anywhere from milliseconds to days depending on the data. Serving — which is Flask's entire job in this chapter — is the cheap, repeated process of taking already-learned parameters and plugging a new input into them to get an answer, which is nothing more than one multiplication and one addition per request. A ConfirmTkt-style confirmation predictor does not retrain itself while you wait for a search result; it loads parameters that were trained earlier, offline, on historical booking records, and simply evaluates them against your specific waitlist position and travel date. Flask's role in any such system is always serving, never training, and recognizing that split is what separates a real understanding of "ML-powered web apps" from a hand-wavy one.
From a hardcoded formula to a real project
Our five-row table and hand-computed m and c were deliberately small enough to verify by hand — that was the point, so you could trust every number instead of taking a library's word for it. A real project with thousands of students would use a library such as scikit-learn to fit the line (the underlying arithmetic is the same idea, just automated and extended to many more input variables), then save the trained parameters to a file using a tool such as pickle or joblib, so that Flask can load them once when the server starts rather than retraining anything on every request. And while app.run(debug=True) is perfect for development on your own laptop, it is a single-visitor testing server; a real deployed site hands the actual traffic-serving job to a production-grade WSGI server such as Gunicorn or uWSGI, which can handle many simultaneous requests reliably — Flask still defines the routes and logic, but a sturdier program does the job of actually talking to the internet at scale.
Check your understanding
Q1. A route is defined as @app.route("/score/<int:hours>"). A visitor requests /score/4.5. What happens, and why?
A1. Flask returns a 404 Not Found page. The <int:hours> converter only matches whole numbers in that position of the URL; "4.5" fails that check before the view function is ever called, so the route simply does not match — no Python exception occurs inside your function, because your function never runs.
Q2. Using our trained formula marks = 9.2 × hours + 30.4, what does the model predict for a student who studied 2 hours, and is that a trustworthy prediction? Why or why not?
A2. 9.2 × 2 + 30.4 = 18.4 + 30.4 = 48.8 marks. This is trustworthy in the sense that 2 hours sits comfortably inside our training data's range (1 to 5 hours), unlike the 10-hour case, which extrapolated far beyond it.
Q3. Why does a login form use method="POST" instead of the default GET?
A3. GET requests place all data in the URL's query string, which is visible in the address bar, gets stored in browser history, and is often logged by servers in plaintext — unsuitable for a password. POST sends data in the request body instead, keeping it out of the URL.
Q4. True or false: every time your Flask app serves a prediction, it re-examines the training data and recomputes m and c. Explain.
A4. False. m and c were computed once, during training (by hand, in our case). Flask's job during serving is only to plug a new hours value into the already-fixed formula — training and serving are separate phases, and Flask performs only the second one.
Summary
An ML-powered web app is two ordinary things wired together: a prediction function whose behavior was fixed once by fitting numbers to past data, and a Flask application that maps URLs to Python functions and turns their return values into HTTP responses. We derived a real linear regression line by hand from a five-student study-hours-versus-marks table — mean_x = 3, mean_y = 58, giving slope m = 92⁄10 = 9.2 and intercept c = 58 − 27.6 = 30.4 — then wrapped that exact formula inside predict_marks(), and connected it to the outside world through Flask routes that read input from a query string (request.args), a URL path converter (<float:hours>), or a submitted form (request.form), before returning either an HTML page for a human or JSON for another program via jsonify. We also confirmed a boundary the model cannot see past on its own — a 10-hour prediction of 122.4 marks is nonsense, because it extrapolates beyond the data the line was fit on — and pinned down the split between training (done once, offline, to produce fixed numbers like our 9.2 and 30.4) and serving (done on every request, by Flask, using those already-fixed numbers). That is the same split running underneath a ConfirmTkt confirmation estimate: nothing about your specific search retrains anything; a model trained earlier on historical data is simply being served to you, instantly, over exactly the request-response loop diagrammed above.
Think About It
Think about this: How would you explain building ml-powered web apps with flask to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.