---
# === IDENTITY ===
id: software/migrations/php-to-python/2026
canonical_question: "How do I migrate a PHP application to Python (Django/Flask)?"
aliases:
  - "PHP to Python migration guide"
  - "convert PHP to Django"
  - "replace PHP with Python"
  - "PHP to Flask migration"
  - "PHP Laravel to Django migration"
  - "modernize PHP app with Python"
  - "rewrite PHP in Django"
  - "PHP to FastAPI migration"
entity_type: software_reference
domain: software > migrations > php_to_python
region: global
jurisdiction: global
temporal_scope: 2018-2026

# === VERIFICATION ===
last_verified: 2026-05-29
confidence: 0.90
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "Django 6.0 (December 2025) — dropped Python 3.10/3.11, removed cx_Oracle, removed positional args to Model.save(), custom ORM expression params must be tuples not lists"
  next_review: 2026-11-25
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Never attempt a big-bang rewrite of a production PHP app — use the strangler fig pattern to migrate incrementally or risk project failure (60-70% of full rewrites are abandoned or severely delayed)"
  - "Set managed = False on all Django models generated via inspectdb until you are ready for Django to own the database schema — otherwise makemigrations will attempt to recreate or alter existing PHP tables"
  - "Both PHP and Python backends must share the same JWT secret during the transition period — mismatched secrets cause silent authentication failures across the proxy boundary"
  - "Django 5.2 LTS requires PostgreSQL 14+; if your PHP app runs MySQL, plan a database migration step or use django.db.backends.mysql"
  - "Django 6.0 (Dec 2025) dropped Python 3.10/3.11 (requires 3.12+) and removed cx_Oracle — for a migration target prefer the 5.2 LTS line (supported through April 2028) unless you control the Python runtime"
  - "Laravel bcrypt hashes use $2y$ prefix; Django defaults to PBKDF2 — add BCryptSHA256PasswordHasher to PASSWORD_HASHERS or all existing users will fail to authenticate"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "You want to migrate PHP to a JavaScript/TypeScript backend (Node.js, Express, NestJS)"
    use_instead: "software/migrations/php-to-nodejs/2026"
  - condition: "You want to migrate PHP to Go for high-performance microservices"
    use_instead: "software/migrations/php-to-go/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: php_framework
    question: "Which PHP framework is the source codebase built on?"
    type: choice
    options: ["Laravel", "Symfony", "CodeIgniter", "Vanilla PHP", "WordPress", "Other"]
  - key: target_framework
    question: "Which Python framework should the migration target?"
    type: choice
    options: ["Django", "Flask", "FastAPI"]
  - key: app_size
    question: "How large is the PHP codebase?"
    type: choice
    options: ["Small (<10K lines)", "Medium (10K-100K lines)", "Large (>100K lines)"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/php-to-python/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-29)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/migrations/php-to-nodejs/2026"
      label: "PHP to Node.js Migration Guide"
    - id: "software/migrations/php-to-go/2026"
      label: "PHP to Go Migration Guide"
    - id: "software/migrations/rails-to-django/2026"
      label: "Rails to Django Migration Guide"
  alternative_to:
    - id: "software/migrations/php-to-nodejs/2026"
      label: "PHP to Node.js (alternative target language)"
    - id: "software/migrations/php-to-go/2026"
      label: "PHP to Go (alternative target language)"
  often_confused_with:
    - id: "software/migrations/python2-to-python3/2026"
      label: "Python 2 to Python 3 Migration (different migration type — same target language, not a framework migration)"

