Skip to content

Limitations

strictaccess is a discipline tool, not a security boundary. The restrictions below are inherent to the implementation. Read this page before relying on the library to keep anything truly hidden.

1. object.__getattribute__ bypasses the check

The engine works by overriding __getattribute__ on the class. Any caller willing to use object.__getattribute__ directly skips the override:

@strict_access_control()
class Vault:
    @private
    def _secret(self): return "leak"

# Normal access raises:
Vault()._secret()                                      # PrivateAccessError

# But this works:
object.__getattribute__(Vault(), "_secret")()          # "leak"

This is not a bug. Any Python-level access control built on __getattribute__ shares this property. If you need true isolation, use a different process, a different language, or a different design (network boundary, capability system, etc.).

2. Pickling dynamically-decorated classes can fail

@strict_access_control creates a wrapper class via type(...). The wrapper shares its __qualname__ with the original class, but pickle's default behaviour expects to find the actual class object at that qualname during unpickling — and the wrapper is not importable there.

Effects:

  • copy.deepcopy(instance) works (it uses __reduce_ex__ and reconstructs via the same wrapper). See tests/test_pickle.py.
  • pickle.dumps(instance) may raise PicklingError or AttributeError depending on context. Workaround: pickle a plain (non-decorated) copy of the data, or define __reduce__ on your class explicitly.

3. Caller detection requires CPython 3.11+

The engine uses frame.f_code.co_qualname, which was added in CPython 3.11. On older interpreters the library simply will not run. requires-python reflects this in pyproject.toml.

PyPy support is best-effort: if a given PyPy release implements 3.11+ frame APIs, strictaccess works. Otherwise, it does not.

4. The check has a runtime cost

Overriding __getattribute__ at the Python level pays a baseline overhead versus the C-implemented default. Measured numbers on the project's benchmark (tests/test_performance.py):

Configuration Cost vs plain object
Plain class, no override 1x
Empty Python __getattribute__ ~5x
strictaccess full check ~20-30x

In absolute terms each access takes roughly 700 nanoseconds. This is imperceptible for business logic — but if you have a hot inner loop accessing decorated attributes millions of times per second, profile first.

5. pickle of the decorated class object itself

Even without instances, pickle.dumps(MyDecoratedClass) may fail because the wrapper is not importable by its qualified name. Reference the original undecorated class if you need to pickle a class object.

6. __slots__ requires opt-in on the decorated class

The mixin and wrapper both declare __slots__ = (), so they do not inject a __dict__. However, if your decorated class does not declare its own __slots__, instances still get a __dict__ (inherited from object's default layout). To get a fully slotted decorated class, the user class must declare __slots__ itself:

@strict_access_control()
class Point:
    __slots__ = ("x", "y")              # required for no-__dict__ behaviour
    ...

7. @private on module-level functions has no defining class

Decorating a free function with @private records _access_level = "private" but _defining_class_qualname = None. If you later attach that function to a decorated class as an attribute, every access is rejected (because no caller can match None). This is the safe default; bind the function in the class body instead.

What strictaccess does NOT claim to do

  • Prevent reading attributes via vars(), __dict__, inspect, or any reflection API.
  • Prevent C extensions or ctypes from reading memory directly.
  • Stop a sufficiently motivated developer from doing whatever they want.

It claims to make accidental misuse loud, and intentional misuse obvious in code review. That is the entire feature.