Flask is a very powerful framework used to develop highly scalable and easy-to-deploy web applications in Python. In just a few lines of code, one can simply build their first API with this framework in their arsenal. One of the recent and most interesting such use-cases is serving Machine Learning based services on the web. Since serving a web application is an example of full-stack development (not the best one of course), it can be a little tricky to go about structuring the entire project and integrating the frontend and backend modules nicely. In this detailed tutorial, we are going to tackle precisely that so let’s get started!
This is the first article in a 3-part series on Developing Web Applications using Flask and Python Series. Check out the next part in the series on Deploying Web Applications to Production using Flask, Gunicorn WSGI and Nginx.
The Prerequisites
Before we begin, the entire source code for this project can be found here. Clone the repository on your machine, fire up a separate virtual environment and install the necessary packages from the requirements.txt file provided.
The predictive model XNet used in this tutorial has been taken from our previous tutorial on Chest X Ray Pneumonia Prediction. If you haven’t given it a read yet, I would highly recommend you to first go through that article and the associated notebook to get a first-hand flavor of building Deep Learning models from scratch and reproducing XNet for use in this project. The model’s architecture and weights need to be reproduced and added to this project’s root directory manually in order to serve it in our web application. They haven’t been uploaded to the GitHub repository due to size constraints.
Once you have the model trained and tested, proceed to save the model’s architecture and weights to XNet.json and XNet.h5 files respectively. Use the following code to achieve this:
# Saving the model for Future Inferences
XNet_json = XNet.to_json()
with open("XNet.json", "w") as json_file:
json_file.write(XNet_json)
# Serialize weights to HDF5
XNet.save_weights("XNet.h5")
Code language: Python (python)
Now let’s see where to put these two files in our project directory.
Structuring our Flask Project
When developing large-scale applications with a huge codebase, it can get very easy to lose track of which scripts are placed in which directories especially when more than one developers are simultaneously working on the project. It is also considered one of the best practices to group similar scripts and modules into their individual directories so as to keep a neat and structured codebase. At this point, the directory structure of our project looks like this:
- webapp/
- static/
- templates/
- uploads/
- __init__.py
- params.py
- views.py
- xnet.py
- README.md
- requirements.txt
- run.py
Create a new directory inside the webapp/ directory with the name XNet/ and place the two files XNet.json and XNet.h5 in this new directory. So now our project directory looks something like this:
- webapp/
- static/
- templates/
- uploads/
- XNet/
- XNet.json
- XNet.h5
- __init__.py
- params.py
- views.py
- xnet.py
- README.md
- requirements.txt
- run.py
Once that’s out of the way, let’s now understand why we have structured our project a particular way.
run.py
This file serves as the entry-point for our Flask application. Whenever the application is fired up, this file is called and executed first, hence it has been placed in the root directory of our project.
Note: Flask will resolve all paths relative to this root directory.
webapp/
This folder serves the core codebase of our web application. Any web page that we want to serve on our application go in here along with all the backend logic.
static/
This folder is used to serve all the static files of our web application. Static files, as the name suggests, are those files that don’t get changed during the execution of our project. This includes all our JavaScript, CSS, Font and Image files that we serve on our website. Flask will automatically create a /static/<path:filename> route that will serve any file under the static/ folder.
templates/
This folder is used to serve all the HTML templates of our web application.
uploads/
All of our uploaded files are saved to this directory and then read from here for processing and running predictions on. We’ll talk more about how to set up file upload restrictions and run validations in a bit.
__init__.py
When the project is fired up, the run.py file serves as an entry-point and calls this file. The Flask object is initialized and configured in this file as shown below. Our XNet model is also loaded into memory here to avoid reloading it every time our application has to serve an API call.
from flask import Flask
webapp = Flask(__name__)
from webapp import views, params, xnet
# Set file upload parameters
webapp.config["SECRET_KEY"] = params.SECRET_KEY
webapp.config["UPLOAD_FOLDER"] = params.UPLOAD_FOLDER
webapp.config["ALLOWED_EXTENSIONS"] = params.ALLOWED_EXTENSIONS
webapp.config['MAX_CONTENT_LENGTH'] = params.MAX_CONTENT_LENGTH
# Load the XNet model into memory
webapp.config['XNET_MODEL'] = xnet.load_model(path = 'webapp/XNet')
Code language: Python (python)
views.py
This file acts as a message broker between the frontend and backend modules of our application. It is used to route user requests, render HTML templates, pass any variables or messages to-and-fro the frontend/backend, and call any necessary backend routines to serve the user.
Phew! 🥱 Now that some of the boring stuff is out of the way, let’s dive into the UI side of our application!

Building the User Interface
Nowadays, there are many powerful and easy to learn tools out there to build an aesthetically pleasing and interactive User Interface (UI). One such tool that I’ve chosen to use is Bootstrap.
Bootstrap is a free and open-source CSS framework directed at responsive, mobile-first front-end web development. Bootstrap provides tons of fresh starter templates for quick development. For this project, I’ve picked this template by Mark Otto and modified it to our use-case. The bootstrap files are placed in the static/ directory of our project.
Now let’s take a look at what happens when we first run our Flask app. In order to run the application, head on over to the root directory of our project and run:
Code language: Bash (bash)python run.py
This starts our web application, PneuXNet, at the address http://localhost:5000/ and serves our landing page. Alternatively, you can choose to run the app in a more elegant way:
export FLASK_APP=webapp.py
export FLASK_DEBUG=true
flask run
Code language: JavaScript (javascript)

Given that setting up this template took just a few lines of code, it looks pretty sleek right? That’s the power of using Bootstrap over conventional CSS and JavaScript which might have taken a beginner like me a whole day to style these divs and the block paddings etc. Let’s Get Started with our prediction service and look at our Predict web page:

One thing to note here is how 90% of the design from the landing page is translated onto the prediction page. This is a common trait among web applications where some design elements are usually common across the website and are reused without significant modifications. This is called boilerplate code in programming jargon and is not necessarily favored with software developers. So how do we proceed with avoiding writing redundant lines of code in our application? Enter the Jinja Templating Engine!

Photo by HaticeEROL
The Jinja Templating Engine
Jinja is a powerful templating engine used to dynamically render templates, inherit other base templates and embed the HTML with loops, conditionals and macros in order to augment the frontend functionality of the application. Let’s see what all of this means.
Template Inheritance
In our templates/ folder, we have some HTML template files listed as shown:
- base.html
- index.html
- predict.html
- upload.html
The base.html file, as the name suggests, serves the purpose of a base template that is inherited across all the other web pages of our application. This includes design elements that are visible with little or no change on each web page e.g. the nav bar at the top, the application title, the background and the foreground color styles etc. This way we can avoid rewriting redundant blocks of code in all the other template files that inherit from this base template. The way you inherit a base template is by writing the following at the start of your child template file and viola that’s it! Simple and elegant indeed.
Code language: HTML, XML (xml){% extends 'base.html' %}
But how about when we do want to customize the design or functionality of some of the child templates? We can do this by defining appropriate placeholders in the base.html file. For example, in the base.html file, we can add a placeholder in the <main> tag like so:
<main class="px-3">
{% block main %}{% endblock %}
</main>
Code language: HTML, XML (xml)
Now, let’s look at our index.html file that served the landing page of our website:
{% block main %}
<h1>Welcome to PneuXNet!</h1>
<p class="lead">PneuXNet is a powerful web application to predict Pneumonia from Chest X-Ray Images!</p>
<p class="lead">
<a href="/predict/" class="btn btn-lg btn-secondary fw-bold border-white bg-white">Get Started</a>
</p>
{% endblock %}
Code language: HTML, XML (xml)
And, our predict.html file that served the predict page of our website:
{% block main %}
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<p class="lead">{{ message }}</p>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<h1>Upload a Chest X-Ray Image (supported formats .jpg, .png, .jpeg)</h1>
<form method="post" action="/upload/" enctype="multipart/form-data">
<div class="input-group" style="margin-top: 25px;">
<input type="file" class="form-control" name="file" id="file"
aria-describedby="inputGroupFileAddon04" aria-label="Upload" required>
</div>
<p class="lead">
<input type="submit" value='Upload' class="btn btn-lg btn-secondary fw-bold border-white bg-white"
style="margin-top: 25px;">
</p>
</form>
{% endblock %}
Code language: HTML, XML (xml)
Don’t worry about the code for now. The important point to note is that template inheritance is a really useful feature among other things in the Jinja toolkit.
Message Flashing with Macros
The best UIs are those that are extremely responsive and interactive for their users. Displaying tooltips and error prompts where users have to provide some kind of input or interaction is not only an expectation but a necessity nowadays. With Jinja, flashing messages to the frontend becomes very easy.
Suppose you have to upload images to your service. There are a number of checks you can place:
- Allowed image extensions
- Maximum allowed image upload size
- Empty file upload etc.
We can run these validation checks at the backend of our application and if any one of these fail, we can send an error prompt to the web interface in a nice manner:
# Check for the allowed file types and save to the local disk
if uploaded_file and __allowed_file(uploaded_file.filename):
path = os.path.join(webapp.config['UPLOAD_FOLDER'], secure_filename(uploaded_file.filename))
uploaded_file.save(path)
else:
# Flash an error message and redirect to the same page
flash('The file must be of one of the accepted formats only! Try again!', 'error')
return render_template("predict.html")
Code language: Python (python)
These error messages can then be displayed on the website promptly like so:
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<p class="lead">{{ message }}</p>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
Code language: HTML, XML (xml)
The logic here is pretty straightforward and is intended to mimic the human-readable syntax of Python.