# === SOURCES (9 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Django Documentation — Topics: Migrations"
    author: Django Software Foundation
    url: https://docs.djangoproject.com/en/5.2/topics/migrations/
    type: official_docs
    published: 2025-04-02
    reliability: authoritative
  - id: src2
    title: "Flask Documentation (3.1.x) — Tutorial"
    author: Pallets Projects
    url: https://flask.palletsprojects.com/en/stable/tutorial/
    type: official_docs
    published: 2024-11-13
    reliability: authoritative
  - id: src3
    title: "My Experience of Moving from Laravel to Django"
    author: Roman Sorin
    url: https://romansorin.com/blog/my-experience-of-moving-from-laravel-to-django
    type: technical_blog
    published: 2023-09-15
    reliability: moderate_high
  - id: src4
    title: "Django vs Laravel — A Comprehensive Comparison"
    author: Better Stack
    url: https://betterstack.com/community/guides/scaling-python/django-vs-laravel/
    type: technical_blog
    published: 2024-08-20
    reliability: high
  - id: src5
    title: "Transitioning from PHP to Python: A Comprehensive Guide for PHP Developers"
    author: Amir Kamizi
    url: https://amirkamizi.com/blog/python-for-php-developers
    type: technical_blog
    published: 2024-03-10
    reliability: moderate_high
  - id: src6
    title: "Strangler Fig Pattern: Migrating PHP Frameworks"
    author: Garry Sanders
    url: https://medium.com/@ganderseng/migrating-php-frameworks-d6bfafec6f89
    type: technical_blog
    published: 2024-05-12
    reliability: moderate_high
  - id: src7
    title: "Django 5.2 Release Notes"
    author: Django Software Foundation
    url: https://docs.djangoproject.com/en/6.0/releases/5.2/
    type: official_docs
    published: 2025-04-02
    reliability: authoritative
  - id: src8
    title: "FastAPI vs Flask vs Django: The 2025 AI Playbook"
    author: Codastra
    url: https://medium.com/@2nick2patel2/fastapi-vs-flask-vs-django-the-2025-ai-playbook-9f55f2a846f5
    type: technical_blog
    published: 2025-03-15
    reliability: moderate_high
  - id: src9
    title: "Django 6.0 Release Notes"
    author: Django Software Foundation
    url: https://docs.djangoproject.com/en/6.0/releases/6.0/
    type: official_docs
    published: 2025-12-03
    reliability: authoritative
---

# How to Migrate a PHP Application to Python (Django/Flask)

## How do I migrate a PHP application to Python (Django/Flask)?

## TL;DR

- **Bottom line**: Migrate incrementally using the strangler fig pattern -- run PHP and Python side-by-side behind a reverse proxy, converting one route/module at a time, starting with the API layer and working inward to models and templates. [src6]
- **Key tool/command**: `django-admin startproject myapp && python manage.py startapp core` (Django) or `pip install flask && flask run` (Flask) or `pip install fastapi uvicorn && uvicorn main:app` (FastAPI)
- **Watch out for**: Trying to do a big-bang rewrite -- the #1 cause of failed migrations. Always migrate incrementally. [src6]
- **Works with**: Django 5.2 LTS (Python 3.10-3.13, recommended for migrations) or Django 6.0 (Python 3.12-3.14), Flask 3.1.x, FastAPI 0.115+ (latest 0.136.x), PHP 8.1-8.5 as source [src9]

## Constraints

<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Never attempt a big-bang rewrite of a production PHP app -- use the strangler fig pattern to migrate incrementally, or risk project failure (60-70% of full rewrites are abandoned or severely delayed). [src6]
- Set `managed = False` on all Django models generated via `inspectdb` until you are ready for Django to own the database schema -- otherwise `makemigrations` will attempt to recreate or alter existing PHP tables. [src1]
- Both PHP and Python backends must share the same JWT secret during the transition period -- mismatched secrets cause silent authentication failures across the proxy boundary. [src4]
- Django 5.2 LTS requires PostgreSQL 14+; if your PHP app runs MySQL, plan a database migration step or use `django.db.backends.mysql`. [src7]
- Django 6.0 (Dec 2025) dropped Python 3.10/3.11 (requires 3.12+), removed `cx_Oracle`, and made custom ORM expression `as_sql()` params return tuples (not lists). For a migration target, prefer the 5.2 LTS line (supported through April 2028) unless you control the Python runtime. [src9]
- Laravel bcrypt hashes use `$2y$` prefix; Django defaults to PBKDF2 -- add `BCryptSHA256PasswordHasher` to `PASSWORD_HASHERS` or all existing users will fail to authenticate. [src4]

## Quick Reference

| PHP Pattern | Python/Django Equivalent | Example |
|---|---|---|
| `Route::get('/users', [UserController::class, 'index'])` | `path('users/', views.UserListView.as_view())` in `urls.py` | Django class-based view [src4] |
| `Route::get('/users', function() {...})` | `@app.route('/users')` decorator (Flask) / `@app.get('/users')` (FastAPI) | Flask/FastAPI route [src2, src8] |
| `$user = User::find($id)` (Eloquent) | `user = User.objects.get(id=id)` (Django ORM) | Single record fetch [src3, src4] |
| `User::where('active', true)->get()` | `User.objects.filter(active=True)` | Filtered queryset [src4] |
| `$user->posts()->create([...])` | `Post.objects.create(author=user, ...)` | Related object creation [src3] |
| `@extends('layouts.app')` (Blade) | `{% extends "layouts/base.html" %}` (Django/Jinja2) | Template inheritance [src4] |
| `{{ $variable }}` (Blade) | `{{ variable }}` (Django templates / Jinja2) | Variable output [src4] |
| `php artisan make:model User -m` | `python manage.py startapp users` + define model in `models.py` | Model + migration scaffold [src3] |
| `php artisan migrate` | `python manage.py makemigrations && python manage.py migrate` | Run database migrations [src1, src3] |
| `php artisan tinker` | `python manage.py shell` (auto-imports models in Django 5.2+) | Interactive REPL [src3, src7] |
| `composer require package/name` | `pip install package-name` or `poetry add package-name` or `uv add package-name` | Package management [src5] |
| `$_SESSION['key'] = 'value'` | `request.session['key'] = 'value'` (Django) / `session['key'] = 'value'` (Flask) | Session handling [src4] |
| `Auth::attempt($credentials)` | `authenticate(request, username=..., password=...)` (Django) | Authentication [src4] |
| `env('DB_HOST')` / `.env` | `os.environ.get('DB_HOST')` / `python-dotenv` or Django `settings.py` | Environment config [src5] |
| `Middleware` class in `app/Http/Middleware/` | `MIDDLEWARE` list in `settings.py` (Django) / `@app.before_request` (Flask) / middleware stack (FastAPI) | Middleware [src4, src8] |

