# RestrictedPython and the honest version of running untrusted Python

> RestrictedPython compiles a restricted subset of Python so that untrusted code can run inside a trusted environment, but the project says outright that it is not a sandbox. The interesting parts are the boundary it draws, the import policy it refuses to provide, and the CPython only restriction that makes the whole approach possible.

**zopefoundation/RestrictedPython** — A restricted execution environment for Python to run untrusted code.

- Repository: https://github.com/zopefoundation/RestrictedPython
- Website: http://restrictedpython.readthedocs.io/
- Stars: 747 · Forks: 51
- Language: Python
- License: NOASSERTION
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/zopefoundation-restrictedpython

## The project says what it is not, before it says what it is

The opening description is careful in a way most security tools are not. RestrictedPython is a tool that helps to define a subset of the Python language which allows you to provide a program input into a trusted environment. And then the sentence that should be quoted in every article about it: RestrictedPython is not a sandbox system or a secured environment, but it helps to define a trusted environment and execute untrusted code inside of it.

That distinction is the whole design. The security here comes from the host. The trusted environment already exists, the untrusted code runs inside it, and RestrictedPython's job is to remove from the language the constructs that would let that code reach outside.

The most consequential limitation is stated as a warning right at the top. RestrictedPython only supports CPython. It does not support PyPy and other Python implementations, as it cannot provide its restrictions there. The reason is structural rather than incidental: the restrictions depend on how CPython compiles names and attributes, so an implementation with a different compilation model cannot enforce the same rules. A team that runs PyPy in production cannot use this approach at all.

The project is maintained by the Zope Foundation and the Plone Foundation, with the package description naming the Zope Foundation and contributors as authors and the Plone Foundation and contributors as maintainers. The package is on PyPI as RestrictedPython, with a readthedocs documentation site, and the repository is not archived with the last push on 2026-09-27.

## compile_restricted rewrites the code before it ever runs

The mechanism is a compiler, not a monitor. You hand source text to `compile_restricted` and it produces byte code that has been rewritten so that the dangerous operations are no longer reachable.

The harmless example in the documentation shows the shape of the call:

```python
    >>> from RestrictedPython import compile_restricted
    >>> from RestrictedPython import safe_globals
    >>>
    >>> source_code = """
    ... def example():
    ...     return 'Hello World!'
    ... """
    >>>
    >>> loc = {}
    >>> byte_code = compile_restricted(source_code, '<inline>', 'exec')
    >>> exec(byte_code, safe_globals, loc)
    >>>
    >>> loc['example']()
    'Hello World!'
```

Three things are happening there. The source is compiled with the restricted compiler rather than the built-in one, the byte code is executed with `safe_globals` as the globals mapping and a separate dict as the locals, and the result is then called out of the locals mapping. The globals and locals split is not incidental: names written by the code land in `loc`, and the host controls exactly what `safe_globals` contains.

The problematic example in the documentation is the same shape with an import in it, and it fails at compile-and-execute time rather than at some later check:

```python
    >>> from RestrictedPython import compile_restricted
    >>> from RestrictedPython import safe_globals
    >>>
    >>> source_code = """
    ... import os
    ...
    ... os.listdir('/')
    ... """
    >>> byte_code = compile_restricted(source_code, '<inline>', 'exec')
    >>> exec(byte_code, safe_globals, {})
    Traceback (most recent call last):
    ImportError: __import__ not found
```

The failure is an ImportError raised because `__import__` is not in the globals. The compiled code still contains an import statement, but there is nothing in the environment for it to call. That is the design working as intended: the restriction is enforced by what the environment provides, and the environment is written by the host application, not by the library.

## No __import__ implementation ships, on purpose

The warning attached to the failing example is the most operationally important line in the documentation, and it is a design decision with teeth.

RestrictedPython ships no `__import__` implementation on purpose. If you provide one, you move the security boundary out of RestrictedPython and into your own import policy. The documentation points to the security considerations page before you do so.

Read that carefully. The library is not withholding an import mechanism because it is hard to write. It is withholding it because the moment you add one, the responsibility for what can be imported becomes yours, and the library's own guarantee no longer covers it. An `__import__` that allows `os` gives back everything the restriction removed. An `__import__` that allows nothing is what you get out of the box, and that is why the example above fails with ImportError rather than with a permission error.

This is the design pattern to take away: the safe default is a failure, and the moment you customise it you own the outcome. Hosts that need to import specific modules for legitimate reasons, a template engine that needs `datetime`, a rule engine that needs `re`, end up writing and reviewing an import allowlist of their own.

It also explains why `safe_globals` is named the way it is. It is not a generic globals dict, it is a deliberately restricted set of builtins, and the host's job is to decide what goes in it.

## A mature package with a custom licence and a dev version number

The packaging metadata tells you who runs this and under what terms.

The licence is ZPL-2.1, the Zope Public License, which the setup.py header describes as a permissive style licence that accompanies the distribution with a disclaimer of warranties. The metadata also notes the package is generated with zope.meta from a template in the zope.meta repository, which is how the Zope ecosystem keeps packaging metadata consistent across its projects. The repository's own licence file is `LICENSE.txt` and the copyright notice is in `COPYRIGHT.txt`.

The version in the manifest is 8.6.dev0, and the development status classifier is `Development Status :: 6 - Mature`. The version scheme tells you something about the release process: the `.dev0` suffix means this checkout is a development state after 8.6, which is what you would expect from a repository that publishes no GitHub releases and manages its versions through `CHANGES.rst` and the `zope.meta` tooling instead. The classifiers list Python 3.10 through 3.15, and `requires-python` is `>=3.10, <3.16`.

