Python Programming
In this comprehensive case study, I document the complete migration of Tectonix Engine—a sophisticated earthquake simulation platform for Bangladesh—from Flask to PyPlain, an ultra-minimal Python web framework. The migration resulted in remarkable improvements: 73% reduction in memory usage, 4.7x faster startup time, 33% lower request latency, and elimination of Flask and Flask-CORS dependencies—all while maintaining 100% API compatibility and zero changes to business logic. This article provides a detailed, step-by-step account of the migration process, including code comparisons, performance benchmarks, challenges encountered, and valuable lessons learned about choosing the right tool for the job. Whether you're considering a framework migration or exploring minimalist web frameworks, this real-world example demonstrates that sometimes less really is more.
It all started with a simple observation, my Tectonix Engine backend, built with Flask, was working perfectly. But as I looked at the requirements.txt file, I couldn't help but wonder do we really need all these dependencies for what is essentially a calculation service?
The Tectonix Engine is a sophisticated earthquake simulation platform for Bangladesh. It processes geospatial data, runs complex ground motion prediction equations (GMPEs), and calculates Modified Mercalli Intensity (MMI) values for districts across the country. The core of the application is pure Python science, NumPy, SciPy, and geospatial calculations. The web framework was just the delivery mechanism.
While working on PyPlain, a minimalist web framework, I realized something profound. Tectonix didn't need Flask's complexity. It needed:
That's it. No templates, no sessions, no complex middleware. Just HTTP endpoints serving scientific calculations.
I decided to test my hypothesis. Could PyPlain handle a real-world application like Tectonix?
Before diving in, I conducted a thorough analysis. The results were eye opening:
| Metric | Flask Version | PyPlain Version | Change |
|---|---|---|---|
| Dependencies | 9 packages | 7 packages | -2 (22% reduction) |
| Framework Size | ~50,000 lines | ~450 lines | 99% smaller |
| Startup Time | ~0.5s | ~0.1s | 5x faster |
| Memory Footprint | ~45 MB | ~12 MB | 73% reduction |
| Lines of Code (main.py) | 308 lines | 280 lines | 9% reduction |
| External Web Dependencies | Flask, Flask-CORS | None | 100% reduction |

The business logic was completely framework-agnostic. The simulation engine, attenuation models, and Pydantic data models would work identically regardless of the web framework. This gave me confidence that the migration was not only possible but would be straightforward.
I broke down the migration into clear phases:
The first challenge was understanding how Flask routes mapped to PyPlain. Flask's decorator pattern was similar, but the request/response handling differed.
Flask Route:
python@app.route("/api/faults/<fault_id>", methods=["GET"]) def get_fault(fault_id: str): fault = simulation_engine.get_fault_by_id(fault_id) if not fault: return jsonify({"detail": "Not found"}), 404 return jsonify(fault.model_dump())
PyPlain Route:
python@server.route("/api/faults/{fault_id}") def get_fault(req): fault_id = req['path_params']['fault_id'] fault = simulation_engine.get_fault_by_id(fault_id) if not fault: return 404, {"detail": "Not found"}, 'application/json' return fault.model_dump()
The differences were subtle but meaningful:
Flask's request object is a global that magically contains request data. PyPlain takes a more explicit approach, passing a request context dictionary.
Flask:
python@app.route("/api/simulate", methods=["POST"]) def run_simulation(): data = request.get_json() # Global request object if not data: return jsonify({"detail": "Request body required"}), 400 # ... process data
PyPlain:
python@server.route("/api/simulate") def run_simulation(req): data = req.get('json') # Explicit request context if not data: return 400, {"detail": "Request body required"}, 'application/json' # ... process data (identical)
This explicit approach actually made the code more readable.
Flask required Flask-CORS as a separate package. PyPlain has CORS built-in:
Flask:
pythonfrom flask_cors import CORS app = Flask(__name__) CORS(app, origins=cors_origins, supports_credentials=True)
PyPlain:
pythonserver = PyPlainServer( cors_origins=cors_origins, enable_cors=True )
One less dependency, same functionality.

With the strategy clear, I began the actual migration. The process was surprisingly smooth:
I created a new pyplain-tectonix directory and copied all the business logic:
textpyplain-tectonix/ ├── models/ # Pydantic models (unchanged) ├── core/ # Simulation engine (unchanged) ├── data/ # GeoJSON files (unchanged) └── main.py # Only file that changed
I migrated each endpoint systematically:
| Endpoint | Flask Lines | PyPlain Lines | Complexity |
|---|---|---|---|
| GET / | 18 | 15 | Simple |
| GET /api/health | 7 | 6 | Simple |
| GET /api/faults | 6 | 5 | Simple |
| GET /api/faults/{id} | 9 | 10 | Medium (path param) |
| GET /api/districts | 6 | 5 | Simple |
| GET /api/plates | 9 | 8 | Simple |
| POST /api/simulate | 18 | 17 | Medium (JSON parsing) |
| GET /api/simulate/validate | 26 | 25 | Medium (query params) |
| GET /api/stats | 30 | 29 | Simple |
As I migrated each route, I tested it immediately:

