When developing a back end for a Python application, itโs essential to remember that the devil is in the details. Using the right framework can boost your appโs performance, security, and scalability. But to make the best choice, you often need to do a lot of research or have practical experience with a specific tool.
So, how can you know which tool to choose? At Apriorit, we have used all popular Python development tools countless times and know their pros and cons from practical experience. We share our knowledge in this article, focusing on three popular Python backend frameworks: Flask, Django, and Tornado. To demonstrate how they work, weโll develop the same back end using each framework and compare their development speed, code maintainability, and security.
This article will be useful for technical leaders looking for a fitting technology stack and expertise to quickly build their applications.
Contents:
How to choose a Python development framework for your back end
An applicationโs back end directly influences its security, performance, and scalability. To build a back end, developers often choose Python frameworks because they provide many pre-built libraries, components, and functionalities that help save time and benefit from the numerous advantages of Python.
Python is one of the most popular programming languages, with a huge and active community and dozens of development frameworks. Hereโs how your project can benefit from determining the best Python backend framework:
- Development speed and efficiency. If a framework provides developers with all the components and features they need out of the box, they donโt need to spend extra time coding custom functionality and searching for answers to their questions.
- Relevant security mechanisms. Different types of products require different security measures, so itโs best to choose a framework with the built-in protections you need. For example, when building a web app, look for something that protects against common vulnerabilities like SQL injection and cross-site scripting.
- Simplified data management. A well-suited framework often comes with tools for seamless database interaction, data validation, and data management. This can streamline data handling for an application that works with large datasets.
- Maintainability. A framework designed with long-term maintenance in mind promotes a clean code architecture, reusability, and a modular design. This ensures that future updates, bug fixes, or feature additions can be implemented easily, reducing overall technical debt.
- Scalability options. Some frameworks are better suited for handling increased traffic and data loads. Selecting a framework that can scale horizontally or vertically without major code rewrites allows your application to grow alongside your business demands.
When choosing the framework for your project, consider the following factors:
- Project type and goal. The Python ecosystem has dedicated frameworks for developing everything from web applications to AI algorithms. Choosing a framework that matches the type of project you are building will provide you with features you need out of the box and reduce development time.
- Project complexity. Your framework has to support your appโs expected level of architectural complexity and flexibility. While some frameworks are designed for quick prototyping of small solutions, others enable building large products with diverse functionality.
- Learning curve. Although Python is considered one of the easier languages to learn, developers need to spend time researching their chosen framework. They have to know its key concepts, capabilities, and hidden issues. Having someone on the team familiar with the framework of your choice can significantly speed up research and development.
- Documentation and community. During development, your team will encounter unexpected issues with any framework โ thatโs just part of the development process. Robust documentation and community support help developers spend less time searching for the solution and increase the chance that the solution will be reliable.
- Known vulnerabilities. Each framework comes with built-in issues and vulnerabilities you have to research and plan for when designing the appโs architecture. Make sure that vulnerabilities of your chosen framework donโt affect your app, or that you can easily mitigate them.
To help you choose a relevant Python backend framework, weโll compare Flask, Django, and Tornado. Weโll create a sample application and write three versions of the back end for it, one for each framework. Then weโll compare our results in terms of development speed, security, code clarity, and maintainability.
Need to develop a reliable back end?
Entrust this task to Apriorit experts and get a robust and secure backend solution.
Drawing the application logic
To demonstrate how to use popular Python frameworks for backend development, weโll develop a sample application that stores and displays information about network packets: time of sending, packet size, and sender. Additionally, weโll implement basic user authentication, authorization, and two user roles:
- Regular users can add packets and request information about them.
- The administrator can add packets, request information about all packets, add and delete users, and retrieve a list of users.
The application will have a command-line interface (CLI) and backend servers. Weโll use a local SQLite 3 database to store information about network packets and application users. We can use the SQLite engine to connect the SQLAlchemy toolkit and use this toolkit to work with objects in our database.
This is a fairly typical task with a variety of applications in data processing and data management. You can check out the full code for our application here and the backend code for all three examples here. Now, letโs see how we can use Flask to build the back end for this app.
1. Backend development with Flask
Flask is a Python backend web development framework that is popular for its lightweight and minimalist code. It follows a micro-framework approach, meaning it provides the core essentials for web development without enforcing a specific project structure or tools. Flask offers features like built-in routing, request handling, and templating, allowing developers to build scalable applications while integrating only the necessary libraries. Its modularity makes it suitable for small to medium-sized projects, and its extensive ecosystem allows you to build various apps.
Letโs start building our appโs back end with Flask by establishing dependencies for user login. Weโll use the Flask-JWT-Extended library to generate JWT after the user logs in and use it for Bearer authorization. We also need User and Packet classes to work with data. Theyโll be accessible via blueprints in url /user and /packet.
To work with JWT, we also need to configure JWT_SECRET_KEY and create a JWTManager. To use WSGI with the HTTPS protocol, letโs point the route to certificate files in the keyfile
and certfile
parameters.
Flask also needs a library like gevent to handle the HTTP protocol. So letโs add Flask, Flask-JWT-Extended, and gevent to our app:
app = Flask(__name__)
app.config["JWT_SECRET_KEY"] = "test" # replace with your secret key
app.register_blueprint(user, url_prefix='/user')
app.register_blueprint(packet, url_prefix='/packet')
if __name__ == '__main__':
parser = argparse.ArgumentParser(prog="flask_interface")
parser.add_argument("-c", "--certfile", help="path to tls certificate", type=str, required=False)
parser.add_argument("-k", "--keyfile", help="path to tls key", type=str, required=False)
options = parser.parse_args()
use_https = ("certfile" in dir(options) and options.certfile and "keyfile" in dir(options) and options.keyfile)
with backend.get_session() as session:
backend.create_tables()
backend.add_admin(session)
jwt.init_app(app)
http_server = (
WSGIServer("0.0.0.0:5000", app, keyfile=options.keyfile, certfile=options.certfile)
if use_https else
WSGIServer( "0.0.0.0:5000", app))
http_server.serve_forever()
Now, letโs work on the API. To allow users to log in, we need to create a POST /user/login endpoint that waits for a username and hashed password as JSON data. When the endpoint verifies that the back end can log a user in, it creates a JWT token and identifies the user by their username. This way, our app can remember a user and not force them to log in every time they use the app.
@user.route('/login', methods=['POST'])
def login():
# login
username = request.json.get('username', None)
password = request.json.get('password', None)
if username is None or password is None:
return jsonify({"msg": "Missing username or password"}), HTTPStatus.BAD_REQUEST
with backend.get_session() as session:
password = hashlib.sha256(password.encode('utf-8')).hexdigest()
user_obj = model.User(username=username, password=password, is_admin=None)
try:
backend.login(user_obj, session)
access_token = create_access_token(identity=username)
User.blacklist.discard(access_token)
return jsonify(access_token=access_token), HTTPStatus.OK
except RuntimeError as ex:
logger.error(ex)
return jsonify({"msg": "Bad username or password"}), HTTPStatus.UNAUTHORIZED
Users also need to be able to end their session and make the JWT token invalid. Weโll add a POST /user/logout endpoint for that. At the start of the function, letโs add verify_jwt_in_request()
to specify that only authenticated users can interact with this endpoint. The JWT of these users will be added to the blacklist. Then, the @jwt.token_in_blocklist_loader decorator makes Flask check that the token is blocked and log out the user.
@user.route('/logout', methods=['POST'])
def logout():
verify_jwt_in_request()
# logout
jti = get_jwt()['jti']
# If already in blacklist, return message that the user is already logged out
if jti in User.blacklist:
return jsonify({"msg": "Already logged out"}), HTTPStatus.OK
User.blacklist.add(jti)
return jsonify({"msg": "Successfully logged out"}), HTTPStatus.OK
Other endpoints in our back end have one simple architecture:
- The back end requests certain parameters.
- The endpoints request the back end to process the parameters.
- The endpoints pass data retrieved from the back end as JSON to the entity that requested it.
class User(object):
blacklist = set()
@user.route('', methods=['GET'])
def get():
verify_jwt_in_request()
# get list of users
with backend.get_session() as session:
user_obj = model.User(username=get_jwt_identity(), password=None, is_admin=None)
try:
backend.check_admin(user_obj, session)
except RuntimeError:
return jsonify({"msg": "You are not admin"}), HTTPStatus.FORBIDDEN
return jsonify([usr.to_dict() for usr in backend.list_users(session)]), HTTPStatus.OK
@user.route('', methods=['POST'])
def post():
# ...
username = request.json.get('username', None)
password = request.json.get('password', None)
is_admin = request.json.get('is_admin', 0)
if username is None or password is None:
return jsonify({"msg": "Missing username or password"}), HTTPStatus.BAD_REQUEST
user_obj = model.User(username=username, password=password, is_admin=is_admin)
backend.add_user(user_obj, session)
return jsonify({"msg": "User added"}), HTTPStatus.OK
# ...
To return an image, we save the plt output to a file with the BytesIO object, which lets us work with the file in memory without using the disk. We then send the image using the send_file function, passing the BytesIO object with the image data.
@packet.route('/plot', methods=['GET'])
def plot():
verify_jwt_in_request()
# get plot
with backend.get_session() as session:
plt = backend.get_packet_plot(session)
# get figure and set its size to 12in x 8 in
fig = plt.gcf()
fig.set_size_inches(12, 8)
buf = BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
# send file to client
return send_file(buf, mimetype='image/png')
# ...
With that, the back end part of our application is complete. Overall, the Flask code is moderately complex during setup but easy to use afterwards. You can also use SQLAlchemy with database migrations and convert data into class objects instead of Pydantic structures.
Next, letโs see how to implement the same backend functionalities with Django.
Read also
Choosing an Effective Python Dependency Management Tool for Flask Microservices: Poetry vs Pip
Our client wanted to enhance VAD platform security and performance by adding new capabilities to their platform and needed quality support for existing features.
2. Backend development with Django
Django is a high-level minimalist Python framework for backend development and management of complex systems and infrastructures. Itโs well-suited for building backend applications due to its robust ORM for database interactions, scalability, and built-in admin interface. This framework has many third-party packages for quick development.
Django provides a variety of security features, including built-in user authentication. But since our application stores user data in an existing database, we need to connect to it without Django. Letโs use JWT tokens for that. As for working with Django, we only need to set up the project using the django-admin startproject django_interface
command and leave the other settings unchanged.
We need to create a JWT that contains data for the username and the time when the token will expire. In this case, the tokenโs lifespan is 30 minutes. Then, letโs call the get_user method to ensure that the user is logged in. If the token has a valid signature, contains the username, and isnโt expired or logged out, this method returns the username.
import jwt
import datetime
SECRET_KEY = 'test' # replace with your secret key
def get_token(username):
payload = {
'username': username,
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30)
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
return token
def check_token_blacklisted(token):
return token not in check_token_blacklisted.blacklist
check_token_blacklisted.blacklist = set()
def blacklist_token(token):
check_token_blacklisted.blacklist.add(token)
def get_user(request: 'django.http.request.HttpRequest'):
# get Authorization header from django request
token = request.META.get('HTTP_AUTHORIZATION').split()[1]
if token in check_token_blacklisted.blacklist:
raise RuntimeError('Token blacklisted')
payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
# check expiration time
exp = payload.get('exp')
if datetime.datetime.utcnow() > datetime.datetime.fromtimestamp(exp):
raise RuntimeError('Token expired')
username = payload.get('username')
return username
Now, we can start working with Django. To register an endpoint, the framework uses a list to specify the path to the endpoint and the class that will handle requests to this endpoint. This code is located in urls.py.
Letโs work on user login and logout processes next. To use any other method than GET, we need to either start with GET and take the CSRF cookie or use the csrf_exempt
decorator. In the case of class-based views, csrf_exempt
doesnโt work, so we need to use method_decorator(csrf_exempt, name='dispatch')
.
Then, we create POST /login and DELETE /login endpoints that perform similar operations to the Flask interface but using the JWT utilities described above. Finally, we add the check_authorization function that verifies that the Bearer token is present and isnโt blacklisted.
def check_authorization(request):
if 'HTTP_AUTHORIZATION' not in request.META:
return HttpResponse(status=HTTPStatus.UNAUTHORIZED)
token = request.META['HTTP_AUTHORIZATION'].split()[1]
if not auth_with_jwt.check_token_blacklisted(token):
return HttpResponse(json.dumps({"msg": "Token already blacklisted"}), status=HTTPStatus.BAD_REQUEST)
return None
@method_decorator(csrf_exempt, name='dispatch')
class LoginView(View):
def post(self, request):
data = json.loads(request.body)
username = data.get('username')
password = data.get('password')
hashed_password = hashlib.sha256(password.encode('utf-8')).hexdigest()
with get_session() as session:
try:
user = model.User(username=username, password=hashed_password)
backend.login(user, session)
token = auth_with_jwt.get_token(username)
return HttpResponse(json.dumps({'access_token': token}))
except RuntimeError as ex:
logger.error(ex)
return HttpResponse(json.dumps({"msg": "Bad username or password"}), status=HTTPStatus.UNAUTHORIZED)
def delete(self, request):
res = check_authorization(request)
if res is not None:
return res
token = request.META['HTTP_AUTHORIZATION'].split()[1]
auth_with_jwt.blacklist_token(token)
return HttpResponse(json.dumps({"msg": "Successfully logged out"}), status=HTTPStatus.OK)
The methods used on the Django back end are similar to the ones we implemented in the Flask back end: they receive parameters from the request, pass them to the back end, and return the results.
@method_decorator(csrf_exempt, name='dispatch')
class User(View):
def get(self, request):
res = check_authorization(request)
if res is not None:
return res
with get_session() as session:
try:
user = auth_with_jwt.get_user(request)
except RuntimeError as ex:
return HttpResponse(json.dumps({"msg": str(ex)}), status=HTTPStatus.UNAUTHORIZED)
try:
backend.check_admin(user, session)
except RuntimeError:
return HttpResponse(json.dumps({"msg": "You are not admin"}), status=HTTPStatus.FORBIDDEN)
users = backend.list_users(session)
users = [user.to_dict() for user in users]
return HttpResponse(json.dumps(users), status=HTTPStatus.OK)
# ...
To return a result as a file, we save the results of backend.get_throughput and backend.get_packet_plot to a memory file and pass the file descriptor to return as the endpoint result.
class Throughput(View):
def get(self, request):
res = check_authorization(request)
if res is not None:
return res
with get_session() as session:
try:
auth_with_jwt.get_user(request)
except RuntimeError as ex:
return HttpResponse(json.dumps({"msg": str(ex)}), status=HTTPStatus.UNAUTHORIZED)
# save plt to file
plt = backend.get_throughput(session)
# get figure and set its size to 12in x 8in
fig = plt.gcf()
fig.set_size_inches(12, 8)
buf = BytesIO()
plt.savefig(buf, format="png")
buf.seek(0)
# send file to client
return HttpResponse(buf, content_type='image/png')
# ...
As you can see, a Django project has a unique structure for Python apps. Itโs well-covered in Django documentation and other sources, but it still presents a learning curve for developers. Also, Django has unique challenges like CSRF cookie extraction, which we mentioned above. Thatโs why Django may not be the best choice for developers who have no experience with it.
Finally, letโs see how to develop the same app back end with Tornado.
Read also
Build Python Web Apps with Django: A Practical Guide with Code Examples
Get a closer look at Django. We analyze how this framework works, why itโs worth using, and how it benefits web development using a practical example.
3. Backend development with Tornado
Tornado is one of the most popular Python frameworks designed to handle high-performance, scalable, and non-blocking applications. Its asynchronous capabilities make it ideal for applications that require real-time updates, such as network management or chat services. Tornado has a simple, flexible routing system and provides built-in support for WebSockets, making it suitable for applications with real-time communication.
Similarly to Django, Tornado includes security features like cookie-based user authentication and supports custom authentication methods. To authenticate a user, it defines the get_current_user(self) method and returns the user ID from it. We can use the tornado.web.authenticated decorator to indicate handlers that only authenticated users can use.
Letโs set up Tornado with the import tornado
command. Note that Tornado passess parameters like URLs and handlers to the tornado.web.Application constructor. Handlers here are separate classes in which methods like GET and POST show which method a class should use to respond to a method in an HTTP request.
import tornado
import tornado.web
def main():
application = tornado.web.Application([
(r"/login", LoginHandler),
(r"/logout", LogoutHandler),
(r"/user", UserHandler),
(r"/packet", PacketHandler),
(r"/packet/total", GetTotalHandler),
(r"/packet/average", GetAverageHandler),
(r"/packet/throughput", GetThroughputHandler),
(r"/packet/plot", GetPacketPlotHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")
# run server
application.listen(5001)
tornado.ioloop.IOLoop.current().start()
if __name__ == "__main__":
main()
Letโs now add login and logout functionalities. BaseHandler
defines the get_current_user(self) method, which checks that the username cookie isnโt logged out and retrieves the user identifier with username signed_cookie. Then, the LoginHandler, on a POST request, checks the username and password and creates a signed_cookie for easier user authentication in the future.
LogoutHandler also requests to delete the cookie from the client and adds the username cookie value to the list of logged-out users.
class BaseHandler(tornado.web.RequestHandler):
blacklisted_tokens = set()
def get_current_user(self):
if self.get_cookie("username") in BaseHandler.blacklisted_tokens:
return None
return self.get_signed_cookie("username").decode('utf-8')
class LoginHandler(BaseHandler):
def post(self):
data = json.loads(self.request.body.decode('utf-8'))
username = data.get('username', None)
password = data.get('password', None)
if username is None or password is None:
self.write({"msg": "Missing username or password"})
self.set_status(HTTPStatus.BAD_REQUEST)
return
# hash password with sha256
password = hashlib.sha256(password.encode('utf-8')).hexdigest()
user = model.User(username=username, password=password)
with backend.get_session() as session:
try:
backend.login(user, session)
self.write({"msg": "Logged in successfully"})
self.set_signed_cookie("username", username)
except RuntimeError as ex:
logger.error(ex)
self.set_status(HTTPStatus.UNAUTHORIZED)
self.write({"msg": "Bad username or password"})
class LogoutHandler(BaseHandler):
@tornado.web.authenticated
def delete(self):
BaseHandler.blacklisted_tokens.add(self.get_cookie("username"))
self.clear_cookie("username")
self.write({"msg": "Successfully logged out"})
Other endpoints in our Tornado-based solution also work as handlers that check whether the user is logged in and can perform the operation they want and return the result of method execution from the back end.
class UserHandler(BaseHandler):
@tornado.web.authenticated
def get(self):
with backend.get_session() as session:
try:
backend.check_admin(model.User(username=self.get_current_user()), session)
except RuntimeError:
self.write({"msg": "You are not admin"})
self.set_status(HTTPStatus.FORBIDDEN)
return
users = backend.list_users(session)
self.write(json.dumps([user.to_dict() for user in users]))
# ...
Transferring a file requires one more action than in other frameworks, as we need to read data from the buffer before adding it to the response.
class GetThroughputHandler(BaseHandler):
@tornado.web.authenticated
def get(self):
with backend.get_session() as session:
# save plt to file
plt = backend.get_throughput(session)
# get figure and set its size to 12in x 8in
fig = plt.gcf()
fig.set_size_inches(12, 8)
buf = BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
content = buf.read()
# set content type to image/png
self.set_header('Content-Type', 'image/png')
self.write(content)
# ...
In general, Tornado code looks more structured than Flask and requires less of a learning curve compared to Django. Letโs analyze and compare these frameworks.
Comparison of results: which framework did best
All three frameworks โ Flask, Django, and Tornado โ allow us to develop a working back end, but as you can see, the code is unique for each framework. Letโs compare them, rating each frameworkโs properties on a scale of 1 to 5, where 1 is poor, and 5 is excellent):
Development speed | Code clarity and maintainability | Application security | |
---|---|---|---|
Flask | 5 | 5 | 5 |
Django | 4 | 4 | 5 |
Tornado | 3 | 4 | 1 |
Note that these are subjective results based on our development experience and framework performance as shown in this article.
Development speed. Flask clearly excels in coding speed, offering a simple and efficient setup that allows you to create the first endpoints in no time. Thatโs why this framework is ideal for projects that need a quick start or MVP. Django, with its automated setup, gets us to endpoint creation just as fast. The heavier framework can add overhead later, since itโs designed to power large-scale projects. Tornado, however, takes multiple steps to launch and execute even a basic endpoint, making it the slowest option.
Code clarity and maintainability. Flask provides a good balance with its moderate complexity and clear methods for adding and defining endpoints, which is why this framework fits projects that require simplicity and flexibility. Django offers excellent code structure, though its unique architecture can be overkill for smaller projects or those requiring a quick project start. Tornado provides a reasonable structure, making it suitable for developers who need flexibility without sacrificing clarity.
Application security. Flask supports HTTPS and offers modules for authorization and JWT authentication, which are must-haves for secure software development. Django provides even more security features, as it includes built-in HTTPS support and comprehensive authentication options. In contrast, Tornado falls behind in security, as it relies heavily on cookies and requires additional server configurations to handle HTTPS, making it a less secure option out of the box.
From our experience at Apriorit, Flask is ideal for small to medium-sized applications as it allows for a quick start to development and works well with smaller codebases. It doesnโt impose significant security limitations, making Flask code usable in many contexts. Django is a better fit for long-term projects due to its more structured framework. Like Flask, it also doesnโt limit itself in terms of security features. Tornado is suitable for servers that have HTTPS set up through a service like Nginx and where security requirements are not particularly stringent.
Conclusion
Selecting the right Python framework for your backend development is a critical decision that can significantly impact your projectโs success. The key is not just choosing a popular framework but finding the one that best fits your specific project requirements. Thanks to Pythonโs rich ecosystem, you can find a fitting framework whether you need fast development, long-term maintainability, or enhanced security for a specific type of product.
Aprioritโs Python experts have worked with various frameworks and know their pros and cons from experience. Whether you need Python for backend development or any other task, we are ready to help you with it.
Planning a Python development project?
Leverage Aprioritโs practical expertise to build an efficient and secure back end for your app.