From Flask to PyPlain: A Journey of Simplification

From Flask to PyPlain: A Journey of Simplification

Python Programming
Rajieb Rojarieo
Rajieb
January 6, 2026
362 views

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.

The Discovery

While working on PyPlain, a minimalist web framework, I realized something profound. Tectonix didn't need Flask's complexity. It needed:

  • Simple route handling
  • JSON request/response
  • CORS support
  • Path parameters

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?

The Analysis

Before diving in, I conducted a thorough analysis. The results were eye opening:

MetricFlask VersionPyPlain VersionChange
Dependencies9 packages7 packages-2 (22% reduction)
Framework Size~50,000 lines~450 lines99% smaller
Startup Time~0.5s~0.1s5x faster
Memory Footprint~45 MB~12 MB73% reduction
Lines of Code (main.py)308 lines280 lines9% reduction
External Web DependenciesFlask, Flask-CORSNone100% reduction

comparision_chart


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.

The Migration Strategy

I broke down the migration into clear phases:

Phase 1: Framework Replacement

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:

  • Path parameters: <fault_id>{fault_id}
  • Request access: fault_id parameter → req['path_params']['fault_id']
  • Response format: jsonify() → direct dict return
  • Error handling: (body, status)(status, body, content_type)

Phase 2: Request Handling

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.


Phase 3: CORS Configuration

Flask required Flask-CORS as a separate package. PyPlain has CORS built-in:

Flask:

python
from flask_cors import CORS

app = Flask(__name__)
CORS(app, origins=cors_origins, supports_credentials=True)

PyPlain:

python
server = PyPlainServer(
    cors_origins=cors_origins,
    enable_cors=True
)

One less dependency, same functionality.

api_health

The Implementation

With the strategy clear, I began the actual migration. The process was surprisingly smooth:

Step 1: Create New Structure

I created a new pyplain-tectonix directory and copied all the business logic:

pyplain-tectonix/
├── models/          # Pydantic models (unchanged)
├── core/            # Simulation engine (unchanged)
├── data/            # GeoJSON files (unchanged)
└── main.py          # Only file that changed

Step 2: Route-by-Route Migration

I migrated each endpoint systematically:

EndpointFlask LinesPyPlain LinesComplexity
GET /1815Simple
GET /api/health76Simple
GET /api/faults65Simple
GET /api/faults/{id}910Medium (path param)
GET /api/districts65Simple
GET /api/plates98Simple
POST /api/simulate1817Medium (JSON parsing)
GET /api/simulate/validate2625Medium (query params)
GET /api/stats3029Simple

Step 3: Testing Each Endpoint

As I migrated each route, I tested it immediately:

api_check

The Challenges

Not everything was smooth sailing. I encountered a few challenges:

Challenge 1: Path Parameter Syntax

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+)>')

Challenge 2: JSON Response Formatting

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.

Challenge 3: Error Handler Migration

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.

The Results

After completing the migration, I ran comprehensive tests. The results exceeded expectations:

Performance Comparison

OperationFlaskPyPlainImprovement
Cold Start0.52s0.11s4.7x faster
Request Latency (avg)12ms8ms33% faster
Memory (idle)45 MB12 MB73% less
Memory (under load)78 MB24 MB69% less
Concurrent Requests150 req/s180 req/s20% more

time_curl

Code Quality Metrics

MetricFlaskPyPlainChange
Cyclomatic Complexity8.26.1-26%
Lines per Function15.312.8-16%
Test Coverage85%85%Same
Maintainability Index7281+12%

radon_

Dependency Analysis

The dependency reduction was significant:

Before (Flask):

flask=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):

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
# PyPlain (no external dependencies)

The Business Logic: Untouched

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:

  • core/simulation.py - Unchanged (269 lines)
  • core/attenuation.py - Unchanged (471 lines)
  • models/*.py - Unchanged (all Pydantic models)
  • data/*.geojson - Unchanged (all data files)

This proved that the web framework was truly just a thin layer over the core functionality.

The Frontend: No Changes Needed

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.

frontend_works

Lessons Learned

This migration taught me several valuable lessons:

1. Simplicity Wins

Complex frameworks aren't always necessary. For API services, a minimal framework can be more appropriate, easier to understand, and better performing.

2. Business Logic Should Be Framework-Agnostic

By keeping all business logic separate from the web layer, the migration was straightforward. This is a best practice regardless of framework choice.

3. Explicit > Implicit

PyPlain's explicit request context (req['json']) is clearer than Flask's global request object. This makes code easier to test and reason about.

4. Fewer Dependencies = Fewer Problems

Removing Flask and Flask-CORS eliminated potential security vulnerabilities, version conflicts, and maintenance burden.

5. Performance Matters

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.

The Numbers Don't Lie

Let's look at the final comparison:

CategoryFlaskPyPlainWinner
Dependencies97PyPlain
Framework Size50K+ lines450 linesPyPlain
Startup Time0.52s0.11sPyPlain
Memory Usage45 MB12 MBPyPlain
Request Latency12ms8msPyPlain
Code ReadabilityGoodExcellentPyPlain
Learning CurveMediumLowPyPlain
Production ReadyYesYesTie

The Future

With PyPlain, Tectonix is now:

  • Lighter: 73% less memory usage
  • Faster: 4.7x faster startup, 33% lower latency
  • Simpler: 99% smaller framework, easier to understand
  • More Maintainable: Fewer dependencies, clearer code

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.

Conclusion

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:

  • Time Spent: ~6 hours
  • Files Changed: 1 (main.py)
  • Lines Modified: ~150
  • Business Logic Changed: 0
  • Tests Broken: 0
  • Frontend Changes: 0
  • Dependencies Removed: 2
  • Performance Improvement: 4.7x faster startup, 33% lower latency
  • Memory Reduction: 73%

pyplain_in_action


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.