Skip to main content
In this lesson we demonstrate strategies for managing long development sessions while building a slightly complex, multi-step web application: a Flask-based image optimizer with drag-and-drop uploads, format-aware compression, and real-time controls (quality, resize, sharpen/blur, metadata stripping, etc.). The aim is to show safe iteration patterns, ways to keep context clear, and how to document progress so long sessions remain reliable and reproducible. What you’ll learn:
  • How to define clear requirements and deliverables for iterative development.
  • How to build a Flask app with upload, validation, optimization, and cleanup.
  • How to run, debug, and extend the app during long interactive sessions.
  • How to capture session notes so future work can rehydrate context quickly.

Requirements (summary)

Build a single image optimization web app using Flask.

Requirements:
- Flask web UI with drag-and-drop upload
- Support JPEG, PNG, WebP only (reject SVG/GIF)
- Rate limiting: 30 requests/min per IP
- Max upload size: 25 MB
- Auto-cleanup of temp files (no retention)
- Show "Before" uploaded image and an "After" pane (placeholder until optimized)
- Use Pillow (PIL) for image I/O; keep OpenCV available for future features
- Dockerize for production with Gunicorn (deployment later)
Deliverables:
- Flask app with routes, config, validators, cleanup
- HTML templates, static JS/CSS
- requirements.txt
- SESSION_NOTES.md documenting what was done
To make the above clearer at a glance, here is a compact reference table:
RequirementPurposeNotes
Flask web UIUser uploads & controlsDrag-and-drop + responsive templates
Supported formatsJPEG / PNG / WebP onlyReject SVG and GIF to avoid edge-case processing
Rate limitingThrottle abuse30 requests/min per IP (Flask-Limiter)
Max upload sizeProtect memory & disk25 MB via MAX_CONTENT_LENGTH
Temp filesNo long-term storageUUID filenames + auto-cleanup
Image I/OPrimary processingPillow (PIL); keep OpenCV for future)
ProductionDeployable containerDocker + Gunicorn recommended

Project scaffold & high-level notes

Deliverables included:
  • app.py — Flask server core
  • templates/index.html — UI with drag-and-drop
  • static/js/main.js — upload + optimize client logic
  • static/css/style.css — responsive UI
  • requirements.txt
  • SESSION_NOTES.md — session documentation
Key decisions:
  • Use Flask-Limiter for IP-based rate limiting. For development the in-memory store is sufficient; switch to Redis for production to preserve rate-limit state across processes.
  • Use Pillow for primary image I/O and operations for broad cross-platform compatibility; keep OpenCV (cv2) installed for future advanced processing.
  • Save uploads with secure, UUID-based filenames into a temporary directory and track them in a thread-safe set. Clean up on response, periodically, and at process exit.
For development, an in-memory Flask-Limiter store is acceptable. In production, configure a persistent backend (for example, Redis) to avoid lost rate-limit state and to scale across worker processes.

Example app.py (core pieces)

Below is a consolidated, corrected example that captures the main functionality described in the lesson: upload validation, rate limiting, MAX_CONTENT_LENGTH, temp file handling, error handlers, and the optimize endpoint skeleton. This is not the full file, but the important, runnable parts:
# app.py
import os
import uuid
import tempfile
import atexit
import traceback
from datetime import datetime, timedelta
from threading import Lock
from flask import Flask, request, render_template, jsonify, send_file, url_for
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from PIL import Image, ImageFilter, ImageOps
import cv2  # kept available for future features
from werkzeug.utils import secure_filename

app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 25 * 1024 * 1024  # 25 MB
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
app.config['UPLOAD_FOLDER'] = tempfile.mkdtemp()

# Rate limiting: 30 requests per minute per IP
limiter = Limiter(app, key_func=get_remote_address, default_limits=["30 per minute"])

ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'webp'}
TEMP_FILES = set()
TEMP_FILES_LOCK = Lock()

def allowed_file(filename: str) -> bool:
    ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
    return ext in ALLOWED_EXTENSIONS