These funny looking curly braces with percentage symbols are what’s called Macros. Macros are Jinja’s way of encapsulating logic into routines much like what functions do in other programming languages. This just barely scratches the surface of the countless possibilities with this engine. Maybe some day down the road we’ll meet again on another one of our tutorials with this.

Building the Backend Service
Time to dive into the mechanics of how to tie in the backend logic to run our web services. Let’s get our coding hats on for this one.

Once we launch our application, our program begins its execution from the root directory of our Flask project. Specifically, when we ran python run.py, our application began execution from this file. Let’s look at this file:
from webapp import webapp
if __name__=="__main__":
webapp.debug = True
webapp.run(host="0.0.0.0", port="5000")
Code language: Python (python)
Here we are importing our Flask webapp, that we have initialized in the __init__.py file inside our webapp directory and running it at localhost with port 5000. The way this works is that the python Interpreter, by default, considers a directory that contains an __init__.py file as a python package. Here’s what the official documentation says:
A regular package is typically implemented as a directory containing an
__init__.py
file. When a regular package is imported, this__init__.py
file is implicitly executed, and the objects it defines are bound to names in the package’s namespace.
Hence, Python looks for a webapp package containing a Flask webapp object bound to its namespace. Obviously one can define the package and the Flask app with different names to avoid any confusions.
Once the package is imported, its __init__.py file is called and executed implicitly, instantiating the Flask app and setting up its configuration parameters as defined in the params.py file:
from flask import Flask
webapp = Flask(__name__)
from webapp import views, params, xnet
# Set file upload parameters
webapp.config["SECRET_KEY"] = params.SECRET_KEY
webapp.config["UPLOAD_FOLDER"] = params.UPLOAD_FOLDER
webapp.config["ALLOWED_EXTENSIONS"] = params.ALLOWED_EXTENSIONS
webapp.config['MAX_CONTENT_LENGTH'] = params.MAX_CONTENT_LENGTH
# Load the XNet model into memory
webapp.config['XNET_MODEL'] = xnet.load_model(path = 'webapp/XNet')
Code language: Python (python)
Here we have defined a few config parameters that can be accessed anywhere from within our project. Some of these include setting up the path to where the uploaded images should be saved, the allowed file types to be uploaded, the maximum upload file size (8 MB) etc. We also load XNet model into memory here and save it inside the config object for later use.
SECRET_KEY = '[email protected]'
UPLOAD_FOLDER = 'webapp/uploads'
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg'}
MAX_CONTENT_LENGTH = 8 * 1000 * 1000
Code language: C++ (cpp)
Now, what happens when we hit http://localhost:5000/ inside our browser or when we move to other web pages within our website? This logic is programmed inside our views.py file:
@webapp.route("/", methods = ['GET'])
def index():
# Routine to render the home page of our website
return render_template("index.html")
@webapp.route("/predict/", methods = ['GET'])
def predict():
# Routine to route requests from the predict page to display the upload dialog box
return render_template("predict.html")
Code language: Python (python)
Flask provides a nice route() decorator to register the view function for a given URL. When the website is routed to http://localhost:5000/, the index() routine is called which renders the landing page of our website. Similarly, hitting the http://localhost:5000/predict/ URL calls the predict() function and renders the prediction page of our website.
Once the user uploads a valid image, that image is bound to the form-data of the HTTP request and sent to the upload() view in our views.py file as indicated by the action attribute of the form tag shown below:
<form method="post" action="/upload/" enctype="multipart/form-data">
<div class="input-group" style="margin-top: 25px;">
<input type="file" class="form-control" name="file" id="file"
aria-describedby="inputGroupFileAddon04" aria-label="Upload" required>
</div>
<p class="lead">
<input type="submit" value='Upload' class="btn btn-lg btn-secondary fw-bold border-white bg-white"
style="margin-top: 25px;">
</p>
</form>
Code language: HTML, XML (xml)
The image is then accessed via the files attribute of our HTTP request module and checked for validations and restrictions in the upload() view as shown below:
def upload():
# Routine to deal with the uploaded form data i.e. image in this case
# Fetches the uploaded image, runs basic validations and saves to the specified directory
def __allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in webapp.config["ALLOWED_EXTENSIONS"]
# Clear any existing flash messages from the session
session.pop('_flashes', None)
# Run sanity checks
if request.method == 'POST':
print("Passed request method sanity check!")
# First check whether or not the request contains our desired file
if 'file' not in request.files:
# Flash an error message and redirect to the same page
flash('No file part! Try again!', 'error')
return render_template("predict.html")
print("Passed file exist sanity check!")
uploaded_file = request.files['file']
# Check for the allowed file types and save to the local disk
if uploaded_file and __allowed_file(uploaded_file.filename):
path = os.path.join(webapp.config['UPLOAD_FOLDER'],
secure_filename(uploaded_file.filename))
uploaded_file.save(path)
else:
# Flash an error message and redirect to the same page
flash('The file must be of one of the accepted formats only! Try again!', 'error')
return render_template("predict.html")
print("Passed file extension and save check!")
Code language: Python (python)
If everything goes well, at this point, the uploaded image will get saved inside the webapps/uploads/ directory of our project. This image can finally be read from the disk and sent to the XNet model for running our Pneumonia prediction service.
# Next, run the XNet prediction service
result = xnet.predict(webapp.config['XNET_MODEL'], path = path)
print("Passed XNet prediction check!")
print(result)
return render_template("upload.html", filename=uploaded_file.filename, result=result)
Code language: Python (python)
The routines to load the XNet model into memory and running predictions on images are given in the xnet.py file and are not discussed here since they have been taken directly from one of our previous tutorials.
Once all the checks are validated and the prediction routine is run without any errors, the result is then sent to the frontend and the website is then rerouted to http://localhost:5000/upload/ to display the resulting prediction. Note that the result is sent as part of the render_template() function used inside our upload view.
{% block main %}
<h1>{{ filename }}</h1>
<p class="lead">{{ result }}</p>
<p class="lead">
<a href="/predict/" class="btn btn-lg btn-secondary fw-bold border-white bg-white">Try Again</a>
</p>
{% endblock %}
Code language: HTML, XML (xml)


…and there we have it! Our first web application, up and running. 🥳
Conclusion
A full stack web application comes with many different gotchas, from application scalability to application security, that need to be kept into consideration when developing production-grade software products and applications. We have looked at a single web framework today and barely unlocked its full potential and capabilities. There are a number of other frameworks out there, e.g. Django, that are used abundantly to ship out fully-contained, secure and scalable web applications from the get-go.
Flask is considered to be a development server and is advised not to be used where production-ready services need to be deployed. Here’s what their official website says:
Do not use the development server when deploying to production. It is intended for use only during local development. It is not designed to be particularly efficient, stable, or secure.
So, how do we go about deploying our services into production-grade environments and what are the nitty-gritty details that we need to worry about in terms of scalability and security of our applications?
Stick around and consider subscribing to our weekly newsletter to stay tuned for the next articles in this series to find the answers to these questions. Alright, till then! ✌