Flask Web Framework
Flask is a lightweight and flexible web framework for Python. Learn how to build web applications with routing, templates, forms, databases, and REST APIs.
What is Flask?
Flask is a micro web framework written in Python. It's designed to be lightweight, flexible, and easy to use. Unlike full-stack frameworks like Django, Flask provides the essentials for web development while allowing you to choose your own tools for database, authentication, and other components.
"Flask is a microframework for Python based on Werkzeug, Jinja 2 and good intentions."
Installation and Basic Setup
Getting started with Flask:
# Install Flask
# pip install flask
from flask import Flask
# Create a Flask application instance
app = Flask(__name__)
# Define a route
@app.route('/')
def hello_world():
return 'Hello, World!'
# Run the application
if __name__ == '__main__':
app.run(debug=True)
Routing and URL Building
Defining routes and handling different HTTP methods:
from flask import Flask, request, url_for, redirect
app = Flask(__name__)
# Basic routing
@app.route('/')
def index():
return 'Welcome to the homepage!'
@app.route('/about')
def about():
return 'About page'
@app.route('/user/')
def show_user_profile(username):
return f'User: {username}'
@app.route('/post/')
def show_post(post_id):
return f'Post ID: {post_id}'
# Routes with multiple HTTP methods
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
# Process login logic here
return f'Login successful for {username}'
return '''
'''
# Dynamic routing with converters
@app.route('/path/')
def show_subpath(subpath):
return f'Subpath: {subpath}'
@app.route('/float/')
def show_float(number):
return f'Float: {number}'
# URL building
@app.route('/url_demo')
def url_demo():
# Generate URLs for routes
home_url = url_for('index')
about_url = url_for('about')
user_url = url_for('show_user_profile', username='john')
post_url = url_for('show_post', post_id=123)
return f'''
URLs:
Home: {home_url}
About: {about_url}
User: {user_url}
Post: {post_url}
'''
# Redirects
@app.route('/old-page')
def old_page():
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(debug=True)
Templates with Jinja2
Using templates to separate HTML from Python code:
from flask import Flask, render_template, request
app = Flask(__name__)
# Basic template rendering
@app.route('/')
def index():
return render_template('index.html')
@app.route('/user/')
def user(name):
return render_template('user.html', name=name)
# Passing data to templates
@app.route('/users')
def users():
user_list = [
{'name': 'Alice', 'age': 25, 'city': 'New York'},
{'name': 'Bob', 'age': 30, 'city': 'London'},
{'name': 'Charlie', 'age': 35, 'city': 'Paris'}
]
return render_template('users.html', users=user_list)
# Template inheritance
@app.route('/about')
def about():
return render_template('about.html')
# Conditional rendering
@app.route('/profile/')
def profile(username):
user_data = {
'username': username,
'is_admin': username == 'admin',
'posts_count': 42,
'joined_date': '2023-01-15'
}
return render_template('profile.html', user=user_data)
# Form handling with templates
@app.route('/contact', methods=['GET', 'POST'])
def contact():
if request.method == 'POST':
name = request.form['name']
email = request.form['email']
message = request.form['message']
# Process the form data
return render_template('contact_success.html', name=name)
return render_template('../contact.html')
if __name__ == '__main__':
app.run(debug=True)
Template Files
Create these template files in a 'templates' folder:
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}My Flask App{% endblock %}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
nav { background: #333; color: white; padding: 10px; }
nav a { color: white; margin: 0 10px; text-decoration: none; }
.content { margin-top: 20px; }
</style>
</head>
<body>
<nav>
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('users') }}">Users</a>
<a href="{{ url_for('about') }}">About</a>
<a href="{{ url_for('contact') }}">Contact</a>
</nav>
<div class="content">
{% block content %}{% endblock %}
</div>
<footer>
<p>© 2024 My Flask App</p>
</footer>
</body>
</html>
{% extends "base.html" %}
{% block title %}Home - My Flask App{% endblock %}
{% block content %}
<h1>Welcome to My Flask App</h1>
<p>This is the homepage of our Flask application.</p>
<a href="{{ url_for('users') }}">View Users</a>
{% endblock %}
{% extends "base.html" %}
{% block title %}Users - My Flask App{% endblock %}
{% block content %}
<h1>Users</h1>
<table border="1">
<tr>
<th>Name</th>
<th>Age</th>
<th>City</th>
</tr>
{% for user in users %}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.age }}</td>
<td>{{ user.city }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% extends "base.html" %}
{% block title %}{{ user.username }}'s Profile{% endblock %}
{% block content %}
<h1>Profile: {{ user.username }}</h1>
{% if user.is_admin %}
<div style="background: yellow; padding: 10px; margin: 10px 0;">
<strong>Administrator Account</strong>
</div>
{% endif %}
<p>Posts: {{ user.posts_count }}</p>
<p>Joined: {{ user.joined_date }}</p>
{% if user.posts_count > 10 %}
<p>🏆 Active contributor!</p>
{% endif %}
{% endblock %}
Working with Forms
Handling form data and validation:
from flask import Flask, render_template, request, flash, redirect, url_for
from wtforms import Form, StringField, TextAreaField, validators
app = Flask(__name__)
app.secret_key = 'your-secret-key-here' # Required for flashing messages
# WTForms form class
class ContactForm(Form):
name = StringField('Name', [validators.Length(min=1, max=50)])
email = StringField('Email', [validators.Email()])
message = TextAreaField('Message', [validators.Length(min=10)])
@app.route('/contact', methods=['GET', 'POST'])
def contact():
form = ContactForm(request.form)
if request.method == 'POST' and form.validate():
name = form.name.data
email = form.email.data
message = form.message.data
# Process the form data (save to database, send email, etc.)
flash(f'Thank you {name}! Your message has been sent.', 'success')
return redirect(url_for('contact'))
return render_template('../contact.html', form=form)
# Manual form validation
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')
# Validation
errors = []
if not username or len(username) < 3:
errors.append('Username must be at least 3 characters long')
if not email or '@' not in email:
errors.append('Please enter a valid email address')
if not password or len(password) < 6:
errors.append('Password must be at least 6 characters long')
if password != confirm_password:
errors.append('Passwords do not match')
if errors:
for error in errors:
flash(error, 'error')
return render_template('register.html',
username=username,
email=email)
# Registration successful
flash('Registration successful! Please log in.', 'success')
return redirect(url_for('login'))
return render_template('register.html')
# File upload
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
file = request.files['file']
if file and file.filename:
# Save the file
file.save(f'uploads/{file.filename}')
flash(f'File {file.filename} uploaded successfully!', 'success')
return redirect(url_for('upload_file'))
else:
flash('No file selected', 'error')
return render_template('upload.html')
if __name__ == '__main__':
app.run(debug=True)
Database Integration with SQLAlchemy
Working with databases in Flask:
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///blog.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
# Database models
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)
posts = db.relationship('Post', backref='author', lazy=True)
def __repr__(self):
return f''
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
content = db.Column(db.Text, nullable=False)
date_posted = db.Column(db.DateTime, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
def __repr__(self):
return f''
# Create database tables
with app.app_context():
db.create_all()
# Routes
@app.route('/')
def index():
posts = Post.query.order_by(Post.date_posted.desc()).all()
return render_template('index.html', posts=posts)
@app.route('/post/')
def post(post_id):
post = Post.query.get_or_404(post_id)
return render_template('post.html', post=post)
@app.route('/create', methods=['GET', 'POST'])
def create_post():
if request.method == 'POST':
title = request.form['title']
content = request.form['content']
user_id = 1 # In a real app, get from session
post = Post(title=title, content=content, user_id=user_id)
db.session.add(post)
db.session.commit()
flash('Post created successfully!', 'success')
return redirect(url_for('index'))
return render_template('create_post.html')
@app.route('/edit/', methods=['GET', 'POST'])
def edit_post(post_id):
post = Post.query.get_or_404(post_id)
if request.method == 'POST':
post.title = request.form['title']
post.content = request.form['content']
db.session.commit()
flash('Post updated successfully!', 'success')
return redirect(url_for('post', post_id=post.id))
return render_template('edit_post.html', post=post)
@app.route('/delete/', methods=['POST'])
def delete_post(post_id):
post = Post.query.get_or_404(post_id)
db.session.delete(post)
db.session.commit()
flash('Post deleted successfully!', 'success')
return redirect(url_for('index'))
@app.route('/users')
def users():
users = User.query.all()
return render_template('users.html', users=users)
if __name__ == '__main__':
app.run(debug=True)
REST API with Flask
Building RESTful APIs:
from flask import Flask, jsonify, request, abort
import json
app = Flask(__name__)
# Sample data
books = [
{'id': 1, 'title': 'Python Basics', 'author': 'John Doe', 'year': 2020},
{'id': 2, 'title': 'Flask Guide', 'author': 'Jane Smith', 'year': 2021},
{'id': 3, 'title': 'Web Development', 'author': 'Bob Johnson', 'year': 2019}
]
# GET /api/books - Get all books
@app.route('/api/books', methods=['GET'])
def get_books():
return jsonify({'books': books, 'count': len(books)})
# GET /api/books/ - Get a specific book
@app.route('/api/books/', methods=['GET'])
def get_book(book_id):
book = next((b for b in books if b['id'] == book_id), None)
if book is None:
abort(404, description="Book not found")
return jsonify(book)
# POST /api/books - Create a new book
@app.route('/api/books', methods=['POST'])
def create_book():
if not request.json or 'title' not in request.json:
abort(400, description="Title is required")
book = {
'id': books[-1]['id'] + 1 if books else 1,
'title': request.json['title'],
'author': request.json.get('author', 'Unknown'),
'year': request.json.get('year', datetime.now().year)
}
books.append(book)
return jsonify(book), 201
# PUT /api/books/ - Update a book
@app.route('/api/books/', methods=['PUT'])
def update_book(book_id):
book = next((b for b in books if b['id'] == book_id), None)
if book is None:
abort(404, description="Book not found")
if not request.json:
abort(400, description="No data provided")
book['title'] = request.json.get('title', book['title'])
book['author'] = request.json.get('author', book['author'])
book['year'] = request.json.get('year', book['year'])
return jsonify(book)
# DELETE /api/books/ - Delete a book
@app.route('/api/books/', methods=['DELETE'])
def delete_book(book_id):
global books
book = next((b for b in books if b['id'] == book_id), None)
if book is None:
abort(404, description="Book not found")
books = [b for b in books if b['id'] != book_id]
return jsonify({'message': 'Book deleted successfully'})
# Error handlers
@app.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Not found', 'message': str(error)}), 404
@app.errorhandler(400)
def bad_request(error):
return jsonify({'error': 'Bad request', 'message': str(error)}), 400
# API documentation endpoint
@app.route('/api')
def api_docs():
return jsonify({
'name': 'Books API',
'version': '1.0',
'endpoints': {
'GET /api/books': 'Get all books',
'GET /api/books/': 'Get a specific book',
'POST /api/books': 'Create a new book',
'PUT /api/books/': 'Update a book',
'DELETE /api/books/': 'Delete a book'
}
})
if __name__ == '__main__':
app.run(debug=True)
Authentication and Sessions
Implementing user authentication:
from flask import Flask, render_template, request, redirect, url_for, flash, session, g
from werkzeug.security import generate_password_hash, check_password_hash
import functools
app = Flask(__name__)
app.secret_key = 'your-super-secret-key-change-this-in-production'
# Mock user database (in production, use a real database)
users = {
'admin': {
'password': generate_password_hash('admin123'),
'role': 'admin'
},
'user': {
'password': generate_password_hash('user123'),
'role': 'user'
}
}
def login_required(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
if 'username' not in session:
flash('Please log in to access this page.', 'error')
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
def admin_required(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
if 'username' not in session:
flash('Please log in to access this page.', 'error')
return redirect(url_for('login'))
if session.get('role') != 'admin':
flash('Admin access required.', 'error')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
@app.route('/')
def index():
return render_template('index.html')
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = users.get(username)
if user and check_password_hash(user['password'], password):
session['username'] = username
session['role'] = user['role']
flash(f'Welcome back, {username}!', 'success')
return redirect(url_for('dashboard'))
else:
flash('Invalid username or password.', 'error')
return render_template('login.html')
@app.route('/logout')
def logout():
session.clear()
flash('You have been logged out.', 'info')
return redirect(url_for('index'))
@app.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html', username=session['username'])
@app.route('/admin')
@admin_required
def admin():
return render_template('admin.html', users=list(users.keys()))
@app.route('/profile')
@login_required
def profile():
username = session['username']
user_info = {
'username': username,
'role': session.get('role', 'user'),
'last_login': session.get('last_login', 'Never')
}
return render_template('profile.html', user=user_info)
@app.before_request
def update_last_login():
if 'username' in session:
session['last_login'] = 'Just now' # In production, store actual timestamp
# Context processor to make user info available in all templates
@app.context_processor
def inject_user():
return {
'current_user': session.get('username'),
'user_role': session.get('role')
}
if __name__ == '__main__':
app.run(debug=True)
Flask Extensions
Useful Flask extensions for common tasks:
# requirements.txt
Flask==2.3.3
Flask-SQLAlchemy==3.0.5
Flask-WTF==1.1.1
Flask-Login==0.6.3
Flask-Mail==0.9.1
Flask-Migrate==4.0.5
Flask-Caching==2.1.0
Flask-CORS==4.0.0
# Example with multiple extensions
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, validators
from wtforms.validators import DataRequired, Email, EqualTo
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from flask_mail import Mail, Message
from flask_caching import Cache
from flask_cors import CORS
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key')
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
# Initialize extensions
db = SQLAlchemy(app)
mail = Mail(app)
cache = Cache(app, config={'CACHE_TYPE': 'simple'})
CORS(app) # Enable CORS for all routes
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
# User model
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(150), unique=True, nullable=False)
email = db.Column(db.String(150), unique=True, nullable=False)
password_hash = db.Column(db.String(150), nullable=False)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
# WTForms
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), validators.Length(min=2, max=150)])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired(), validators.Length(min=6)])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Login')
# Routes
@app.route('/')
@cache.cached(timeout=300) # Cache for 5 minutes
def index():
return render_template('index.html')
@app.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
# Check if user exists
existing_user = User.query.filter_by(email=form.email.data).first()
if existing_user:
flash('Email already registered', 'error')
return redirect(url_for('register'))
# Create new user
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! Please log in.', 'success')
return redirect(url_for('login'))
return render_template('register.html', form=form)
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and check_password_hash(user.password_hash, form.password.data):
login_user(user)
flash('Login successful!', 'success')
return redirect(url_for('dashboard'))
else:
flash('Invalid email or password', 'error')
return render_template('login.html', form=form)
@app.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html', user=current_user)
@app.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('index'))
@app.route('/send-email')
@login_required
def send_email():
msg = Message(
'Hello from Flask!',
sender=app.config['MAIL_USERNAME'],
recipients=[current_user.email]
)
msg.body = f'Hello {current_user.username}! This is a test email from your Flask app.'
try:
mail.send(msg)
flash('Email sent successfully!', 'success')
except Exception as e:
flash(f'Failed to send email: {str(e)}', 'error')
return redirect(url_for('dashboard'))
# API with CORS
@app.route('/api/data')
def api_data():
data = {
'message': 'This API endpoint supports CORS',
'user': current_user.username if current_user.is_authenticated else None,
'timestamp': datetime.utcnow().isoformat()
}
return jsonify(data)
if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)
Deployment and Production
Deploying Flask applications:
# Production configuration
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
DEBUG = False
TESTING = False
class DevelopmentConfig(Config):
DEBUG = True
class ProductionConfig(Config):
# Production settings
DEBUG = False
# Database URL from environment variable
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
# Security settings
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
REMEMBER_COOKIE_SECURE = True
REMEMBER_COOKIE_HTTPONLY = True
# Gunicorn server file (wsgi.py)
from app import create_app
app = create_app()
if __name__ == '__main__':
app.run()
# Application factory pattern
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
# Initialize extensions
db.init_app(app)
login_manager.init_app(app)
mail.init_app(app)
cache.init_app(app)
# Register blueprints
from app.main import bp as main_bp
app.register_blueprint(main_bp)
from app.auth import bp as auth_bp
app.register_blueprint(auth_bp)
from app.api import bp as api_bp
app.register_blueprint(api_bp)
return app
# requirements.txt for production
Flask==2.3.3
Flask-SQLAlchemy==3.0.5
Flask-Login==0.6.3
Flask-Mail==0.9.1
Flask-WTF==1.1.1
gunicorn==21.2.0
python-dotenv==1.0.0
# Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "wsgi:app"]
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- "8000:8000"
environment:
- FLASK_ENV=production
- SECRET_KEY=your-production-secret-key
- DATABASE_URL=postgresql://user:password@db:5432/app
depends_on:
- db
db:
image: postgres:13
environment:
- POSTGRES_DB=app
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
# .env file (don't commit to version control)
SECRET_KEY=your-production-secret-key
DATABASE_URL=postgresql://user:password@localhost:5432/app
MAIL_USERNAME=your-email@gmail.com
MAIL_PASSWORD=your-app-password
Best Practices
- Use application factory pattern: For better testability and configuration
- Organize with blueprints: Modular application structure
- Use environment variables: For configuration secrets
- Implement proper error handling: User-friendly error pages
- Use Flask-WTF for forms: Built-in CSRF protection
- Implement authentication properly: Use Flask-Login
- Use SQLAlchemy for databases: ORM with migration support
- Cache expensive operations: Use Flask-Caching
- Enable CORS for APIs: Flask-CORS extension
- Use proper logging: For debugging and monitoring
- Test your application: Unit tests and integration tests
- Use HTTPS in production: Security best practices
- Monitor performance: Use profiling tools
- Keep dependencies updated: Security patches
- Use version control: Git for code management
Common Patterns and Tips
- URL routing: Use meaningful URL patterns
- Template organization: Use inheritance and includes
- Database relationships: Proper foreign key constraints
- Form validation: Client and server-side validation
- Session management: Secure session handling
- API design: RESTful principles
- Error handling: Custom error pages
- Security: CSRF protection, input sanitization
- Performance: Database indexing, caching
- Scalability: Stateless design, load balancing
Flask is an excellent choice for building web applications in Python. Its lightweight nature and extensive ecosystem of extensions make it suitable for everything from simple APIs to complex web applications. The key to success with Flask is understanding its philosophy of providing the essentials while allowing you to choose your own tools for specific needs.