## Decision Tree

> Full script: [decision-tree.txt](scripts/decision-tree.txt) (38 lines)

```
START: Should I migrate my PHP app to Python?
+-- Is your PHP app actively maintained and working well?
|   +-- YES -> Consider staying with PHP. Migration has real cost. Only proceed if
|   |          there's a strategic reason (team skills, ecosystem needs, ML/AI integration).
|   +-- NO v
+-- Do you need ML/AI, data science, or scientific computing?
|   +-- YES -> Migrate to Python. Python's ecosystem is unmatched here. [src8]
|   +-- NO v
+-- Is the codebase > 100K lines of PHP?
|   +-- YES -> Use Strangler Fig pattern. Run both side-by-side. -> See Step 4 [src6]
|   +-- NO v
+-- Do you need a full-featured admin, ORM, auth out of the box?
|   +-- YES -> Choose Django -> See Code Example 1 [src4]
|   +-- NO v
+-- Need a lightweight microservice or API?
|   +-- YES -> Choose Flask or FastAPI -> See Code Example 2 [src2, src8]
|   +-- NO v
+-- DEFAULT -> Django (batteries-included is safer for most migrations) [src3]

Framework choice:
+-- Need admin panel, user management, ORM, forms? -> Django [src4]
+-- Need maximum flexibility, minimal overhead? -> Flask [src2]
+-- Need async-first, high-performance API, auto-docs? -> FastAPI [src8]
+-- Migrating from Laravel specifically? -> Django (closest conceptual match) [src3]
```

## Decision Logic

Agent-ready if/then rules for picking the migration target and strategy. Each rule maps a concrete situation to a recommendation.

### If the codebase is a production app over 100K lines

--> Use the strangler fig pattern: run PHP and Python behind a reverse proxy and convert one route/module at a time; never attempt a big-bang rewrite (60-70% fail). [src6]

### If you need a batteries-included framework (admin, ORM, auth, forms)

--> Choose Django, and target Django 5.2 LTS (supported through April 2028) rather than 6.0 unless you fully control the Python 3.12+ runtime. [src4, src7, src9]

### If you need an async-first, API-only service with auto-generated OpenAPI docs

--> Choose FastAPI (0.115+, latest 0.136.x); it outperforms Flask/Django for I/O-bound APIs but ships no admin/ORM/templates. [src8]

### If you need a minimal, flexible microservice without Django's conventions

--> Choose Flask 3.1.x with Flask-SQLAlchemy and Flask-Migrate; add `flask-wtf` for CSRF. [src2]

### If your PHP app stores Laravel bcrypt ($2y$) password hashes

--> Add `BCryptSHA256PasswordHasher` to `PASSWORD_HASHERS` and install `bcrypt` before pointing Python at the users table, or every login fails. [src4]

### If the PHP app runs MySQL and you want Django on PostgreSQL

--> Keep Django on MySQL via `django.db.backends.mysql` during transition, or run a dedicated data-migration step first; do not switch the database engine and the framework in the same cutover. [src1, src7]

### If the PHP app is small, stable, well-maintained, and has no ML/AI need

--> Do not migrate; the maintenance cost is lower than the migration cost. Revisit only if team skills or ecosystem needs change. [src5, src6]

### 1. Audit the PHP codebase and map dependencies

Catalog every route, model, middleware, job, and third-party integration in your PHP app. Create a spreadsheet mapping each PHP component to its Python equivalent. Identify shared infrastructure (databases, caches, queues) that both systems must access during the transition. [src6]

