# SM016: run_python_no_reverse

| Property       | Value                 |
| -------------- | --------------------- |
| **Rule ID**    | SM016                 |
| **Severity**   | INFO                  |
| **Name**       | run_python_no_reverse |
| **Applies to** | All databases         |

## What Triggers This Rule

This rule triggers when a migration contains `RunPython` without reverse code:

```python
# Triggers SM016
def forward_func(apps, schema_editor):
    User = apps.get_model('myapp', 'User')
    User.objects.filter(status='').update(status='active')

migrations.RunPython(forward_func)  # No reverse_code provided
```

## Why It's Flagged

Without reverse code, the migration cannot be rolled back:

1. **`migrate --reverse` fails** — Django can't undo the migration
2. **Testing difficulties** — Can't reset database state
3. **Emergency rollback blocked** — If deployment fails, can't revert
4. **CI/CD complications** — Some workflows require reversible migrations

### Related to SM007

This rule is similar to [SM007 (run_sql_no_reverse)](SM007.md) but for Python code instead of SQL.

## When It's Safe to Ignore

This rule can be safely ignored when:

1. **The operation is truly irreversible** — Some operations can't be undone
2. **Reverse would be destructive** — Undoing would cause data loss
3. **Using forward-only deployments** — Never roll back migrations
4. **The operation is idempotent** — Running forward again achieves same result

## How to Fix It

### Provide Reverse Code

```python
def forward_func(apps, schema_editor):
    User = apps.get_model('myapp', 'User')
    User.objects.filter(status='').update(status='active')

def reverse_func(apps, schema_editor):
    User = apps.get_model('myapp', 'User')
    User.objects.filter(status='active').update(status='')

migrations.RunPython(forward_func, reverse_func)
```

### Use noop for Non-Reversible Operations

If the operation truly can't be reversed, explicitly mark it:

```python
def populate_data(apps, schema_editor):
    """Populate initial data - cannot be reversed without data loss."""
    Category = apps.get_model('shop', 'Category')
    Category.objects.bulk_create([
        Category(name='Electronics'),
        Category(name='Books'),
        Category(name='Clothing'),
    ])

# Explicitly mark as non-reversible
migrations.RunPython(populate_data, migrations.RunPython.noop)
```

### Common Patterns

**Backfilling a new column:**

```python
def forward(apps, schema_editor):
    User = apps.get_model('myapp', 'User')
    User.objects.filter(full_name__isnull=True).update(
        full_name=Concat('first_name', Value(' '), 'last_name')
    )

def reverse(apps, schema_editor):
    # Can't truly reverse - data was computed
    # But we can clear it
    User = apps.get_model('myapp', 'User')
    User.objects.all().update(full_name=None)

migrations.RunPython(forward, reverse)
```

**Creating initial data:**

```python
def create_defaults(apps, schema_editor):
    Setting = apps.get_model('config', 'Setting')
    Setting.objects.get_or_create(key='site_name', defaults={'value': 'My Site'})

def remove_defaults(apps, schema_editor):
    Setting = apps.get_model('config', 'Setting')
    Setting.objects.filter(key='site_name').delete()

migrations.RunPython(create_defaults, remove_defaults)
```

**Migrating data between columns:**

```python
def forward(apps, schema_editor):
    Order = apps.get_model('shop', 'Order')
    Order.objects.all().update(new_status=F('old_status'))

def reverse(apps, schema_editor):
    Order = apps.get_model('shop', 'Order')
    Order.objects.all().update(old_status=F('new_status'))

migrations.RunPython(forward, reverse)
```

## Best Practice: Document Why No Reverse

When using `noop`, add a comment explaining why:

```python
def encrypt_passwords(apps, schema_editor):
    """
    Encrypt all plaintext passwords using bcrypt.
    This is a one-way operation - cannot be reversed without
    storing original passwords, which would be a security risk.
    """
    User = apps.get_model('myapp', 'User')
    for user in User.objects.filter(password_encrypted=False):
        user.password = bcrypt.hashpw(user.password.encode(), bcrypt.gensalt())
        user.password_encrypted = True
        user.save()

# Cannot reverse: original passwords not stored
migrations.RunPython(encrypt_passwords, migrations.RunPython.noop)
```

## How to Suppress

### In Django Settings

```python
SAFE_MIGRATIONS = {
    "DISABLED_RULES": ["SM016"],
}
```

## See Also

- [SM007: run_sql_no_reverse](SM007.md) — Similar rule for RunSQL
- [SM008: data_migration](SM008.md) — General data migration rule
- [Safe Patterns: RunPython Best Practices](../patterns.md#runpython-best-practices)