def register_temp_file(path: str):
    with TEMP_FILES_LOCK:
        TEMP_FILES.add(path)

def cleanup_temp_file(path: str):
    try:
        with TEMP_FILES_LOCK:
            TEMP_FILES.discard(path)
        if os.path.exists(path):
            os.remove(path)
    except Exception:
        traceback.print_exc()

@atexit.register
def cleanup_all_temp_files():
    with TEMP_FILES_LOCK:
        files = list(TEMP_FILES)
    for f in files:
        try:
            if os.path.exists(f):
                os.remove(f)
        except Exception:
            traceback.print_exc()

@app.errorhandler(413)
def too_large(e):
    return jsonify({'error': 'File too large. Maximum size is 25MB'}), 413

@app.errorhandler(429)
def ratelimit_handler(e):
    return jsonify({'error': 'Rate limit exceeded. Please try again later.'}), 429

@app.route('/', methods=['GET'])
def index():
    return render_template('index.html')

@app.route('/upload', methods=['POST'])
@limiter.limit("30/minute")
def upload():
    file = request.files.get('file')
    if not file or file.filename == '':
        return jsonify({'error': 'No file uploaded'}), 400
    if not allowed_file(file.filename):
        return jsonify({'error': 'Unsupported file type'}), 400

    filename = f"{uuid.uuid4().hex}_{secure_filename(file.filename)}"
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    file.save(filepath)
    register_temp_file(filepath)

    return jsonify({
        'success': True,
        'filename': filename,
        'url': url_for('get_temp_file', filename=filename)
    }), 200

@app.route('/temp/<filename>')
def get_temp_file(filename):
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if not os.path.exists(filepath):
        return jsonify({'error': 'File not found'}), 404
    return send_file(filepath)

@app.route('/optimize/<filename>', methods=['POST'])
@limiter.limit("30/minute")
def optimize(filename):
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
    if not os.path.exists(filepath):
        return jsonify({'error': 'Source file not found'}), 404

    try:
        # Read parameters
        quality = int(request.form.get('quality', 85))
        resize_percent = int(request.form.get('resize_percent', 100))
        strip_metadata = request.form.get('strip_metadata', 'true').lower() == 'true'
        auto_orient = request.form.get('auto_orient', 'true').lower() == 'true'
        sharpen_value = float(request.form.get('sharpen', 0.0))
        output_format = request.form.get('format', 'JPEG').upper()  # JPEG, PNG, WEBP

        # Prepare optimized filename
        optimized_filename = f"opt_{uuid.uuid4().hex}_{filename}"
        optimized_filepath = os.path.join(app.config['UPLOAD_FOLDER'], optimized_filename)

        # Ensure previous optimized file cleaned up
        if os.path.exists(optimized_filepath):
            cleanup_temp_file(optimized_filepath)

        # Open and process image with Pillow
        with Image.open(filepath) as img:
            if auto_orient:
                img = ImageOps.exif_transpose(img)

            # Resize if needed
            if resize_percent != 100:
                width = max(1, int(img.width * resize_percent / 100))
                height = max(1, int(img.height * resize_percent / 100))
                img = img.resize((width, height), Image.LANCZOS)

            # Sharpen/blur handling: use UnsharpMask for sharpening, GaussianBlur for negative values
            if sharpen_value > 0:
                # UnsharpMask expects integers for percent - compute safe int
                percent = max(0, min(500, int(round(sharpen_value * 100))))
                img = img.filter(ImageFilter.UnsharpMask(radius=2, percent=percent, threshold=3))
            elif sharpen_value < 0:
                radius = max(0, abs(sharpen_value))
                img = img.filter(ImageFilter.GaussianBlur(radius=radius))

            # Handle metadata: include EXIF only when strip_metadata is False and EXIF exists
            save_kwargs = {}
            if not strip_metadata:
                exif_bytes = img.info.get('exif')
                if exif_bytes:
                    save_kwargs['exif'] = exif_bytes

            # Format-aware saving and mode adjustments
            fmt = output_format
            if fmt == 'JPEG':
                # JPEG cannot handle alpha - convert if necessary
                if img.mode in ('RGBA', 'LA', 'P'):
                    img = img.convert('RGB')
                save_kwargs['quality'] = max(1, min(95, quality))
                save_kwargs['optimize'] = True
                save_kwargs['progressive'] = True
                fmt = 'JPEG'
            elif fmt == 'WEBP':
                save_kwargs['quality'] = max(1, min(100, quality))
                fmt = 'WEBP'
            else:  # PNG or other
                fmt = 'PNG'

            img.save(optimized_filepath, format=fmt, **save_kwargs)

        register_temp_file(optimized_filepath)
        return jsonify({'success': True, 'optimized_url': url_for('get_temp_file', filename=optimized_filename)}), 200

    except Exception as e:
        print(f"Optimization error: {e}")
        traceback.print_exc()
        return jsonify({'error': f'Failed to optimize image: {str(e)}'}), 500