```bash
# List all routes in Laravel
php artisan route:list --json > routes_inventory.json

# Count models, controllers, middleware
find app/Models -name "*.php" | wc -l
find app/Http/Controllers -name "*.php" | wc -l
find app/Http/Middleware -name "*.php" | wc -l

# List Composer dependencies and check for Python equivalents
composer show --format=json > php_dependencies.json

# Estimate lines of code
find app/ -name "*.php" -exec cat {} + | wc -l
```

**Verify**: Review `routes_inventory.json` -- every route must have a planned Python equivalent before proceeding.

### 2. Set up the Python project alongside PHP

Create the Django or Flask project in a separate directory. Both apps will run simultaneously, sharing the same database. Use a reverse proxy (Nginx, Caddy, or Cloudflare) to route traffic to the appropriate backend. [src4]

```bash
# Django setup (recommended for most migrations)
python -m venv venv
source venv/bin/activate  # Linux/macOS
# venv\Scripts\activate   # Windows
pip install django==5.2.* djangorestframework psycopg[binary] python-dotenv
django-admin startproject myproject .
python manage.py startapp core

# Flask setup (alternative — lightweight)
pip install flask flask-sqlalchemy flask-migrate python-dotenv
mkdir -p app/{models,routes,templates}

# FastAPI setup (alternative — async-first APIs)
pip install fastapi uvicorn[standard] sqlalchemy python-dotenv
mkdir -p app/{models,routes}
```

```python
# Django: myproject/settings.py — connect to existing PHP database
import os
from dotenv import load_dotenv
load_dotenv()

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST', 'localhost'),
        'PORT': os.environ.get('DB_PORT', '5432'),
    }
}
```

**Verify**: `python manage.py check` -> `System check identified no issues.`

### 3. Introspect and generate models from the existing database

Django can reverse-engineer your existing PHP database schema into Python models. This avoids rewriting schema definitions manually and ensures your Python app reads the same tables. [src1]

```bash
# Generate models from existing database
python manage.py inspectdb > core/models_generated.py

# Review and clean up the generated models, then move to models.py
# Key: set managed = False initially to prevent Django from altering tables
```

```python
# core/models.py — cleaned up from inspectdb output
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=255)
    email = models.EmailField(unique=True)
    password = models.CharField(max_length=255)  # Laravel bcrypt hash
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        db_table = 'users'      # Match existing PHP table name
        managed = False          # Don't let Django alter this table yet
```

**Verify**: `python manage.py shell -c "from core.models import User; print(User.objects.count())"` -- should return the same count as your PHP database.

### 4. Migrate routes incrementally using the strangler fig pattern

Start with low-traffic, read-only API endpoints. Configure your reverse proxy to route specific paths to the Python backend while everything else continues going to PHP. The rhythm is: proxy, extract, validate, repeat. [src6]

```nginx
# Nginx config: route /api/v2/* to Django, everything else to PHP
upstream php_backend {
    server 127.0.0.1:8080;
}
upstream django_backend {
    server 127.0.0.1:8000;
}

server {
    listen 80;

    # New Python endpoints
    location /api/v2/ {
        proxy_pass http://django_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # Everything else stays on PHP
    location / {
        proxy_pass http://php_backend;
        proxy_set_header Host $host;
    }
}
```

**Verify**: `curl -s http://localhost/api/v2/health | jq .` -> `{"status": "ok", "backend": "django"}`

### 5. Convert authentication and session handling

Authentication is the critical bridge between PHP and Python. Both systems must validate the same user sessions during the transition period. The simplest approach is token-based auth (JWT) shared between both backends. [src4]

```python
# Django: shared JWT authentication between PHP and Python
# pip install djangorestframework-simplejwt
from datetime import timedelta

# myproject/settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ],
}

SIMPLE_JWT = {
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': os.environ.get('JWT_SECRET'),  # Same secret as PHP
    'AUTH_HEADER_TYPES': ('Bearer',),
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
}

# Handle Laravel's bcrypt $2y$ password hashes
PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
]
```

```php
// PHP side: generate JWT tokens compatible with Django
// composer require firebase/php-jwt
use Firebase\JWT\JWT;

$payload = [
    'user_id' => $user->id,
    'exp' => time() + 3600,
    'iat' => time(),
];
$token = JWT::encode($payload, env('JWT_SECRET'), 'HS256');
```

**Verify**: Generate a token from PHP, validate it in Django: `python manage.py shell -c "from rest_framework_simplejwt.tokens import UntypedToken; UntypedToken('TOKEN_HERE')"`

### 6. Migrate templates and frontend layer

Convert Blade/Twig templates to Django templates or Jinja2. If you are building an API-first architecture, this step may be replaced by serving a JavaScript frontend (React, Vue) that calls your new Python API. [src3, src4]

