diff --git a/pytools/__init__.py b/pytools/__init__.py
index ae06dd171664fe46dc2652223ee96942c4e25a81..c8cfa90b96583a1a59475ce6d752858f4cc5ab20 100644
--- a/pytools/__init__.py
+++ b/pytools/__init__.py
@@ -680,8 +680,9 @@ def memoize_on_first_arg(function, cache_dict_name=None):
     """
 
     if cache_dict_name is None:
-        cache_dict_name = intern("_memoize_dic_"
-                + function.__module__ + function.__name__)
+        cache_dict_name = intern(
+                f"_memoize_dic_{function.__module__}{function.__name__}"
+                )
 
     def wrapper(obj, *args, **kwargs):
         if kwargs:
@@ -693,7 +694,7 @@ def memoize_on_first_arg(function, cache_dict_name=None):
             return getattr(obj, cache_dict_name)[key]
         except AttributeError:
             result = function(obj, *args, **kwargs)
-            setattr(obj, cache_dict_name, {key: result})
+            object.__setattr__(obj, cache_dict_name, {key: result})
             return result
         except KeyError:
             result = function(obj, *args, **kwargs)
@@ -701,7 +702,7 @@ def memoize_on_first_arg(function, cache_dict_name=None):
             return result
 
     def clear_cache(obj):
-        delattr(obj, cache_dict_name)
+        object.__delattr__(obj, cache_dict_name)
 
     from functools import update_wrapper
     new_wrapper = update_wrapper(wrapper, function)
@@ -712,9 +713,15 @@ def memoize_on_first_arg(function, cache_dict_name=None):
 
 def memoize_method(method: F) -> F:
     """Supports cache deletion via ``method_name.clear_cache(self)``.
+
+    .. versionchanged:: 2021.2
+
+        Can memoize methods on classes that do not allow setting attributes
+        (e.g. by overwritting ``__setattr__``).
     """
 
-    return memoize_on_first_arg(method, intern("_memoize_dic_"+method.__name__))
+    return memoize_on_first_arg(method,
+            intern(f"_memoize_dic_{method.__name__}"))
 
 
 class keyed_memoize_on_first_arg:  # noqa: N801
@@ -734,8 +741,7 @@ class keyed_memoize_on_first_arg:  # noqa: N801
         self.cache_dict_name = cache_dict_name
 
     def _default_cache_dict_name(self, function):
-        return intern("_memoize_dic_"
-                + function.__module__ + function.__name__)
+        return intern(f"_memoize_dic_{function.__module__}{function.__name__}")
 
     def __call__(self, function):
         cache_dict_name = self.cache_dict_name
@@ -751,7 +757,7 @@ class keyed_memoize_on_first_arg:  # noqa: N801
                 return getattr(obj, cache_dict_name)[cache_key]
             except AttributeError:
                 result = function(obj, *args, **kwargs)
-                setattr(obj, cache_dict_name, {cache_key: result})
+                object.__setattr__(obj, cache_dict_name, {cache_key: result})
                 return result
             except KeyError:
                 result = function(obj, *args, **kwargs)
@@ -759,7 +765,7 @@ class keyed_memoize_on_first_arg:  # noqa: N801
                 return result
 
         def clear_cache(obj):
-            delattr(obj, cache_dict_name)
+            object.__delattr__(obj, cache_dict_name)
 
         from functools import update_wrapper
         new_wrapper = update_wrapper(wrapper, function)
@@ -775,9 +781,14 @@ class keyed_memoize_method(keyed_memoize_on_first_arg):  # noqa: N801
         which computes and returns the cache key.
 
     .. versionadded :: 2020.3
+
+    .. versionchanged:: 2021.2
+
+        Can memoize methods on classes that do not allow setting attributes
+        (e.g. by overwritting ``__setattr__``).
     """
     def _default_cache_dict_name(self, function):
-        return intern("_memoize_dic_" + function.__name__)
+        return intern(f"_memoize_dic_{function.__name__}")
 
 
 def memoize_method_with_uncached(uncached_args=None, uncached_kwargs=None):
@@ -802,7 +813,7 @@ def memoize_method_with_uncached(uncached_args=None, uncached_kwargs=None):
     uncached_kwargs = list(uncached_kwargs)
 
     def parametrized_decorator(method):
-        cache_dict_name = intern("_memoize_dic_"+method.__name__)
+        cache_dict_name = intern(f"_memoize_dic_{method.__name__}")
 
         def wrapper(self, *args, **kwargs):
             cache_args = list(args)
@@ -828,7 +839,7 @@ def memoize_method_with_uncached(uncached_args=None, uncached_kwargs=None):
                 return getattr(self, cache_dict_name)[key]
             except AttributeError:
                 result = method(self, *args, **kwargs)
-                setattr(self, cache_dict_name, {key: result})
+                object.__setattr__(self, cache_dict_name, {key: result})
                 return result
             except KeyError:
                 result = method(self, *args, **kwargs)
@@ -836,7 +847,7 @@ def memoize_method_with_uncached(uncached_args=None, uncached_kwargs=None):
                 return result
 
         def clear_cache(self):
-            delattr(self, cache_dict_name)
+            object.__delattr__(self, cache_dict_name)
 
         if sys.version_info >= (2, 5):
             from functools import update_wrapper
diff --git a/test/test_pytools.py b/test/test_pytools.py
index f3231c6fba52ee49ab31302c7f1edaa7bbba9e52..49a4390dae466af422045e9f3e7ffab449539a6a 100644
--- a/test/test_pytools.py
+++ b/test/test_pytools.py
@@ -154,6 +154,48 @@ def test_memoize_keyfunc():
     assert count[0] == 2
 
 
+def test_memoize_frozen():
+    from dataclasses import dataclass
+    from pytools import memoize_method
+
+    # {{{ check frozen dataclass
+
+    @dataclass(frozen=True)
+    class FrozenDataclass:
+        value: int
+
+        @memoize_method
+        def double_value(self):
+            return 2 * self.value
+
+    c = FrozenDataclass(10)
+    assert c.double_value() == 20
+    c.double_value.clear_cache(c)       # pylint: disable=no-member
+
+    # }}}
+
+    # {{{ check class with no setattr
+
+    class FrozenClass:
+        value: int
+
+        def __init__(self, value):
+            object.__setattr__(self, "value", value)
+
+        def __setattr__(self, key, value):
+            raise AttributeError(f"cannot set attribute {key}")
+
+        @memoize_method
+        def double_value(self):
+            return 2 * self.value
+
+    c = FrozenClass(10)
+    assert c.double_value() == 20
+    c.double_value.clear_cache(c)       # pylint: disable=no-member
+
+    # }}}
+
+
 @pytest.mark.parametrize("dims", [2, 3])
 def test_spatial_btree(dims, do_plot=False):
     pytest.importorskip("numpy")