if __name__ == '__main__':
    # If port 5000 is in use by system services (e.g., macOS AirPlay Receiver), choose a different port
    app.run(debug=True, port=5001)
Notes:
  • Keep the cv2 import available but prefer Pillow operations for cross-platform portability and smaller runtime surface.
  • The code prints tracebacks on exceptions to help during interactive debugging—a useful habit for long sessions.

requirements.txt (example)

Flask==2.3.3
Flask-Limiter==3.5.0
Pillow==10.0.0
opencv-python-headless==4.8.1.78
gunicorn==20.1.0

Run locally: virtual environment, install, run

Quick local setup:
# Create virtual environment
python3 -m venv venv

# Activate (macOS/Linux)
source venv/bin/activate

# Install dependencies
pip install -r requirements.txt

# Run in development (choose an available port)
python app.py
# or explicitly
python -m flask run --port 5001
Common console messages and how to address them:
  • zsh: command not found: python
    • Use python3 instead of python on systems where python is not aliased.
  • Flask-Limiter warning about in-memory storage:
    • Development-only; configure Redis for production.
  • Address already in use (port 5000):
    • Use lsof to find the process and stop it, or run the app on another port.
Troubleshooting commands (macOS / Linux):
# Find process using port 5000
lsof -i :5000

# Kill process by PID (be careful)
kill -9 <PID>

# Alternatively, run app on a different port:
python app.py  # if app.py sets port=5001 or use flask run --port 5001

UI and example workflow

The app provides:
  • Drag-and-drop upload (client-side JS)
  • “Before” pane that displays the uploaded image
  • “After” pane with placeholder until the image is optimized
  • Controls: quality slider, resize percent, sharpen/blur slider, strip metadata toggle, format dropdown, presets
When a user uploads an image, the server stores it temporarily. The UI displays the “before” image and allows the user to adjust optimization parameters, which are applied server-side against the same temporary source so you can iterate without re-uploading. Here’s the UI screenshot referenced in the lesson:
A screenshot of an "Image Optimizer" webpage showing a vintage black-and-white family portrait in the "Before" pane and an empty "After" pane with an "Optimize Image" button highlighted. The interface sits on a purple gradient background with a red notice bar along the bottom.

Debugging: common runtime errors & fixes

  1. 500 Internal Server Error on /optimize:
    • Inspect server logs and printed tracebacks.
    • Common causes:
      • Passing float where an integer is required (e.g., UnsharpMask percent).
      • Trying to save a JPEG from an image with an alpha channel.
      • Attempting to re-optimize a non-existent or already-deleted source file.
    • Fixes:
      • Cast or round floats to integers where Pillow expects ints.
      • Convert image mode to RGB before saving as JPEG.
      • Ensure the original upload is preserved as the canonical source; write optimized images to new temp files.
  2. Port conflicts:
    • On macOS, system services (like AirPlay Receiver) may claim port 5000. Use lsof to identify or change the app port.