```html
<!-- PHP Blade template -->
@extends('layouts.app')
@section('content')
    <h1>{{ $user->name }}</h1>
    @foreach($posts as $post)
        <article>{{ $post->title }}</article>
    @endforeach
@endsection

<!-- Django template equivalent -->
{% extends "layouts/base.html" %}
{% block content %}
    <h1>{{ user.name }}</h1>
    {% for post in posts %}
        <article>{{ post.title }}</article>
    {% endfor %}
{% endblock %}
```

**Verify**: Compare HTML output of both templates -- `diff <(curl -s php-app/users/1) <(curl -s django-app/users/1)` should show minimal structural differences.

### 7. Decommission PHP routes and clean up

Once all routes are migrated and tested, remove the reverse proxy split, point all traffic to Python, and retire the PHP codebase. Run a parallel period of at least 2-4 weeks with monitoring before full cutover. [src6]

```bash
# Run Django test suite
python manage.py test --verbosity=2

# Check for any remaining PHP route references
grep -r "php_backend" /etc/nginx/conf.d/

# Monitor error rates after cutover
# Set up alerting on 5xx responses for the first 2 weeks

# Verify all traffic goes through Python
python manage.py shell -c "from django.contrib.auth.models import User; print(f'Total users: {User.objects.count()}')"
```

**Verify**: `php artisan route:list` should be empty or the PHP app should be fully stopped. All traffic goes through Python.

## Code Examples

### Django: REST API with Model and Serializer

> Full script: [django-rest-api-with-model-and-serializer.py](scripts/django-rest-api-with-model-and-serializer.py) (54 lines)

```python
# Input:  HTTP GET /api/v2/products/?category=electronics&limit=10
# Output: JSON array of product objects with pagination
# core/models.py
from django.db import models
class Product(models.Model):
    name = models.CharField(max_length=255)
    slug = models.SlugField(unique=True)
    category = models.CharField(max_length=100, db_index=True)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    is_active = models.BooleanField(default=True)
    class Meta:
        db_table = 'products'   # Match existing PHP table
        ordering = ['-created_at']
# ... (see full script)
```

### Flask: Equivalent REST API

> Full script: [flask-equivalent-rest-api.py](scripts/flask-equivalent-rest-api.py) (50 lines)

```python
# Input:  HTTP GET /api/v2/products/?category=electronics&limit=10
# Output: JSON array of product objects with pagination
# app.py
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
import os

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
db = SQLAlchemy(app)
# ... (see full script)
```

### FastAPI: High-Performance Async API

```python
# Input:  HTTP GET /api/v2/products/?category=electronics&limit=10
# Output: JSON array of product objects with auto-generated OpenAPI docs
# main.py
from fastapi import FastAPI, Query
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from pydantic import BaseModel
from typing import Optional
import os

app = FastAPI(title="Migrated API", version="2.0")

class ProductResponse(BaseModel):
    id: int
    name: str
    slug: str
    category: str
    price: float
    is_active: bool

    class Config:
        from_attributes = True  # Replaces orm_mode in Pydantic v2

@app.get("/api/v2/products/", response_model=list[ProductResponse])
async def list_products(
    category: Optional[str] = None,
    limit: int = Query(default=20, le=100),
    page: int = Query(default=1, ge=1),
):
    """List products — auto-documented at /docs."""
    # SQLAlchemy async query against existing PHP database
    query = select(Product).where(Product.is_active == True)
    if category:
        query = query.where(Product.category == category)
    query = query.offset((page - 1) * limit).limit(limit)
    async with AsyncSession(engine) as session:
        result = await session.execute(query)
        return result.scalars().all()
```

### Database Migration Script: Verify Data Integrity

> Full script: [database-migration-script-verify-data-integrity.py](scripts/database-migration-script-verify-data-integrity.py) (33 lines)

```python
# Input:  Existing PHP/MySQL database and new Django/PostgreSQL setup
# Output: Comparison report of record counts and data integrity
# scripts/verify_migration.py
"""
Cross-database verification script.
# ... (see full script)
```

## Anti-Patterns

### Wrong: Big-bang rewrite (rewriting everything at once)

```python
# BAD — Attempting to rewrite the entire PHP app from scratch
# This is the "Second System Effect" — takes months/years, often fails

# "Let's just rewrite the whole thing in Django over the next 6 months"
# Meanwhile: PHP app gets zero maintenance, bugs pile up,
# business features are frozen, team morale drops

# Result: 60-70% of big-bang rewrites are abandoned or significantly
# delayed (industry experience) [src6]
```

### Correct: Strangler fig pattern (incremental migration)

