Flask Best Practices

This guide covers best practices for developing Flask applications, helping you write more maintainable, scalable, and professional code.

Project Structure

Use Application Factory Pattern

# app/__init__.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

def create_app(config_name='default'):
    app = Flask(__name__)
    app.config.from_object(f'config.{config_name}')
    
    # Initialize extensions
    db.init_app(app)
    
    # Register blueprints
    from app.main import main as main_blueprint
    app.register_blueprint(main_blueprint)
    
    return app

Organize Code with Blueprints

# app/main/__init__.py
from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, errors
myapp/
├── app/
│   ├── __init__.py
│   ├── models.py
│   ├── main/
│   │   ├── __init__.py
│   │   ├── views.py
│   │   └── errors.py
│   ├── auth/
│   │   ├── __init__.py
│   │   └── views.py
│   ├── static/
│   └── templates/
├── migrations/
├── tests/
├── venv/
├── config.py
├── requirements.txt
└── run.py

Configuration Management

Use Environment-Specific Configs

# config.py
import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard-to-guess-string'
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    
    @staticmethod
    def init_app(app):
        pass

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = 'sqlite:///dev.db'

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')

config = {
    'development': DevelopmentConfig,
    'production': ProductionConfig,
    'default': DevelopmentConfig
}

Use Environment Variables

# .env
SECRET_KEY=your-secret-key
DATABASE_URL=postgresql://user:pass@localhost/dbname
FLASK_DEBUG=1

Database Best Practices

Use Migrations

Always use Flask-Migrate for database schema changes:

flask db init
flask db migrate -m "Initial migration"
flask db upgrade

Implement Proper Models

from datetime import datetime
from app import db

class User(db.Model):
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), unique=True, index=True)
    email = db.Column(db.String(120), unique=True, index=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    def __repr__(self):
        return f'<User {self.username}>'

Use Database Sessions Properly

from app import db

# Good practice
try:
    user = User(username='john')
    db.session.add(user)
    db.session.commit()
except Exception as e:
    db.session.rollback()
    raise e
finally:
    db.session.close()

Error Handling

Implement Custom Error Pages

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

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

Use Logging

import logging
from logging.handlers import RotatingFileHandler

if not app.debug:
    file_handler = RotatingFileHandler('logs/app.log', maxBytes=10240, backupCount=10)
    file_handler.setFormatter(logging.Formatter(
        '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'
    ))
    file_handler.setLevel(logging.INFO)
    app.logger.addHandler(file_handler)
    app.logger.setLevel(logging.INFO)
    app.logger.info('Application startup')

Security Best Practices

Protect Against CSRF

from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect()
csrf.init_app(app)

Use Strong Secret Keys

import secrets

# Generate a secure secret key
secret_key = secrets.token_hex(32)

Validate User Input

from flask_wtf import FlaskForm
from wtforms import StringField, validators

class RegistrationForm(FlaskForm):
    username = StringField('Username', [
        validators.Length(min=4, max=25),
        validators.Regexp('^[A-Za-z0-9_]+$')
    ])
    email = StringField('Email', [validators.Email()])

Performance Optimization

Use Caching

from flask_caching import Cache

cache = Cache(config={'CACHE_TYPE': 'simple'})
cache.init_app(app)

@app.route('/expensive')
@cache.cached(timeout=300)
def expensive_operation():
    # Expensive computation
    return result

Implement Pagination

@app.route('/users')
def users():
    page = request.args.get('page', 1, type=int)
    users = User.query.paginate(page=page, per_page=20)
    return render_template('users.html', users=users)

Use Database Connection Pooling

app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {
    'pool_size': 10,
    'pool_recycle': 3600,
    'pool_pre_ping': True
}

Testing

Write Unit Tests

import unittest
from app import create_app, db

class BasicTestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()
        self.client = self.app.test_client()
    
    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()
    
    def test_home_page(self):
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)

Deployment

Use Production WSGI Server

Never use Flask's built-in server in production:

# Use Gunicorn
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"

Set Up Proper Logging

# Production logging configuration
if not app.debug and not app.testing:
    if app.config['LOG_TO_STDOUT']:
        stream_handler = logging.StreamHandler()
        stream_handler.setLevel(logging.INFO)
        app.logger.addHandler(stream_handler)
    else:
        if not os.path.exists('logs'):
            os.mkdir('logs')
        file_handler = RotatingFileHandler('logs/app.log',
                                          maxBytes=10240, backupCount=10)
        file_handler.setFormatter(logging.Formatter(
            '%(asctime)s %(levelname)s: %(message)s '
            '[in %(pathname)s:%(lineno)d]'))
        file_handler.setLevel(logging.INFO)
        app.logger.addHandler(file_handler)

Code Quality

Follow PEP 8

Use tools like flake8 or black for code formatting:

pip install black flake8
black app/
flake8 app/

Use Type Hints

from typing import Optional, List

def get_user(user_id: int) -> Optional[User]:
    return User.query.get(user_id)

def get_all_users() -> List[User]:
    return User.query.all()

Document Your Code

def process_payment(amount: float, currency: str = 'USD') -> dict:
    """
    Process a payment transaction.
    
    Args:
        amount: The payment amount
        currency: The currency code (default: USD)
    
    Returns:
        A dictionary containing transaction details
    
    Raises:
        ValueError: If amount is negative
    """
    if amount < 0:
        raise ValueError("Amount cannot be negative")
    # Process payment logic
    return {'status': 'success', 'amount': amount}

Summary

Following these best practices will help you:

  • Write more maintainable and scalable code
  • Improve application security
  • Enhance performance
  • Make debugging easier
  • Facilitate team collaboration

Remember that best practices evolve over time, so stay updated with the Flask community and documentation.