🔐 localscope#

https://github.com/tillahoffmann/localscope/actions/workflows/build.yml/badge.svg https://img.shields.io/pypi/v/localscope.svg https://readthedocs.org/projects/localscope/badge/?version=latest

Interactive python sessions, such as Jupyter notebooks, are outstanding tools for analysing data, generating visualisations, and training machine learning models. However, the interactive nature allows global variables to leak into the scope of functions accidentally, leading to unexpected behaviour. Localscope gives you peace of mind by restricting the variables a function has access to.

>>> a = 'hello world'
>>> @localscope
... def print_a():
...     print(a)
Traceback (most recent call last):
  ...
localscope.LocalscopeException: `a` is not a permitted global (file "...",
   line 1, in print_a)

Motivation and detailed example#

Suppose you are evaluating the mean squared error between two lists of numbers, including a scale factor sigma.

>>> sigma = 7
>>> # [other notebook cells and bits of code]
>>> xs = [1, 2, 3]
>>> ys = [4, 5, 6]
>>> mse = sum(((x - y) / sigma) ** 2 for x, y in zip(xs, ys))
>>> mse
0.55102...

Everything works nicely, and you package the code in a function for later use but forget about the scale factor introduced earlier in the notebook.

>>> def evaluate_mse(xs, ys):  # missing argument sigma
...     return sum(((x - y) / sigma) ** 2 for x, y in zip(xs, ys))
>>> mse = evaluate_mse(xs, ys)
>>> round(mse, 5)
0.55102...

The variable sigma is obtained from the global scope, and the code executes without any issue. But the output is affected by changing the value of sigma.

>>> sigma = 13
>>> evaluate_mse(xs, ys)
0.15976...

This example may seem contrived. But unintended information leakage from the global scope to the local function scope often leads to unreproducible results, hours spent debugging, and many kernel restarts to identify the source of the problem. Localscope fixes this problem by restricting the allowed scope.

>>> from localscope import localscope
>>> @localscope
... def evaluate_mse(xs, ys):  # missing argument sigma
...     return sum(((x - y) / sigma) ** 2 for x, y in zip(xs, ys))
Traceback (most recent call last):
  ...
localscope.LocalscopeException: `sigma` is not a permitted global (file "...",
   line 3, in <genexpr>)

Interface#

localscope.localscope(func: function | code | None = None, *, predicate: Callable | None = None, allowed: Set[str] | None = None, allow_closure: bool = False)#

Restrict the scope of a callable to local variables to avoid unintentional information ingress.

Parameters:
  • func – Callable whose scope to restrict.

  • predicate – Predicate to determine whether a global variable is allowed in the scope. Defaults to allow any module.

  • allowed – Names of globals that are allowed to enter the scope.

localscope.mfc#

Decorator allowing modules, functions, and classes to enter the local scope.

Examples

Basic example demonstrating the functionality of localscope.

>>> a = 'hello world'
>>> @localscope
... def print_a():
...     print(a)
Traceback (most recent call last):
...
localscope.LocalscopeException: `a` is not a permitted global (file "...",
    line 1, in print_a)

The scope of a function can be extended by providing a list of allowed exceptions.

>>> a = 'hello world'
>>> @localscope(allowed=['a'])
... def print_a():
...     print(a)
>>> print_a()
hello world

The predicate keyword argument can be used to control which values are allowed to enter the scope (by default, only modules may be used in functions).

>>> a = 'hello world'
>>> allow_strings = localscope(predicate=lambda x: isinstance(x, str))
>>> @allow_strings
... def print_a():
...     print(a)
>>> print_a()
hello world

Localscope is strict by default, but localscope.mfc can be used to allow modules, functions, and classes to enter the function scope: a common use case in notebooks.

>>> class MyClass:
...     pass
>>> @localscope.mfc
... def create_instance():
...     return MyClass()
>>> create_instance()
<MyClass object at 0x...>

Notes

The localscope decorator analysis the decorated function (and any dependent code blocks) at the time of declaration because static analysis has a minimal impact on performance and it is easier to implement.