```python
# GOOD — Migrate one module at a time, both systems run in parallel
# Start with the lowest-risk, highest-value endpoint

# Week 1-2: Migrate /api/v2/health and /api/v2/products (read-only)
# Week 3-4: Migrate /api/v2/users (read-only)
# Week 5-8: Migrate write operations for products
# Week 9-12: Migrate authentication
# ...continue until PHP serves zero routes [src6]
```

### Wrong: Direct SQL queries instead of ORM

```python
# BAD — Bringing PHP habits into Python
from django.db import connection

def get_active_users():
    with connection.cursor() as cursor:
        cursor.execute("SELECT * FROM users WHERE active = 1")
        return cursor.fetchall()  # Returns raw tuples, no model instances
```

### Correct: Use the Django ORM properly

```python
# GOOD — Use Django ORM querysets for type safety, SQL injection prevention,
# and lazy evaluation [src1]
from core.models import User

def get_active_users():
    return User.objects.filter(active=True).select_related('profile')
    # Returns model instances, supports chaining, auto-escapes values
```

### Wrong: Copying PHP's $GLOBALS / global state pattern

```python
# BAD — Using global variables like PHP superglobals
_current_user = None  # Module-level mutable state

def login(user_id):
    global _current_user
    _current_user = User.objects.get(id=user_id)

def get_dashboard():
    # Relies on global state — breaks in multi-threaded/async contexts
    return render(f"Welcome {_current_user.name}")
```

### Correct: Use request context and dependency injection

```python
# GOOD — Django/Flask handle request state through the request object [src2, src4]
# Django
from django.contrib.auth.decorators import login_required

@login_required
def get_dashboard(request):
    # request.user is thread-safe, scoped to this request
    return render(request, 'dashboard.html', {'user': request.user})

# Flask
from flask_login import login_required, current_user

@app.route('/dashboard')
@login_required
def get_dashboard():
    return render_template('dashboard.html', user=current_user)

# FastAPI
from fastapi import Depends
from .auth import get_current_user

@app.get('/dashboard')
async def get_dashboard(user: User = Depends(get_current_user)):
    return {"message": f"Welcome {user.name}"}
```

### Wrong: Mixing PHP naming conventions in Python code

```python
# BAD — Using camelCase and PHP naming patterns in Python
class UserController:  # Python uses "View" not "Controller" in Django
    def getUserById(self, userId):  # camelCase is not Pythonic
        $user = User.objects.get(id=userId)  # $ prefix doesn't exist in Python
        return $user
```

### Correct: Follow Python/Django conventions (PEP 8)

```python
# GOOD — snake_case, Django naming conventions [src3, src5]
class UserDetailView(generics.RetrieveAPIView):  # "View" in Django
    def get_queryset(self):  # snake_case methods
        user_id = self.kwargs['pk']  # No $ prefix
        return User.objects.filter(id=user_id)
```

### Wrong: Ignoring Python's package structure

```python
# BAD — Flat file structure like a PHP project
# myproject/
#   models.py          (all models in one file)
#   views.py           (all views in one file)
#   urls.py            (500+ routes in one file)
```

### Correct: Use Django apps for modular organization

```python
# GOOD — Django app-per-domain pattern [src1, src4]
# myproject/
#   users/
#     models.py, views.py, urls.py, serializers.py, tests.py
#   products/
#     models.py, views.py, urls.py, serializers.py, tests.py
#   orders/
#     models.py, views.py, urls.py, serializers.py, tests.py

# myproject/urls.py
from django.urls import path, include

urlpatterns = [
    path('api/v2/users/', include('users.urls')),
    path('api/v2/products/', include('products.urls')),
    path('api/v2/orders/', include('orders.urls')),
]
```

### Wrong: Not pinning dependencies during migration

```python
# BAD — Unpinned requirements allow breaking changes mid-migration
# requirements.txt
django
djangorestframework
psycopg2
```

### Correct: Pin all dependency versions

```python
# GOOD — Pinned requirements ensure reproducible builds [src5]
# requirements.txt
django==5.2.1
djangorestframework==3.15.2
psycopg[binary]==3.2.4
python-dotenv==1.0.1

# Or better: use pyproject.toml with uv/poetry for lockfile support
```

## Common Pitfalls

