Python Web Development with Flask

Flask is a lightweight Python web framework perfect for building web applications and APIs. It’s designed to make getting started quick and easy while being powerful enough for complex applications.

Why Choose Flask?

  • Lightweight: Minimal core, easy to understand
  • Flexible: Choose your own tools and libraries
  • Beginner Friendly: Excellent documentation and gentle learning curve
  • Extensible: Rich ecosystem of extensions
  • Production Ready: Used by companies worldwide
  • Great for APIs: Perfect for REST API development

Installation

Basic Installation

# Install Flask
pip install flask

# Verify installation
python -c "import flask; print('Flask installed successfully!')"

Virtual Environment (Recommended)

# Create virtual environment
python -m venv flask_app

# Activate on Windows
flask_app\Scripts\activate

# Activate on Mac/Linux
source flask_app/bin/activate

# Install Flask in virtual environment
pip install flask

# Deactivate when done
deactivate

Your First Flask App

Basic Hello World

Create a file called app.py:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return '<h1>Hello, World!</h1>'

if __name__ == '__main__':
    app.run(debug=True)

Run your application:

python app.py

Visit http://localhost:5000 in your browser to see “Hello, World!”

Understanding the Code

  • Flask(__name__): Creates Flask application
  • @app.route('/'): Decorator that defines URL route
  • debug=True: Enables development server with auto-reload

Routing

Basic Routes

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def home():
    return '<h1>Welcome to my website!</h1>'

@app.route('/about')
def about():
    return '<h1>About this website</h1>'

@app.route('/contact')
def contact():
    return '<h1>Contact us at [email protected]</h1>'

if __name__ == '__main__':
    app.run(debug=True)

Dynamic Routes with Parameters

@app.route('/user/<username>')
def user_profile(username):
    return f'<h1>Profile for {username}</h1>'

@app.route('/product/<int:product_id>')
def show_product(product_id):
    return f'<h1>Product ID: {product_id}</h1>'

@app.route('/search/<query>')
def search_results(query):
    return f'<h1>Search results for: {query}</h1>'

Route Methods

from flask import request

@app.route('/api/data', methods=['GET'])
def get_data():
    return {'message': 'GET request received'}

@app.route('/api/data', methods=['POST'])
def create_data():
    data = request.get_json()
    return {'message': 'Data created', 'received': data}, 201

@app.route('/api/data/<int:id>', methods=['PUT'])
def update_data(id):
    return {'message': f'Data {id} updated'}

@app.route('/api/data/<int:id>', methods=['DELETE'])
def delete_data(id):
    return {'message': f'Data {id} deleted'}

Templates

Using HTML Templates

