
Image by Author | ChatGPT
Machine learning has powerful applications across various domains, but effectively deploying machine learning models in real-world scenarios often necessitates the use of a web framework.
Django, a high-level web framework for Python, is particularly popular for creating scalable and secure web applications. When paired with libraries like scikit-learn, Django enables developers to serve machine learning model inference via APIs and also lets you build intuitive web interfaces for user interaction with these models.
In this tutorial, you will learn how to build a simple Django application that serves predictions from a machine learning model. This step-by-step guide will walk you through the entire process, starting from initial model training to inference and testing APIs.
## 1. Project Setup
We will start by creating the base project structure and installing the required dependencies.
Create a new project directory and move into it:
bash
mkdir django-ml-app && cd django-ml-app
Next, install the required Python packages:
bash
pip install Django scikit-learn joblib
Then, initialize a new Django project called `mlapp` and create a new app named `predictor`:
bash
django-admin startproject mlapp .
python manage.py startapp predictor
Set up template directories for our app’s HTML files:
bash
mkdir -p templates/predictor
Your project folder will now have a structured layout ready for development.
## 2. Train the Machine Learning Model
Next, we will create a model that our Django app will use for predictions. For this tutorial, we will work with the classic Iris dataset, included in scikit-learn.
In the root directory, create a script named `train.py`. This script will load the Iris dataset, split it into training and testing sets, and then train a Random Forest classifier on the training data. After training, the model will be saved into the `predictor/model/` directory using the `joblib` library.
python
from pathlib import Path
import joblib
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
MODEL_DIR = Path(“predictor”) / “model”
MODEL_DIR.mkdir(parents=True, exist_ok=True)
MODEL_PATH = MODEL_DIR / “iris_rf.joblib”
def main():
data = load_iris()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
clf = RandomForestClassifier(n_estimators=200, random_state=42)
clf.fit(X_train, y_train)
joblib.dump({
“estimator”: clf,
“target_names”: data.target_names,
“feature_names”: data.feature_names,
}, MODEL_PATH)
print(f”Saved model to {MODEL_PATH.resolve()}”)
if __name__ == “__main__”:
main()
Run the training script to ensure everything works smoothly.
## 3. Configure Django Settings
Now that we have our app and training script ready, we need to configure Django so it recognizes our new application and where to find templates.
Open `mlapp/settings.py` and update the following:
– Register the `predictor` app in `INSTALLED_APPS`.
– Add the `templates/` directory in the `TEMPLATES` configuration.
– Set `ALLOWED_HOSTS` to accept all hosts during development.
python
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
INSTALLED_APPS = [
“django.contrib.admin”,
“django.contrib.auth”,
“django.contrib.contenttypes”,
“django.contrib.sessions”,
“django.contrib.messages”,
“django.contrib.staticfiles”,
“predictor”, # add this line
]
TEMPLATES = [
{
“BACKEND”: “django.template.backends.django.DjangoTemplates”,
“DIRS”: [BASE_DIR / “templates”], # add this line
“APP_DIRS”: True,
“OPTIONS”: {
“context_processors”: [
“django.template.context_processors.debug”,
“django.template.context_processors.request”,
“django.contrib.auth.context_processors.auth”,
“django.contrib.messages.context_processors.messages”,
],
},
},
]
ALLOWED_HOSTS = [“*”] # for development
## 4. Add URLs
With our app registered, the next step is to wire up the URL routing so users can access our pages and API endpoints. Django uses `urls.py` files for this purpose.
We will configure two sets of routes:
– **Project-level URLs (`mlapp/urls.py`)** – global routes like the admin panel and routes from the `predictor` app.
– **App-level URLs (`predictor/urls.py`)** – specific routes for our web form and API.
Update `mlapp/urls.py`:
python
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path(“admin/”, admin.site.urls),
path(“”, include(“predictor.urls”)), # web & API routes
]
Now create `predictor/urls.py` and define the app-specific routes:
python
from django.urls import path
from .views import home, predict_view, predict_api
urlpatterns = [
path(“”, home, name=”home”),
path(“predict/”, predict_view, name=”predict”),
path(“api/predict/”, predict_api, name=”predict_api”),
]
## 5. Build the Form
To let users interact with our model through a web interface, we need an input form where they can enter flower measurements (sepal and petal dimensions). Django simplifies this process with its built-in forms module.
Create a simple form class in a new file called `forms.py` within your `predictor` app:
python
from django import forms
class IrisForm(forms.Form):
sepal_length = forms.FloatField(min_value=0, label=”Sepal length (cm)”)
sepal_width = forms.FloatField(min_value=0, label=”Sepal width (cm)”)
petal_length = forms.FloatField(min_value=0, label=”Petal length (cm)”)
petal_width = forms.FloatField(min_value=0, label=”Petal width (cm)”)
## 6. Load Model and Predict
Now that we have trained and saved our Iris classifier, we need a way for the Django app to load the model and use it for predictions. To keep the project organized, we will place prediction logic inside `services.py` in the `predictor` app.
This approach ensures that views focus on request/response handling, while the prediction logic is encapsulated in a reusable service.
python
from pathlib import Path
import joblib
import numpy as np
_MODEL_CACHE = {}
def get_model_bundle():
“””Loads and caches the trained model.”””
global _MODEL_CACHE
if “bundle” not in _MODEL_CACHE:
model_path = Path(__file__).resolve().parent / “model” / “iris_rf.joblib”
_MODEL_CACHE[“bundle”] = joblib.load(model_path)
return _MODEL_CACHE[“bundle”]
def predict_iris(features):
“””Predict the Iris class based on features.”””
bundle = get_model_bundle()
clf = bundle[“estimator”]
target_names = bundle[“target_names”]
X = np.array([features], dtype=float)
proba = clf.predict_proba(X)[0]
idx = int(np.argmax(proba))
return {
“class_index”: idx,
“class_name”: str(target_names[idx]),
“probabilities”: {str(name): float(p) for name, p in zip(target_names, proba)},
}
## 7. Views
Views act as the glue between user inputs, the model, and the final response (HTML or JSON). Here we will build three views:
1. **home** – Renders the prediction form.
2. **predict_view** – Handles form submissions from the web interface.
3. **predict_api** – Provides a JSON API endpoint for programmatic predictions.
In `predictor/views.py`, add the following:
python
from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
from .forms import IrisForm
from .services import predict_iris
import json
def home(request):
return render(request, “predictor/predict_form.html”, {“form”: IrisForm()})
@require_http_methods([“POST”])
def predict_view(request):
form = IrisForm(request.POST)
if not form.is_valid():
return render(request, “predictor/predict_form.html”, {“form”: form})
data = form.cleaned_data
features = [
data[“sepal_length”],
data[“sepal_width”],
data[“petal_length”],
data[“petal_width”],
]
result = predict_iris(features)
return render(
request,
“predictor/predict_form.html”,
{“form”: IrisForm(), “result”: result, “submitted”: True},
)
@csrf_exempt
@require_http_methods([“POST”])
def predict_api(request):
if request.META.get(“CONTENT_TYPE”, “”).startswith(“application/json”):
try:
payload = json.loads(request.body or “{}”)
except json.JSONDecodeError:
return JsonResponse({“error”: “Invalid JSON.”}, status=400)
else:
payload = request.POST.dict()
required = [“sepal_length”, “sepal_width”, “petal_length”, “petal_width”]
missing = [k for k in required if k not in payload]
if missing:
return JsonResponse({“error”: f”Missing: {‘, ‘.join(missing)}”}, status=400)
try:
features = [float(payload[k]) for k in required]
except ValueError:
return JsonResponse({“error”: “All features must be numeric.”}, status=400)
return JsonResponse(predict_iris(features))
## 8. Template
Finally, we will create an HTML template that serves as the user interface for our Iris predictor.
This template will:
– Render the Django form fields we defined earlier.
– Provide a clean layout with responsive form inputs.
– Display prediction results when available.
– Mention the API endpoint for programmatic access.
Create a file named `predict_form.html` in `templates/predictor/`:
Iris Predictor
Enter Iris flower measurements to get a prediction.
{% if submitted and result %}
Predicted class: {{ result.class_name }}
Probabilities:
{% for name, p in result.probabilities.items %}
- {{ name }}: {{ p|floatformat:3 }}
{% endfor %}
{% endif %}
API available at POST /api/predict/
## 9. Run the Application
With everything in place, it’s time to run our Django project and test both the web form and the API endpoint.
Run the following command to set up the default Django database:
bash
python manage.py migrate
Next, launch the Django development server:
bash
python manage.py runserver
You will see output indicating that the server is running. Open your browser and visit: http://127.0.0.1:8000/ to use the web form interface.
Additionally, you can send a POST request to the API using curl:
bash
curl -X POST http://127.0.0.1:8000/api/predict/ \
-H “Content-Type: application/json” \
-d ‘{“sepal_length”:5.1,”sepal_width”:3.5,”petal_length”:1.4,”petal_width”:0.2}’
## 10. Testing
Before wrapping up, it is good practice to verify that our application works as expected. Django provides a built-in **testing framework** that integrates with Python’s `unittest` module.
Create simple tests in `predictor/tests.py` to ensure:
– The homepage renders correctly.
– The API endpoint returns a valid prediction response.
Add the following code:
python
from django.test import TestCase
from django.urls import reverse
class PredictorTests(TestCase):
def test_home_renders(self):
resp = self.client.get(reverse(“home”))
self.assertEqual(resp.status_code, 200)
self.assertContains(resp, “Iris Predictor”)
def test_api_predict(self):
url = reverse(“predict_api”)
payload = {
“sepal_length”: 5.0,
“sepal_width”: 3.6,
“petal_length”: 1.4,
“petal_width”: 0.2,
}
resp = self.client.post(url, payload)
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertIn(“class_name”, data)
self.assertIn(“probabilities”, data)
Run the tests using:
bash
python manage.py test
If all tests pass, it confirms your Django + machine learning app is functioning as expected.