- **PHP arrays are not Python lists**: PHP arrays serve as both indexed arrays and associative arrays (dicts). In Python, use `list` for ordered sequences and `dict` for key-value pairs. Mixing them up causes `TypeError` exceptions. Fix: audit every `$array` usage and decide if it maps to `list` or `dict`. [src5]
- **Forgetting `managed = False` on introspected models**: When using `inspectdb` to generate models from an existing database, Django defaults to `managed = True`, which means `makemigrations` will try to recreate or alter existing tables. Fix: set `managed = False` in the model's `Meta` class until you are ready to let Django own the schema. [src1]
- **Password hash incompatibility**: Laravel uses bcrypt with a `$2y$` prefix while Django uses PBKDF2 by default. If you point Django at an existing users table, authentication will fail. Fix: add `'django.contrib.auth.hashers.BCryptSHA256PasswordHasher'` to `PASSWORD_HASHERS` in `settings.py` and install `bcrypt`. [src4]
- **Session format mismatch during transition**: PHP serializes sessions differently from Django. Users will be logged out when traffic switches between backends. Fix: use token-based auth (JWT) shared between both backends during the transition, or implement a shared session store in Redis. [src4, src6]
- **NULL vs None semantics**: PHP's `null`, `""`, `0`, `false`, and `[]` are all falsy. Python's `None` is distinct from `""`, `0`, `False`, and `[]`. Porting `if (!$value)` to `if not value` may produce different behavior. Fix: be explicit -- use `if value is None` when checking for null specifically. [src5]
- **Missing CSRF protection**: Laravel includes CSRF tokens automatically via `@csrf` in Blade. Django also enforces CSRF but requires `{% csrf_token %}` in templates and proper middleware configuration. Flask requires `flask-wtf` for CSRF. Forgetting this causes 403 errors on all POST requests. Fix: ensure `django.middleware.csrf.CsrfViewMiddleware` is in `MIDDLEWARE` and templates include `{% csrf_token %}`. [src1, src2]
- **Timezone handling differences**: PHP uses `date_default_timezone_set()` globally. Django has `USE_TZ = True` in settings and stores everything in UTC by default, converting on display. Migrating datetime columns without timezone awareness leads to shifted timestamps. Fix: set `USE_TZ = True`, ensure database columns are timezone-aware, and audit all date comparisons. [src1]
- **Deployment model mismatch**: PHP runs on cheap shared hosting with Apache/mod_php. Python requires WSGI/ASGI servers (Gunicorn, Uvicorn), process managers, and typically container-based deployment. Fix: budget for infrastructure changes and consider containerization (Docker) early in the migration planning. [src8]

## Diagnostic Commands

```bash
# Check Django project configuration for production readiness
python manage.py check --deploy

# Verify database connectivity
python manage.py dbshell

# Introspect existing PHP database schema into Django models
python manage.py inspectdb > models_generated.py

# Compare model definitions vs actual database
python manage.py showmigrations

# Run Django development server
python manage.py runserver 0.0.0.0:8000

# List all registered URL routes in Django (requires django-extensions)
python manage.py show_urls

# Check Flask routes
flask routes

# Verify Python/pip environment
python --version && pip list --format=columns

# Test database connection from Python
python -c "import psycopg; conn = psycopg.connect('dbname=mydb user=myuser'); print('Connected'); conn.close()"

# Compare record counts between PHP and Python databases
php artisan tinker --execute="echo App\Models\User::count();"
python manage.py shell -c "from core.models import User; print(User.objects.count())"

# Verify bcrypt password compatibility
python -c "import bcrypt; print(bcrypt.checkpw(b'testpassword', b'\$2y\$10\$HASH_FROM_PHP'))"

# Check Django 5.2 compatibility with current database
python manage.py migrate --check
```

## Version History & Compatibility

| Version | Status | Key Features | Migration Notes |
|---|---|---|---|
| Django 6.0 | Current (Dec 2025) | Built-in CSP, template partials, background tasks framework, AsyncPaginator, cross-DB StringAgg | Requires Python 3.12-3.14; dropped Python 3.10/3.11; removed cx_Oracle; custom expression params must be tuples; not yet an LTS [src9] |
| Django 5.2 LTS | Current LTS (April 2025) | Composite PKs, generated fields, auto-import in shell, facet filters | Recommended migration target; LTS support through April 2028; supports Python 3.10-3.13; requires PostgreSQL 14+ [src7] |
| Django 5.1 | EOL Dec 2025 | LoginRequiredMiddleware, db_default | End-of-life; jump to 5.2 LTS or 6.0 |
| Django 4.2 LTS | EOL April 2026 | Psycopg 3 support, comments on columns | End-of-life approaching; migrate to 5.2 LTS [src7] |
| Flask 3.1.x | Current (Nov 2024) | Partitioned cookies, per-request max_content_length, Python 3.9+ required | Stable; latest patch 3.1.3 [src2] |
| Flask 2.x | EOL | Async support added, nested blueprints | No longer maintained; upgrade to 3.1.x |
| FastAPI 0.115+ | Current (latest 0.136.x, Apr 2026) | Python 3.12 baseline, Pydantic v2, async-first | Rapidly growing; best for API-only migrations [src8] |
| PHP 8.5 | Current source (Nov 2025) | Pipe operator `\|>`, clone-with, array_first()/array_last(), URI/Lexbor libs | Newest migration source; map pipe chains and property hooks to Python expressions |
| PHP 8.4 | Stable source (Nov 2024) | Property hooks, asymmetric visibility, new HTML5 DOM API, array_find() | Map property hooks to Python @property; asymmetric visibility has no direct Python equivalent |
| PHP 8.3 | Maintenance source | Typed class constants, json_validate(), readonly amendments | Map typed properties to Django model fields |
| PHP 8.1 | EOL (security-only ended) | Enums, fibers, readonly properties | EOL — map PHP enums to Django TextChoices/IntegerChoices |
| Laravel 11.x | Current source | Simplified structure, per-second scheduling | Map artisan commands to manage.py equivalents [src3] |
| Laravel 10.x | LTS source | Process interaction, pest testing | Most common migration source |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Team expertise is shifting to Python | PHP app is small, stable, and well-maintained | Keep PHP; maintenance < migration cost |
| You need ML/AI/data science integration (PyTorch, scikit-learn, pandas) | You only need a simple CMS or blog | WordPress or Laravel with PHP -- mature ecosystems |
| PHP codebase is unmaintainable (no tests, no docs, no team knowledge) | Timeline is less than 3 months for a large app | Incremental refactoring within PHP first |
| You want Django's built-in admin, ORM, auth, or FastAPI's auto-docs | Your team has zero Python experience | Invest in PHP modernization (Laravel upgrade) or train team first |
| Building microservices / API-first architecture | Legacy PHP app has heavy WordPress/Drupal plugin dependencies | Stay on PHP; plugin ecosystem has no Python equivalent |
| Need async capabilities (Django 5.x async views, FastAPI, ASGI) | Database schema is tightly coupled to PHP-specific tooling (MySQL stored procs) | Decouple database layer first, then consider migration |
| Multiple services need to share ML models or data pipelines | PHP app uses extensive Composer packages with no Python equivalents | Evaluate bridge solutions (API gateway) or stay on PHP |

## Important Caveats

- Django and Laravel are conceptually similar (both batteries-included MVC frameworks), but the terminology is inverted: Laravel's "Controller" is Django's "View", and Laravel's "View" (Blade) is Django's "Template". This causes significant confusion during migration. [src3]
- Automated code translation tools (like Ispirer or AI-based converters) can handle syntax conversion but cannot replicate framework-specific patterns, middleware chains, or business logic. Budget 30-40% of migration time for manual review and refactoring.
- PHP's loose typing means many implicit type coercions happen silently. Python is strongly typed at runtime -- migrated code will throw `TypeError` exceptions where PHP silently coerced. Add type hints and run `mypy` early in the migration. [src5]
- Database migration is often the hardest part. If your PHP app uses MySQL-specific features (e.g., `ENUM` columns, spatial queries, full-text indexes), verify Django/PostgreSQL equivalents exist before committing to the migration. [src1]
- Hosting costs and deployment patterns change significantly. PHP runs on cheap shared hosting with Apache/mod_php. Python requires WSGI/ASGI servers (Gunicorn, Uvicorn), process managers, and typically a container-based deployment. Budget for infrastructure changes.
- FastAPI is gaining significant traction (20% adoption in 2025 surveys) and is the strongest option for async API-only migrations. However, it lacks Django's built-in admin, ORM, and template system -- evaluate your needs carefully. [src8]
- Django 5.2 LTS composite primary key support cannot be retrofitted to existing models with single PKs -- plan table structures before migration if you need composite keys. [src7]
- Django 6.0 (Dec 2025) is the newest line but is not an LTS. It drops Python 3.10/3.11, removes `cx_Oracle`, requires custom ORM expression `as_sql()` methods to return params as tuples (not lists), and makes `DEFAULT_AUTO_FIELD` default to `BigAutoField`. For a migration where you want a long, stable target, pick Django 5.2 LTS; choose 6.0 only if you want its built-in CSP, template partials, or the new background-tasks framework and control the Python 3.12+ runtime. [src9]

## Related Units

- [PHP to Node.js Migration Guide](/software/migrations/php-to-nodejs/2026)
- [PHP to Go Migration Guide](/software/migrations/php-to-go/2026)
- [Rails to Django Migration Guide](/software/migrations/rails-to-django/2026)
- [Python 2 to Python 3 Migration Guide](/software/migrations/python2-to-python3/2026)