Create a templates folder and add index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }}</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .container { max-width: 800px; margin: 0 auto; }
        .header { text-align: center; margin-bottom: 30px; }
        .content { background: #f5f5f5; padding: 20px; border-radius: 8px; }
    </style>
</head>
<body>
    <div class="container">
        <header class="header">
            <h1>{{ title }}</h1>
            <p>{{ description }}</p>
        </header>
        <main class="content">
            {{ content }}
        </main>
    </div>
</body>
</html>

Update your Flask app:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def home():
    return render_template('index.html', 
                         title='My Flask App',
                         description='Welcome to my awesome website!',
                         content='This is the main content area.')

Template Inheritance

Create base.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}My Flask App{% endblock %}</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 0; padding: 20px; }
        nav { background: #333; color: white; padding: 1rem; margin-bottom: 20px; }
        nav a { color: white; margin-right: 1rem; text-decoration: none; }
        .content { max-width: 800px; margin: 0 auto; }
    </style>
</head>
<body>
    <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
        <a href="/contact">Contact</a>
    </nav>
    
    <div class="content">
        {% block content %}{% endblock %}
    </div>
</body>
</html>

Create child templates:

<!-- home.html -->
{% extends "base.html" %}

{% block title %}Home - {% endblock %}

{% block content %}
    <h1>Welcome to My Website</h1>
    <p>This is the home page content.</p>
{% endblock %}

<!-- about.html -->
{% extends "base.html" %}

{% block title %}About - {% endblock %}

{% block content %}
    <h1>About Us</h1>
    <p>We are a company that builds amazing web applications with Flask!</p>
{% endblock %}

Update your Flask app:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def home():
    return render_template('home.html')

@app.route('/about')
def about():
    return render_template('about.html')

Forms and User Input

HTML Forms

Create templates/login.html:

{% extends "base.html" %}

{% block title %}Login - {% endblock %}

{% block content %}
    <h1>Login</h1>
    
    {% if error %}
        <p style="color: red;">{{ error }}</p>
    {% endif %}
    
    <form method="POST">
        <div>
            <label for="username">Username:</label>
            <input type="text" id="username" name="username" required>
        </div>
        <div>
            <label for="password">Password:</label>
            <input type="password" id="password" name="password" required>
        </div>
        <div>
            <button type="submit">Login</button>
        </div>
    </form>
{% endblock %}

Form Handling in Flask

from flask import Flask, render_template, request, redirect, url_for, flash, session

app = Flask(__name__)
app.secret_key = 'your-secret-key-here'  # Required for sessions

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        
        # Simple validation (use real authentication in production)
        if username == 'admin' and password == 'password123':
            session['logged_in'] = True
            session['username'] = username
            flash('Login successful!', 'success')
            return redirect(url_for('dashboard'))
        else:
            flash('Invalid username or password', 'error')
    
    return render_template('login.html')

@app.route('/dashboard')
def dashboard():
    if not session.get('logged_in'):
        return redirect(url_for('login'))
    
    username = session.get('username')
    return f'<h1>Welcome to your dashboard, {username}!</h1>'

@app.route('/logout')
def logout():
    session.clear()
    flash('You have been logged out', 'info')
    return redirect(url_for('login'))

Displaying Flash Messages

Add this to your base template:

{% with messages = get_flashed_messages(with_categories=true) %}
    {% if messages %}
        {% for category, message in messages %}
            <div class="flash flash-{{ category }}">
                {{ message }}
            </div>
        {% endfor %}
    {% endif %}
{% endwith %}

<style>
    .flash { padding: 10px; margin-bottom: 20px; border-radius: 4px; }
    .flash-success { background: #d4edda; color: #155724; }
    .flash-error { background: #f8d7da; color: #721c24; }
    .flash-info { background: #d1ecf1; color: #0c5460; }
</style>

Working with Databases

Using SQLite

Install Flask-SQLAlchemy:

pip install flask-sqlalchemy

Database models:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
db = SQLAlchemy(app)

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 = db.Column(db.String(200), nullable=False)
    created_at = db.Column(db.DateTime, default=db.func.current_timestamp())

    def __repr__(self):
        return f'<User {self.username}>'

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(200), nullable=False)
    content = db.Column(db.Text, nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    created_at = db.Column(db.DateTime, default=db.func.current_timestamp())

    def __repr__(self):
        return f'<Post {self.title}>'

# Create database tables
with app.app_context():
    db.create_all()

CRUD Operations

from flask import render_template, request, redirect, url_for, flash

@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')
        
        # Check if user already exists
        if User.query.filter_by(username=username).first():
            flash('Username already exists', 'error')
            return redirect(url_for('register'))
        
        # Create new user (hash password in real app!)
        new_user = User(username=username, email=email, password=password)
        db.session.add(new_user)
        db.session.commit()
        
        flash('Registration successful!', 'success')
        return redirect(url_for('login'))
    
    return render_template('register.html')

@app.route('/posts')
def posts():
    all_posts = Post.query.order_by(Post.created_at.desc()).all()
    return render_template('posts.html', posts=all_posts)

@app.route('/post/new', methods=['GET', 'POST'])
def new_post():
    if request.method == 'POST':
        title = request.form.get('title')
        content = request.form.get('content')
        
        new_post = Post(title=title, content=content, user_id=1)  # Hardcoded user ID
        db.session.add(new_post)
        db.session.commit()
        
        flash('Post created successfully!', 'success')
        return redirect(url_for('posts'))
    
    return render_template('new_post.html')

APIs with Flask

RESTful API

from flask import Flask, jsonify, request
from models import db, User, Post

@app.route('/api/users', methods=['GET'])
def get_users():
    users = User.query.all()
    return jsonify([{
        'id': user.id,
        'username': user.username,
        'email': user.email,
        'created_at': user.created_at.isoformat()
    } for user in users])

@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    user = User.query.get_or_404(user_id)
    return jsonify({
        'id': user.id,
        'username': user.username,
        'email': user.email,
        'created_at': user.created_at.isoformat()
    })

@app.route('/api/users', methods=['POST'])
def create_user():
    data = request.get_json()
    
    if not data or not data.get('username') or not data.get('email'):
        return jsonify({'error': 'Username and email required'}), 400
    
    # Check if user exists
    if User.query.filter_by(username=data['username']).first():
        return jsonify({'error': 'Username already exists'}), 400
    
    user = User(username=data['username'], email=data['email'])
    db.session.add(user)
    db.session.commit()
    
    return jsonify({
        'id': user.id,
        'username': user.username,
        'email': user.email
    }), 201

API Authentication with JWT

Install required packages:

pip install flask-jwt-extended
pip install bcrypt

Authentication setup:

from flask import Flask, request, jsonify
from flask_jwt_extended import JWTManager, jwt_required, create_access_token
import bcrypt

app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'your-super-secret-jwt-key'
jwt = JWTManager(app)

@app.route('/api/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    user = User.query.filter_by(username=username).first()
    
    if not user or not bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-8')):
        return jsonify({'error': 'Invalid credentials'}), 401
    
    access_token = create_access_token(identity=user.id)
    return jsonify({
        'access_token': access_token,
        'user': {
            'id': user.id,
            'username': user.username
        }
    })

@app.route('/api/profile')
@jwt_required()
def profile():
    current_user_id = get_jwt_identity()
    user = User.query.get(current_user_id)
    
    return jsonify({
        'id': user.id,
        'username': user.username,
        'email': user.email
    })

File Uploads

Basic File Upload

from flask import Flask, request, render_template, redirect, url_for
from werkzeug.utils import secure_filename
import os

app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024  # 16MB max file size

ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            return render_template('upload.html', error='No file selected')
        
        file = request.files['file']
        
        if file.filename == '':
            return render_template('upload.html', error='No file selected')
        
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return redirect(url_for('uploaded_file', filename=filename))
        else:
            return render_template('upload.html', error='File type not allowed')
    
    return render_template('upload.html')

Upload template:

{% extends "base.html" %}

{% block title %}Upload File - {% endblock %}

{% block content %}
    <h1>Upload a File</h1>
    
    {% if error %}
        <p style="color: red;">{{ error }}</p>
    {% endif %}
    
    <form method="POST" enctype="multipart/form-data">
        <div>
            <label for="file">Choose file:</label>
            <input type="file" name="file" id="file">
        </div>
        <div>
            <button type="submit">Upload</button>
        </div>
    </form>
{% endblock %}

Error Handling

Custom Error Handlers

from flask import Flask, render_template

app = Flask(__name__)

@app.errorhandler(404)
def not_found_error(error):
    return render_template('404.html'), 404

@app.errorhandler(500)
def internal_error(error):
    return render_template('500.html'), 500

@app.errorhandler(403)
def forbidden_error(error):
    return render_template('403.html'), 403

Global Error Handling

from flask import Flask, request, jsonify
import logging

app = Flask(__name__)

# Configure logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

@app.before_request
def before_request():
    logger.info(f"Request: {request.method} {request.url}")

@app.after_request
def after_request(response):
    logger.info(f"Response status: {response.status_code}")
    return response

@app.errorhandler(Exception)
def handle_exception(e):
    logger.error(f"Unhandled exception: {str(e)}")
    
    if request.accept_mimetypes.accept_json:
        return jsonify({'error': 'Internal server error'}), 500
    else:
        return render_template('error.html', error=str(e)), 500

Flask Extensions

Popular Extensions

# Database
pip install flask-sqlalchemy
pip install flask-migrate

# Authentication
pip install flask-login
pip install flask-jwt-extended

# Forms
pip install flask-wtf
pip install wtforms

# Caching
pip install flask-caching
pip install redis

# Admin interface
pip install flask-admin

# REST API
pip install flask-restful
pip install flask-marshmallow

Using Flask-WTF

from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length

class LoginForm(FlaskForm):
    username = StringField('Username', 
                       validators=[DataRequired(), Length(min=4, max=25)])
    email = StringField('Email', 
                    validators=[DataRequired(), Email()])
    password = PasswordField('Password', 
                           validators=[DataRequired(), Length(min=6)])
    submit = SubmitField('Login')

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    
    if form.validate_on_submit():
        # Process form data
        username = form.username.data
        password = form.password.data
        # Authentication logic here
        
    return render_template('login.html', form=form)

Deployment

Gunicorn Production Server

# Install production server
pip install gunicorn

# Run with Gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 app:app

# Configuration file (gunicorn.conf.py)
bind = "0.0.0.0:8000"
workers = 4
worker_class = "sync"
worker_connections = 1000
max_requests = 1000
max_requests_jitter = 50
timeout = 30
keepalive = 2

Docker Deployment

Create Dockerfile:

FROM python:3.9-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY . .

# Create non-root user
RUN useradd --create-home --shell /bin/bash appuser
USER appuser

# Expose port
EXPOSE 5000

# Run application
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "app:app"]

Create requirements.txt:

Flask==2.3.2
Flask-SQLAlchemy==1.0.0
Flask-WTF==1.0.0
gunicorn==20.1.0

Docker Compose

Create docker-compose.yml:

version: '3.8'

services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      - FLASK_ENV=production
    volumes:
      - ./uploads:/app/uploads
    depends_on:
      - db

  db:
    image: postgres:13
    environment:
      - POSTGRES_DB=flask_app
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - postgres_data:/var/lib/postgresql/data

Complete Project Example

Blog Application Structure

flask_blog/
├── app.py
├── requirements.txt
├── config.py
├── models.py
├── forms.py
├── templates/
│   ├── base.html
│   ├── index.html
│   ├── login.html
│   ├── register.html
│   └── dashboard.html
├── static/
│   ├── css/
│   │   └── style.css
│   └── js/
│       └── main.js
└── uploads/

Main application file:

from flask import Flask, render_template, redirect, url_for, flash, session
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
import os

app = Flask(__name__)
app.config.from_pyfile('config.py')
db = SQLAlchemy(app)

# Import models and forms
from models import User, Post
from forms import LoginForm, RegistrationForm

# Routes
@app.route('/')
def index():
    posts = Post.query.order_by(Post.created_at.desc()).limit(5).all()
    return render_template('index.html', posts=posts)

@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        # Login logic here
        session['user_id'] = form.user.id
        flash('Login successful!', 'success')
        return redirect(url_for('dashboard'))
    
    return render_template('login.html', form=form)

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(debug=True)

Best Practices

Security Tips

# Never use eval() with user input
# Always validate and sanitize input
# Use parameterized queries for database operations
# Store secrets in environment variables, not code
# Use HTTPS in production
# Implement proper authentication and authorization
# Keep dependencies updated

# Good: Using parameterized queries
user = User.query.filter_by(username=username).first()

# Bad: String concatenation (SQL injection risk)
query = f"SELECT * FROM user WHERE username = '{username}'"

Performance Tips

# Use database indexes
# Implement caching for expensive operations
# Use connection pooling
# Optimize database queries
# Use lazy loading for relationships
# Monitor application performance
# Use pagination for large datasets

# Example of pagination
@app.route('/posts')
def posts():
    page = request.args.get('page', 1, type=int)
    per_page = 10
    posts = Post.query.offset((page - 1) * per_page).limit(per_page).all()
    return render_template('posts.html', posts=posts, page=page)

External Resources:

Related Tutorials:

Last updated on