diff --git a/aleksis/core/checks.py b/aleksis/core/checks.py
index e52290eb3a48ae4d7e7efe90fbec61d8f86ed365..19c8b77bab78db18441e71f8efe49bd797b1e9e2 100644
--- a/aleksis/core/checks.py
+++ b/aleksis/core/checks.py
@@ -22,7 +22,7 @@ def check_app_configs_base_class(
         if not isinstance(app_config, AppConfig):
             results.append(
                 Warning(
-                    "App config %s does not derive from aleksis.core.util.apps.AppConfig." % app_config.name,
+                    f"App config {app_config.name} does not derive from aleksis.core.util.apps.AppConfig.",
                     hint="Ensure the app uses the correct base class for all registry functionality to work.",
                     obj=app_config,
                     id="aleksis.core.W001",
@@ -48,7 +48,7 @@ def check_app_models_base_class(
             if ExtensibleModel not in model.__mro__ and PureDjangoModel not in model.__mro__:
                 results.append(
                     Warning(
-                        "Model %s in app config %s does not derive from aleksis.core.mixins.ExtensibleModel." % (model._meta.object_name, app_config.name),
+                        f"Model {model._meta.object_name} in app config {app_config.name} does not derive from aleksis.core.mixins.ExtensibleModel.",
                         hint="Ensure all models in AlekSIS use ExtensibleModel as base. If your deviation is intentional, you can add the PureDjangoModel mixin instead to silence this warning.",
                         obj=model,
                         id="aleksis.core.W002",
diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index 1695b7a42411327a2aaa7e46ddca6222084ef694..0063750f80162ecd310253f9b26ebf221c5218fa 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -134,11 +134,11 @@ class ExtensibleModel(CRUDMixin):
             if name.isidentifier():
                 prop_name = name
             else:
-                raise ValueError("%s is not a valid name." % name)
+                raise ValueError(f"{name} is not a valid name.")
 
         # Verify that attribute name does not clash with other names in the class
         if hasattr(cls, prop_name):
-            raise ValueError("%s already used." % prop_name)
+            raise ValueError(f"{prop_name} already used.")
 
         # Let Django's model magic add the attribute if we got here
         cls.add_to_class(name, obj)
@@ -172,7 +172,7 @@ class ExtensibleModel(CRUDMixin):
 
         # Force kwargs to be exactly one argument
         if len(kwargs) != 1:
-            raise TypeError("field() takes 1 keyword argument but %d were given" % len(kwargs))
+            raise TypeError(f"field() takes 1 keyword argument but {len(kwargs)} were given")
         name, field = kwargs.popitem()
 
         # Force the field to be one of the jsonstore fields
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 8f112c57bb3d0e43fec017735d1f21258baf929c..03e38d52a8a7f7b3c9a76533dc7e18a59010469d 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -275,7 +275,7 @@ class Group(ExtensibleModel):
         return list(self.members.all()) + list(self.owners.all())
 
     def __str__(self) -> str:
-        return "%s (%s)" % (self.name, self.short_name)
+        return f"{self.name} ({self.short_name})"
 
     def save(self, *args, **kwargs):
         super().save(*args, **kwargs)
@@ -593,7 +593,7 @@ class CustomMenuItem(ExtensibleModel):
     )
 
     def __str__(self):
-        return "[{}] {}".format(self.menu, self.name)
+        return f"[{self.menu}] {self.name}"
 
     class Meta:
         verbose_name = _("Custom menu item")
diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py
index 14d08defd986b115058b3c659c1feb9fcca82ac6..9cd0fc2fd9acb465fe259c9c5166bab57f117d13 100644
--- a/aleksis/core/settings.py
+++ b/aleksis/core/settings.py
@@ -227,10 +227,11 @@ if _settings.get("ldap.uri", None):
 
     # Discover flags by LDAP groups
     if _settings.get("ldap.groups.base", None):
+        group_type = _settings.get("ldap.groups.type", "groupOfNames")
         AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
             _settings.get("ldap.groups.base"),
             ldap.SCOPE_SUBTREE,
-            _settings.get("ldap.groups.filter", "(objectClass=%s)" % _settings.get("ldap.groups.type", "groupOfNames")),
+            _settings.get("ldap.groups.filter", f"(objectClass={group_type})"),
         )
 
         _group_type = _settings.get("ldap.groups.type", "groupOfNames").lower()
@@ -244,7 +245,7 @@ if _settings.get("ldap.uri", None):
         AUTH_LDAP_USER_FLAGS_BY_GROUP = {
         }
         for _flag in ["is_active", "is_staff", "is_superuser"]:
-            _dn = _settings.get("ldap.groups.flags.%s" % _flag, None)
+            _dn = _settings.get(f"ldap.groups.flags.{_flag}", None)
             if _dn:
                 AUTH_LDAP_USER_FLAGS_BY_GROUP[_flag] = _dn
 
diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py
index 8fa00fb040551bd415c73e03a86d1b353672967e..91e32af87edfbaa21335394c342ab69ee75395e7 100644
--- a/aleksis/core/urls.py
+++ b/aleksis/core/urls.py
@@ -85,7 +85,7 @@ for app_config in apps.app_configs.values():
         continue
 
     try:
-        urlpatterns.append(path("app/%s/" % app_config.label, include("%s.urls" % app_config.name)))
+        urlpatterns.append(path(f"app/{app_config.label}/", include(f"{app_config.name}.urls")))
     except ModuleNotFoundError:
         # Ignore exception as app just has no URLs
         pass  # noqa
diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py
index 7646acacf3ac2730bbe87000422dd2b225fe9b30..7063e378908d5092efd9bea259d64759483ccd1c 100644
--- a/aleksis/core/util/apps.py
+++ b/aleksis/core/util/apps.py
@@ -101,7 +101,8 @@ class AppConfig(django.apps.AppConfig):
                     licence_dict = default_dict
                 else:
                     # Add missing licence link to SPDX data
-                    licence_dict["url"] = "https://spdx.org/licenses/{}.html".format(licence_dict["licenseId"])
+                    licence_id = licence_dict["licenseId"]
+                    licence_dict["url"] = f"https://spdx.org/licenses/{licence_id}.html"
 
                 # Drop summed up flags to False if this licence is False
                 flags["isFsfLibre"] = flags["isFsfLibre"] and licence_dict["isFsfLibre"]
diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py
index 9c2119a83c71645ced8e67124e8e601dfb31213e..f39b58f6f801fd52babf39efb51117be62cf3999 100644
--- a/aleksis/core/util/core_helpers.py
+++ b/aleksis/core/util/core_helpers.py
@@ -54,7 +54,7 @@ def get_app_packages() -> Sequence[str]:
     except ImportError:
         return []
 
-    return ["aleksis.apps.%s" % pkg[1] for pkg in pkgutil.iter_modules(aleksis.apps.__path__)]
+    return [f"aleksis.apps.{pkg[1]}" for pkg in pkgutil.iter_modules(aleksis.apps.__path__)]
 
 
 def merge_app_settings(setting: str, original: Union[dict, list], deduplicate: bool = False) -> Union[dict, list]:
@@ -81,7 +81,7 @@ def merge_app_settings(setting: str, original: Union[dict, list], deduplicate: b
         for entry in app_setting:
             if entry in original:
                 if not deduplicate:
-                    raise AttributeError("%s already set in original." % entry)
+                    raise AttributeError(f"{entry} already set in original.")
             else:
                 if isinstance(original, list):
                     original.append(entry)
@@ -105,7 +105,7 @@ def lazy_preference(section: str, name: str) -> Callable[[str, str], Any]:
     """
 
     def _get_preference(section: str, name: str) -> Any:
-        return get_site_preferences()["%s__%s" % (section, name)]
+        return get_site_preferences()[f"{section}__{name}"]
 
     # The type is guessed from the default value to improve lazy()'s behaviour
     # FIXME Reintroduce the behaviour described above
@@ -169,7 +169,7 @@ def path_and_rename(instance, filename: str, upload_to: str = "files") -> str:
     _, ext = os.path.splitext(filename)
 
     # set filename as random string
-    new_filename = '{}.{}'.format(uuid4().hex, ext)
+    new_filename = f"{uuid4().hex}.{ext}"
 
     # Create upload directory if necessary
     os.makedirs(os.path.join(settings.MEDIA_ROOT, upload_to), exist_ok=True)
diff --git a/aleksis/core/util/predicates.py b/aleksis/core/util/predicates.py
index bdfa1c0cdede47436cb3b967be819cc2db0cbd4e..1ff0a91f5dfd160da8c28311d8fc43b2f5e6c1e2 100644
--- a/aleksis/core/util/predicates.py
+++ b/aleksis/core/util/predicates.py
@@ -34,7 +34,7 @@ def check_object_permission(user: User, perm: str, obj: Model) -> bool:
 def has_global_perm(perm: str):
     """ Builds predicate which checks whether a user has a global permission """
 
-    name = "has_global_perm:{}".format(perm)
+    name = f"has_global_perm:{perm}"
 
     @predicate(name)
     def fn(user: User) -> bool:
@@ -46,7 +46,7 @@ def has_global_perm(perm: str):
 def has_object_perm(perm: str):
     """ Builds predicate which checks whether a user has a permission on a object """
 
-    name = "has_global_perm:{}".format(perm)
+    name = f"has_global_perm:{perm}"
 
     @predicate(name)
     def fn(user: User, obj: Model) -> bool:
@@ -60,7 +60,7 @@ def has_object_perm(perm: str):
 def has_any_object(perm: str, klass):
     """ Build predicate which checks whether a user has access to objects with the provided permission """
 
-    name = "has_any_object:{}".format(perm)
+    name = f"has_any_object:{perm}"
 
     @predicate(name)
     def fn(user: User) -> bool:
diff --git a/aleksis/core/util/sass_helpers.py b/aleksis/core/util/sass_helpers.py
index 50e886f23ee5d8a116a3f043faf6e822722ce464..2dcbf0aa0314354c91b417b40a6e5606ab841197 100644
--- a/aleksis/core/util/sass_helpers.py
+++ b/aleksis/core/util/sass_helpers.py
@@ -22,7 +22,7 @@ def get_colour(html_colour: str) -> SassColor:
 def get_preference(section: str, name: str) -> str:
     """ Get a preference from dynamic-preferences """
 
-    return get_site_preferences()["%s__%s" % (section, name)]
+    return get_site_preferences()[f"{section}__{name}"]
 
 
 def clean_scss(*args, **kwargs) -> None: