Python Flask: Lightweight Web Framework
Flask calls itself a 'micro' framework, but don't mistake that for limited. The 'micro' refers to Flask's philosophy: keep the core simple and let developers choose their own tools for databases,...
Key Insights
- Flask’s minimalist core gives you complete control over your architecture—you add only the components you need, making it ideal for APIs, microservices, and applications where Django feels like overkill.
- The decorator-based routing system and Jinja2 templating engine provide an intuitive developer experience that gets you from idea to working prototype in minutes, not hours.
- Flask scales from single-file prototypes to production applications through Blueprints and extensions, letting you start simple and add complexity only when your application demands it.
Introduction to Flask
Flask calls itself a “micro” framework, but don’t mistake that for limited. The “micro” refers to Flask’s philosophy: keep the core simple and let developers choose their own tools for databases, form validation, authentication, and other concerns. Unlike Django’s “batteries included” approach, Flask gives you a lightweight foundation and trusts you to make architectural decisions.
This makes Flask perfect for REST APIs, microservices, small-to-medium web applications, and situations where you need fine-grained control over dependencies. Choose Flask when you want to avoid framework lock-in or when your project doesn’t need Django’s admin panel, ORM, and built-in authentication system.
Here’s the canonical Flask “Hello World”:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello, World!'
Run it with flask run, and you have a working web server. That’s the Flask philosophy in action.
Setting Up Your First Flask Application
Start with a virtual environment to isolate dependencies:
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install flask python-dotenv
For any real application, structure matters. Here’s a production-ready layout:
myapp/
├── app/
│ ├── __init__.py
│ ├── routes.py
│ ├── models.py
│ └── templates/
├── config.py
├── requirements.txt
└── run.py
Your config.py should handle different environments:
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///app.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(Config):
DEBUG = True
class ProductionConfig(Config):
DEBUG = False
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
Initialize your application in app/__init__.py:
from flask import Flask
from config import config
def create_app(config_name='default'):
app = Flask(__name__)
app.config.from_object(config[config_name])
from app import routes
app.register_blueprint(routes.bp)
return app
This factory pattern enables testing with different configurations and keeps your application modular.
Routing and Request Handling
Flask’s routing uses decorators that map URLs to Python functions. The system is intuitive and powerful:
from flask import Blueprint, request, jsonify
bp = Blueprint('api', __name__, url_prefix='/api')
@bp.route('/tasks', methods=['GET'])
def get_tasks():
# Retrieve all tasks
return jsonify({'tasks': []})
@bp.route('/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
# Retrieve specific task
return jsonify({'id': task_id, 'title': 'Example Task'})
@bp.route('/tasks', methods=['POST'])
def create_task():
data = request.get_json()
title = data.get('title')
if not title:
return jsonify({'error': 'Title required'}), 400
# Create task in database
return jsonify({'id': 1, 'title': title}), 201
@bp.route('/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
data = request.get_json()
# Update task logic
return jsonify({'id': task_id, 'updated': True})
@bp.route('/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
# Delete task logic
return '', 204
The request object provides access to form data, JSON payloads, query parameters, and headers. For query strings, use request.args.get('param'). For form data, use request.form.get('field').
Templates and Static Files
Flask uses Jinja2 for templating. Create a base template for consistency:
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My App{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<nav>
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('about') }}">About</a>
</nav>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>
Child templates extend the base:
<!-- templates/tasks.html -->
{% extends "base.html" %}
{% block title %}Tasks{% endblock %}
{% block content %}
<h1>My Tasks</h1>
<ul>
{% for task in tasks %}
<li>{{ task.title }} - {{ task.status }}</li>
{% else %}
<li>No tasks yet!</li>
{% endfor %}
</ul>
{% endblock %}
Render templates from your routes:
from flask import render_template
@app.route('/tasks')
def tasks():
tasks = get_tasks_from_db()
return render_template('tasks.html', tasks=tasks)
Working with Forms and Databases
Install Flask-WTF and SQLAlchemy:
pip install flask-wtf flask-sqlalchemy flask-migrate
Define a model:
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
def __repr__(self):
return f'<User {self.username}>'
Create a form with validation:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[
DataRequired(), Length(min=3, max=80)
])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[
DataRequired(), Length(min=8)
])
submit = SubmitField('Register')
Handle the form in your route:
from flask import flash, redirect, url_for
from werkzeug.security import generate_password_hash
@bp.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(
username=form.username.data,
email=form.email.data,
password_hash=generate_password_hash(form.password.data)
)
db.session.add(user)
db.session.commit()
flash('Registration successful!', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
Blueprints and Application Scaling
As applications grow, Blueprints organize code into modules:
# app/auth/__init__.py
from flask import Blueprint
bp = Blueprint('auth', __name__, url_prefix='/auth')
from app.auth import routes
# app/auth/routes.py
from app.auth import bp
@bp.route('/login', methods=['GET', 'POST'])
def login():
# Login logic
pass
@bp.route('/logout')
def logout():
# Logout logic
pass
Register blueprints in your application factory:
def create_app(config_name='default'):
app = Flask(__name__)
app.config.from_object(config[config_name])
db.init_app(app)
from app.auth import bp as auth_bp
from app.api import bp as api_bp
from app.main import bp as main_bp
app.register_blueprint(auth_bp)
app.register_blueprint(api_bp)
app.register_blueprint(main_bp)
return app
This modular structure keeps related functionality together and makes testing easier.
Deployment and Production Considerations
Never run Flask’s development server in production. Use a WSGI server like Gunicorn:
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"
Create a production configuration with environment variables:
# .env
SECRET_KEY=your-production-secret-key
DATABASE_URL=postgresql://user:pass@localhost/dbname
FLASK_ENV=production
Here’s a Dockerfile for containerized deployment:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV FLASK_APP=run.py
ENV FLASK_ENV=production
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:create_app()"]
Add security headers to protect your application:
@app.after_request
def set_security_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
return response
Flask’s simplicity is its strength. You build exactly what you need, no more, no less. Start with the basics, add extensions as requirements emerge, and scale your architecture as your application grows. The framework stays out of your way while providing the tools to build production-ready web applications.