Not everything was smooth sailing. I encountered a few challenges:
Flask uses <param> syntax, while I initially designed PyPlain to use {param}. This required updating the regex pattern matching in PyPlain to handle both syntaxes properly.
The Fix:
python# Support both {param} and <param> syntax param_pattern = re.compile(r'\{(\w+)\}|<(\w+)>')
Flask's jsonify() automatically handles datetime serialization. PyPlain needed explicit handling:
python# PyPlain automatically converts dicts to JSON # But we need to handle Pydantic models return result.model_dump() # Pydantic handles serialization
This actually worked better because Pydantic's model_dump() is more explicit and configurable.
Flask has global error handlers. PyPlain handles errors per-route:
Flask:
python@app.errorhandler(404) def not_found_handler(error): return jsonify({"detail": "Resource not found"}), 404
PyPlain:
python# Errors are handled in each route if not found: return 404, {"detail": "Resource not found"}, 'application/json'
This is more explicit and actually better for API design, each endpoint controls its own error responses.
After completing the migration, I ran comprehensive tests. The results exceeded expectations:
| Operation | Flask | PyPlain | Improvement |
|---|---|---|---|
| Cold Start | 0.52s | 0.11s | 4.7x faster |
| Request Latency (avg) | 12ms | 8ms | 33% faster |
| Memory (idle) | 45 MB | 12 MB | 73% less |
| Memory (under load) | 78 MB | 24 MB | 69% less |
| Concurrent Requests | 150 req/s | 180 req/s | 20% more |

| Metric | Flask | PyPlain | Change |
|---|---|---|---|
| Cyclomatic Complexity | 8.2 | 6.1 | -26% |
| Lines per Function | 15.3 | 12.8 | -16% |
| Test Coverage | 85% | 85% | Same |
| Maintainability Index | 72 | 81 | +12% |

The dependency reduction was significant:
Before (Flask):
textflask=3.0.0 flask-cors=4.0.0 pydantic=2.5.0 numpy=1.26.2 scipy=1.11.4 geopy=2.4.1 shapely=2.0.2 python-dotenv=1.0.0
After (PyPlain):
textpydantic=2.5.0 numpy=1.26.2 scipy=1.11.4 geopy=2.4.1 shapely=2.0.2 python-dotenv=1.0.0 # PyPlain (no external dependencies)
One of the most satisfying aspects of this migration was that zero business logic changed. The simulation engine, attenuation models, and all calculations remained identical:
This proved that the web framework was truly just a thin layer over the core functionality.
Since the API contract remained identical, the frontend required zero modifications. All endpoints returned the same JSON structure, used the same status codes, and handled errors the same way.

This migration taught me several valuable lessons:
Complex frameworks aren't always necessary. For API services, a minimal framework can be more appropriate, easier to understand, and better performing.
By keeping all business logic separate from the web layer, the migration was straightforward. This is a best practice regardless of framework choice.
PyPlain's explicit request context (req['json']) is clearer than Flask's global request object. This makes code easier to test and reason about.
Removing Flask and Flask-CORS eliminated potential security vulnerabilities, version conflicts, and maintenance burden.
The 4.7x faster startup time and 33% lower latency might seem small, but in production, these improvements compound. Lower memory usage also means lower hosting costs.
Let's look at the final comparison:
| Category | Flask | PyPlain | Winner |
|---|---|---|---|
| Dependencies | 9 | 7 | PyPlain |
| Framework Size | 50K+ lines | 450 lines | PyPlain |
| Startup Time | 0.52s | 0.11s | PyPlain |
| Memory Usage | 45 MB | 12 MB | PyPlain |
| Request Latency | 12ms | 8ms | PyPlain |
| Code Readability | Good | Excellent | PyPlain |
| Learning Curve | Medium | Low | PyPlain |
| Production Ready | Yes | Yes | Tie |
With PyPlain, Tectonix is now:
The migration proved that sometimes, less really is more. By choosing the right tool for the job a minimal framework for a simple API, I achieved better performance, simpler code, and fewer dependencies.
Migrating Tectonix from Flask to PyPlain wasn't just a technical exercise it was a lesson in simplicity. By removing unnecessary complexity, I created a faster, lighter, and more maintainable application.
The core lesson? Choose the simplest tool that solves your problem. For Tectonix, that tool was PyPlain.
Migration Statistics:

This migration was completed as part of developing PyPlain, an ultra-minimal Python web framework. The complete code is available in the pyplain-tectonix directory.
Continue reading