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). Seetests/test_pickle.py.pickle.dumps(instance)may raisePicklingErrororAttributeErrordepending 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
ctypesfrom 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.