Example console error captured in the lesson:
Failed to load resource: the server responded with a status of 500 (INTERNAL SERVER ERROR)
http://localhost:5001/optimize/b0376_eb95a_morgans.png 500 (INTERNAL SERVER ERROR)
Add a robust exception handler (with traceback printing and JSON error payloads) to speed up iterative debugging.

Adding new features iteratively

During the lesson we iteratively added:
  • Quality slider for JPEG/WebP (1–95 for JPEG)
  • Resize by percentage (10–200%) while preserving aspect ratio
  • Strip metadata toggle (EXIF removal)
  • Sharpen and blur sliders (convert floats safely for Pillow filters)
  • Contrast/brightness controls (planned/added later)
  • Format conversion (JPEG, PNG, WebP)
  • Live preview workflow (optimize multiple times without re-uploading)
  • Auto-cleanup for temp files and thread-safe tracking of temp file paths
When adding features:
  • Validate all incoming parameters (type and bounds).
  • Cast floats to ints where required by Pillow filtering APIs (or compute safe integer equivalents).
  • Preserve the original upload as the source for all reprocessing so iterative tuning produces reproducible results.

Session documentation (SESSION_NOTES.md)

Capture progress and decisions in a session notes file so future sessions or collaborators can rehydrate context quickly. Example (excerpt):
## Project Overview
Built a Flask app for image optimization:
- Drag-and-drop upload
- JPEG/PNG/WebP support (SVG/GIF rejected)
- Rate limiting 30 req/min per IP
- Max upload 25MB
- Auto-cleanup of temp files
- Before/After UI
- Uses Pillow (OpenCV available)

## SESSION 1: Core features
- Upload, validation, temp file handling
- Rate limiting with Flask-Limiter (in-memory store for dev)
- Error handlers for 413 and 429 responses

## SESSION 2: Advanced features
- Quality slider, resize by percent
- Sharpen/blur controls, contrast/brightness (added)
- Format conversion (JPEG/PNG/WebP)
- Live iterative optimization workflow: upload once, optimize multiple times
- Fix: convert float sharpen values to int percent for UnsharpMask
- UI improvements: responsive layout, presets, live preview

## Development notes
- Use python3 -m venv venv and source venv/bin/activate
- If port 5000 in use on macOS, run on a different port or disable AirPlay Receiver
- For production, configure Redis for rate limiter storage

## Next steps / Roadmap
- Add persistent rate-limiter storage (Redis)
- Add presets persistence and user accounts (requires DB)
- Add server-side caching for repeated identical optimizations
Keeping SESSION_NOTES.md concise and up to date helps you break long sessions into logical steps and prevents context loss.

Best practices for long interactive sessions

Avoid attempting an entire project or a major refactor in a single continuous interactive session. Long contexts can cause confusion and increase the risk of mistakes. Instead:
  • Break work into logical steps and commit frequently.
  • Maintain a session notes file and update it after each major change.
  • When context becomes noisy, compact or restart your session and rehydrate from SESSION_NOTES.md.
Practical tips:
  • Summarize progress before adding large new features.
  • Use session notes to provide a concise context snapshot for a fresh session.
  • Start a fresh session for major refactors or when debugging unexpected behavior.
  • Print tracebacks and return structured JSON error payloads to speed up iterative debugging.
  • For production, replace in-memory rate-limiter storage with Redis and run the app under Gunicorn inside Docker.


Summary

This lesson walked through building an image-optimization Flask app with practical choices for safe iteration:
  • Core features: MAX_CONTENT_LENGTH, allowed types, UUID filenames, temp file cleanup.
  • Rate limiting with Flask-Limiter (dev vs. production).
  • Image processing with Pillow (and keeping OpenCV available).
  • Debugging tips for 500 errors and port conflicts.
  • Session hygiene: SESSION_NOTES.md, breaking work into chunks, and restarting sessions when necessary.
Next steps: implement contrast/brightness refinements, persist presets, add production hardening (Redis-backed rate limiter, Docker + Gunicorn), and maintain session notes to keep long development efforts reproducible and maintainable.

Watch Video