Technology

Show HN: Localscope–Limit scope of Python functions for reproducible execution

Toggle table of contents sidebar


https://img.shields.io/static/v1?label=&message=GitHub&color=gray&logo=github
https://github.com/tillahoffmann/localscope/actions/workflows/build.yml/badge.svg
https://readthedocs.org/projects/localscope/badge/?version=latest
https://img.shields.io/pypi/v/localscope.svg

Have you ever hunted bugs caused by accidentally using a global variable in a function in a Jupyter notebook? Have you ever scratched your head because your code broke after restarting the Python kernel? localscope can help by restricting the variables a function can access.

>>> from localscope import 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)

See the Interface section for an exhaustive list of options and the Motivation and Example for a more detailed example.

Installation#

Interface#

localscope.localscope(func: function | code | None = None, *, predicate: Callable | None = None, allowed: Iterable[str] | 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.

  • allow_closure – Allow access to non-local variables from the enclosing scope.

localscope.mfc#

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

Examples

Basic example demonstrating the functionality of localscope.

>>> from localscope import 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 an iterable of allowed
variable names or a string of space-separated allowed variable names.

>>> 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'
>>>
>>> @localscope(predicate=lambda x: isinstance(x, str))
... 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()

Notes

The localscope decorator analyses the decorated function at the time of
declaration because static analysis has a minimal impact on performance and it
is easier to implement.

This also ensures localscope does not affect how your code runs in any way.

>>> def my_func():
...     pass
>>>
>>> my_func is localscope(my_func)
True

Motivation and Example#

Interactive python sessions 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. For 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)
>>> mse
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.

>>> @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 )

Related Articles

Back to top button