Image by Author | ChatGPT
Machine learning has powerful applications across various domains, but effectively deploying machine learning models in real-world scenarios often necessitates the use of a web framework.
Django, a high-level web framework for Python, is particularly popular for creating scalable and secure web applications. When paired with libraries like scikit-learn, Django enables developers to serve machine learning model inference via APIs and also lets you build intuitive web interfaces for user interaction with these models.
In this tutorial, you will learn how to build a simple Django application that serves predictions from a machine learning model. This step-by-step guide will walk you through the entire process, starting from initial model training to inference and testing APIs.
## 1. Project Setup
We will start by creating the base project structure and installing the required dependencies.
Create a new project directory and move into it:
bash
mkdir django-ml-app && cd django-ml-app
Next, install the required Python packages:
bash
pip install Django scikit-learn joblib
Then, initialize a new Django project called `mlapp` and create a new app named `predictor`:
bash
django-admin startproject mlapp .
python manage.py startapp predictor
Set up template directories for our app’s HTML files:
bash
mkdir -p templates/predictor
Your project folder will now have a structured layout ready for development.
## 2. Train the Machine Learning Model
Next, we will create a model that our Django app will use for predictions. For this tutorial, we will work with the classic Iris dataset, included in scikit-learn.
In the root directory, create a script named `train.py`. This script will load the Iris dataset, split it into training and testing sets, and then train a Random Forest classifier on the training data. After training, the model will be saved into the `predictor/model/` directory using the `joblib` library.
python
from pathlib import Path
import joblib
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
MODEL_DIR = Path(“predictor”) / “model”
MODEL_DIR.mkdir(parents=True, exist_ok=True)
MODEL_PATH = MODEL_DIR / “iris_rf.joblib”
def main():
data = load_iris()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
clf = RandomForestClassifier(n_estimators=200, random_state=42)
clf.fit(X_train, y_train)
joblib.dump({
“estimator”: clf,
“target_names”: data.target_names,
“feature_names”: data.feature_names,
}, MODEL_PATH)
print(f”Saved model to {MODEL_PATH.resolve()}”)
if __name__ == “__main__”:
main()
Run the training script to ensure everything works smoothly.
## 3. Configure Django Settings
Now that we have our app and training script ready, we need to configure Django so it recognizes our new application and where to find templates.
Open `mlapp/settings.py` and update the following:
– Register the `predictor` app in `INSTALLED_APPS`.
– Add the `templates/` directory in the `TEMPLATES` configuration.
– Set `ALLOWED_HOSTS` to accept all hosts during development.
python
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
INSTALLED_APPS = [
“django.contrib.admin”,
“django.contrib.auth”,
“django.contrib.contenttypes”,
“django.contrib.sessions”,
“django.contrib.messages”,
“django.contrib.staticfiles”,
“predictor”, # add this line
]
TEMPLATES = [
{
“BACKEND”: “django.template.backends.django.DjangoTemplates”,
“DIRS”: [BASE_DIR / “templates”], # add this line
“APP_DIRS”: True,
“OPTIONS”: {
“context_processors”: [
“django.template.context_processors.debug”,
“django.template.context_processors.request”,
“django.contrib.auth.context_processors.auth”,
“django.contrib.messages.context_processors.messages”,
],
},
},
]
ALLOWED_HOSTS = [“*”] # for development
## 4. Add URLs
With our app registered, the next step is to wire up the URL routing so users can access our pages and API endpoints. Django uses `urls.py` files for this purpose.
We will configure two sets of routes:
– **Project-level URLs (`mlapp/urls.py`)** – global routes like the admin panel and routes from the `predictor` app.
– **App-level URLs (`predictor/urls.py`)** – specific routes for our web form and API.
Update `mlapp/urls.py`:
python
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path(“admin/”, admin.site.urls),
path(“”, include(“predictor.urls”)), # web & API routes
]
Now create `predictor/urls.py` and define the app-specific routes:
python
from django.urls import path
from .views import home, predict_view, predict_api
urlpatterns = [
path(“”, home, name=”home”),
path(“predict/”, predict_view, name=”predict”),
path(“api/predict/”, predict_api, name=”predict_api”),
]
## 5. Build the Form
To let users interact with our model through a web interface, we need an input form where they can enter flower measurements (sepal and petal dimensions). Django simplifies this process with its built-in forms module.
Create a simple form class in a new file called `forms.py` within your `predictor` app:
python
from django import forms
class IrisForm(forms.Form):
sepal_length = forms.FloatField(min_value=0, label=”Sepal length (cm)”)
sepal_width = forms.FloatField(min_value=0, label=”Sepal width (cm)”)
petal_length = forms.FloatField(min_value=0, label=”Petal length (cm)”)
petal_width = forms.FloatField(min_value=0, label=”Petal width (cm)”)
## 6. Load Model and Predict
Now that we have trained and saved our Iris classifier, we need a way for the Django app to load the model and use it for predictions. To keep the project organized, we will place prediction logic inside `services.py` in the `predictor` app.
This approach ensures that views focus on request/response handling, while the prediction logic is encapsulated in a reusable service.
python
from pathlib import Path
import joblib
import numpy as np
_MODEL_CACHE = {}
def get_model_bundle():
“””Loads and caches the trained model.”””
global _MODEL_CACHE
if “bundle” not in _MODEL_CACHE:
model_path = Path(__file__).resolve().parent / “model” / “iris_rf.joblib”
_MODEL_CACHE[“bundle”] = joblib.load(model_path)
return _MODEL_CACHE[“bundle”]
def predict_iris(features):
“””Predict the Iris class based on features.”””
bundle = get_model_bundle()
clf = bundle[“estimator”]
target_names = bundle[“target_names”]
X = np.array([features], dtype=float)
proba = clf.predict_proba(X)[0]
idx = int(np.argmax(proba))
return {
“class_index”: idx,
“class_name”: str(target_names[idx]),
“probabilities”: {str(name): float(p) for name, p in zip(target_names, proba)},
}
## 7. Views
Views act as the glue between user inputs, the model, and the final response (HTML or JSON). Here we will build three views:
1. **home** – Renders the prediction form.
2. **predict_view** – Handles form submissions from the web interface.
3. **predict_api** – Provides a JSON API endpoint for programmatic predictions.
In `predictor/views.py`, add the following:
python
from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
from .forms import IrisForm
from .services import predict_iris
import json
def home(request):
return render(request, “predictor/predict_form.html”, {“form”: IrisForm()})
@require_http_methods([“POST”])
def predict_view(request):
form = IrisForm(request.POST)
if not form.is_valid():
return render(request, “predictor/predict_form.html”, {“form”: form})
data = form.cleaned_data
features = [
data[“sepal_length”],
data[“sepal_width”],
data[“petal_length”],
data[“petal_width”],
]
result = predict_iris(features)
return render(
request,
“predictor/predict_form.html”,
{“form”: IrisForm(), “result”: result, “submitted”: True},
)
@csrf_exempt
@require_http_methods([“POST”])
def predict_api(request):
if request.META.get(“CONTENT_TYPE”, “”).startswith(“application/json”):
try:
payload = json.loads(request.body or “{}”)
except json.JSONDecodeError:
return JsonResponse({“error”: “Invalid JSON.”}, status=400)
else:
payload = request.POST.dict()
required = [“sepal_length”, “sepal_width”, “petal_length”, “petal_width”]
missing = [k for k in required if k not in payload]
if missing:
return JsonResponse({“error”: f”Missing: {‘, ‘.join(missing)}”}, status=400)
try:
features = [float(payload[k]) for k in required]
except ValueError:
return JsonResponse({“error”: “All features must be numeric.”}, status=400)
return JsonResponse(predict_iris(features))
## 8. Template
Finally, we will create an HTML template that serves as the user interface for our Iris predictor.
This template will:
– Render the Django form fields we defined earlier.
– Provide a clean layout with responsive form inputs.
– Display prediction results when available.
– Mention the API endpoint for programmatic access.
Create a file named `predict_form.html` in `templates/predictor/`:
Enter Iris flower measurements to get a prediction.
{% if submitted and result %}
-
{% for name, p in result.probabilities.items %}
- {{ name }}: {{ p|floatformat:3 }}
{% endfor %}
{% endif %}
API available at POST /api/predict/
## 9. Run the Application
With everything in place, it’s time to run our Django project and test both the web form and the API endpoint.
Run the following command to set up the default Django database:
bash
python manage.py migrate
Next, launch the Django development server:
bash
python manage.py runserver
You will see output indicating that the server is running. Open your browser and visit: http://127.0.0.1:8000/ to use the web form interface.
Additionally, you can send a POST request to the API using curl:
bash
curl -X POST http://127.0.0.1:8000/api/predict/ \
-H “Content-Type: application/json” \
-d ‘{“sepal_length”:5.1,”sepal_width”:3.5,”petal_length”:1.4,”petal_width”:0.2}’
## 10. Testing
Before wrapping up, it is good practice to verify that our application works as expected. Django provides a built-in **testing framework** that integrates with Python’s `unittest` module.
Create simple tests in `predictor/tests.py` to ensure:
– The homepage renders correctly.
– The API endpoint returns a valid prediction response.
Add the following code:
python
from django.test import TestCase
from django.urls import reverse
class PredictorTests(TestCase):
def test_home_renders(self):
resp = self.client.get(reverse(“home”))
self.assertEqual(resp.status_code, 200)
self.assertContains(resp, “Iris Predictor”)
def test_api_predict(self):
url = reverse(“predict_api”)
payload = {
“sepal_length”: 5.0,
“sepal_width”: 3.6,
“petal_length”: 1.4,
“petal_width”: 0.2,
}
resp = self.client.post(url, payload)
self.assertEqual(resp.status_code, 200)
data = resp.json()
self.assertIn(“class_name”, data)
self.assertIn(“probabilities”, data)
Run the tests using:
bash
python manage.py test
If all tests pass, it confirms your Django + machine learning app is functioning as expected.

