apache/superset · error · SystemThemeProtectedError

Cannot modify system themes.

Error message

Cannot modify system themes.

What it means

SystemThemeProtectedError raised by DeleteThemesCommand.validate when any theme in the bulk payload has is_system True. System themes (the built-in light/dark defaults shipped with Superset) are protected rows that can never be deleted, regardless of admin status — this is a data-integrity guard, not a permission check.

Source

Thrown at superset/commands/theme/delete.py:61

    def run(self) -> None:
        self.validate()
        assert self._models

        # Dissociate dashboards from themes before deleting
        self._dissociate_dashboards()

        ThemeDAO.delete(self._models)

    def validate(self) -> None:
        # Validate/populate model exists
        self._models = ThemeDAO.find_by_ids(self._model_ids)
        if not self._models or len(self._models) != len(self._model_ids):
            raise ThemeNotFoundError()

        # Check if any of the themes are system themes
        for theme in self._models:
            if theme.is_system:
                raise SystemThemeProtectedError()
            # Check if theme is in use as system default or dark
            if theme.is_system_default or theme.is_system_dark:
                raise SystemThemeInUseError()

        # Check for dashboard usage
        self._dashboard_usage = self._get_dashboard_usage()

    def _dissociate_dashboards(self) -> None:
        """Dissociate dashboards from themes before deletion."""
        from superset.models.dashboard import Dashboard

        theme_ids = [theme.id for theme in self._models or []]
        if not theme_ids:
            return

        # Get count of affected dashboards for logging
        affected_count = (
            db.session.query(Dashboard)

View on GitHub (pinned to f4587218dd)

Solutions

  1. Filter is_system themes out of the delete payload — they are permanent.
  2. Adjust UI/API callers to disable deletion affordances on system themes.
  3. To change built-in look and feel, override the system default/dark selection instead of deleting the theme.

Example fix

# before
DeleteThemesCommand([theme.id for theme in all_themes]).run()
# after
DeleteThemesCommand([t.id for t in all_themes if not t.is_system]).run()
Defensive patterns

Strategy: type-guard

Validate before calling

deletable = [t.id for t in themes if not t.is_system]
if len(deletable) != len(themes):
    warn_user("system themes cannot be deleted")

Type guard

def is_deletable_theme(theme) -> bool:
    return not theme.is_system and not theme.is_system_default and not theme.is_system_dark

Try / catch

try:
    DeleteThemesCommand(ids).run()
except (SystemThemeProtectedError, SystemThemeInUseError):
    ids = [i for i in ids if i not in blocked_theme_ids()]
    retry_with(ids)

Prevention

When it happens

Trigger: Bulk delete including a theme flagged is_system — typically the seeded default themes created at install/upgrade time.

Common situations: 'Select all and delete' in a theme management UI that includes built-in themes; attempting to clean up all themes before importing a fresh set; scripts that assume every theme is user-created.

Related errors


AI-assisted analysis of apache/superset@f4587218dd (2026-08-14). Data as JSON: /api/errors/71fe1639f14f1207. Report an issue: GitHub.