diff --git a/pytools/__init__.py b/pytools/__init__.py
index ec878b57659ff92db73ad5c67361ed8817097007..0292680ba9591093f6a2c56815c50ce56df999de 100644
--- a/pytools/__init__.py
+++ b/pytools/__init__.py
@@ -675,8 +675,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:
@@ -688,7 +689,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)
@@ -696,7 +697,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)
@@ -707,9 +708,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
@@ -729,8 +736,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
@@ -746,7 +752,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)
@@ -754,7 +760,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)
@@ -770,9 +776,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):
@@ -797,7 +808,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)
@@ -823,7 +834,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)
@@ -831,7 +842,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 87b5d3a76d2d76e4a61e8343252e9b81cd7c767c..87e62a13a83c486e380b29466367a3a37153c7b8 100644
--- a/test/test_pytools.py
+++ b/test/test_pytools.py
@@ -131,6 +131,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")