Two classifier entries are worth pausing on. `Programming Language :: Python :: Implementation :: CPython` is declared explicitly, which mirrors the warning at the top of the documentation, and `Topic :: Security` with `Typing :: Typed` tells you the package ships type information.

The optional dependency groups are minimal and conventional: `test` pulls pytest and pytest-mock, `docs` pulls Sphinx and furo, and `typecheck` pulls mypy. The repository also carries `tox.ini`, `constraints.txt`, `buildout.cfg`, a `.pre-commit-config.yaml` and a `.readthedocs.yaml`, which is the standard Zope layout for a project with a long history and a large downstream.

## Where this approach is the wrong tool

The clearest case for not using RestrictedPython is when the untrusted code is actually hostile.

If the code you are running may be written by an attacker, the trusted environment has to be a separate process, a container, or a machine. RestrictedPython is explicit that it is not a sandbox, which means it does not isolate memory, does not isolate the filesystem, and does not protect the host from a denial of service. The protections it does offer are syntactic: the restricted compiler removes or rewrites the constructs that would let code reach the host, and the host controls what the environment contains. That is a meaningful protection against a script that does something careless, and it is not a protection against someone who is trying to escape.

There is a second, subtler limit. The restrictions are enforced at compile time, and the environment at run time is what makes them meaningful. A host that puts `os` into `safe_globals` has removed the protection. A host that implements `__import__` permissively has removed the protection and moved the problem. Neither is a mistake RestrictedPython can prevent, and both are mistakes that are easy to make under deadline pressure in a platform team that is adding a feature rather than a security boundary.

A third case is code that simply does not fit the subset. RestrictedPython defines a subset of the language, so valid Python that uses a construct the subset excludes will not run. The host has to work within the subset, which for a general purpose scripting feature can be a real constraint on what users can write.

And the CPython-only requirement removes the option of running the same restricted code on an alternative interpreter, which is exactly the case where you might want an isolation layer you can deploy anywhere.

## The alternative is process isolation, and the difference is where the trust lives

The realistic alternative to RestrictedPython is not another Python library. It is running the untrusted code in a separate process, and letting the operating system do the restricting.

The difference in approach is where the trust boundary sits. With RestrictedPython, the trust lives in the host application. The host constructs `safe_globals`, decides whether to supply an `__import__`, and controls what the code can reach, all inside one interpreter that the trusted code also runs in. A mistake in that configuration is a mistake in the same process.

With process isolation, the trust lives in the OS boundary. The untrusted code runs in a child process, with its own address space, its own filesystem view, resource limits and a network policy, and the only channel between it and the host is a pipe or a socket. A bug in the host's configuration is a bug in one process, not a way out of the sandbox.

The trade-off is cost and flexibility. A subprocess needs lifecycle management, serialisation of the results, and usually a container or at least a restricted user, and communication with it is more expensive than a function call. RestrictedPython is cheap, in-process, and easy to embed in something like a template evaluation, a rule engine, or a CMS script.

There is a hybrid that many hosts end up with, and it is worth naming: RestrictedPython inside a worker process. The library then narrows what the code can do, and the process boundary contains what happens if the narrowing is wrong. That is more work than either approach alone, but it is the configuration that matches the project's own framing, since the documentation describes it as helping to define a trusted environment and executing untrusted code inside of it rather than as a substitute for the environment.

## Conclusion

RestrictedPython fits a platform that needs to run code it did not write, such as a CMS letting site owners add logic, and that already has a trusted environment to contain it. It is the wrong tool for isolating genuinely hostile code, because the project states plainly that it is not a sandbox or a secured environment, and a separate process boundary or a container is the tool for that. Before adopting, verify three things: that you are on CPython, since the restrictions cannot be provided on PyPy, that you have read the security considerations page before writing your own `__import__`, and that your host environment exposes only the names the restricted code is meant to see. The code is under ZPL-2.1, the version in the manifest is 8.6.dev0, and the last push was on 2026-09-27.

## FAQ

### What is RestrictedPython and is it a sandbox?

It is a tool that helps to define a subset of the Python language so untrusted code can be executed inside a trusted environment. The documentation is explicit that RestrictedPython is not a sandbox system or a secured environment; it helps you define the trusted environment in which untrusted code then runs.

### Does RestrictedPython work on PyPy?

No. The documentation carries a warning that RestrictedPython only supports CPython and does not support PyPy and other Python implementations, as it cannot provide its restrictions there. The package metadata declares `Programming Language :: Python :: Implementation :: CPython` for the same reason.

### Why does importing a module inside restricted code raise ImportError?

Because RestrictedPython ships no `__import__` implementation on purpose. The documentation warns that if you provide one you move the security boundary out of RestrictedPython and into your own import policy, and points to the security considerations page before you do.

### How do I run a piece of code with RestrictedPython?

Compile the source with `compile_restricted(source_code, '<inline>', 'exec')` and execute the resulting byte code with `exec(byte_code, safe_globals, loc)`, keeping the host controlled globals in `safe_globals` and the names written by the code in a separate locals dict.

### What licence is RestrictedPython released under?

The Zope Public License, Version 2.1, recorded as `ZPL-2.1` in the package metadata and described in the setup.py header as a Zope Public License accompanying the distribution. The setup.py also states the software is provided as is, with all implied warranties disclaimed.

## Sources

- [Issues](https://github.com/zopefoundation/RestrictedPython/issues)
- [Project website](http://restrictedpython.readthedocs.io/)
- [README](https://github.com/zopefoundation/RestrictedPython/blob/master/README.md)
- [zopefoundation/RestrictedPython on GitHub](https://github.com/zopefoundation/RestrictedPython)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/zopefoundation-restrictedpython
