Re: discussion in comments:
Consider this example:
var = "123"
code = "print(var)"
exec(code)
Calling exec like this will execute the string in the current scope, so it'll have access to your var and will print "123".
But you can restrict eval's context by specifying dictionaries that should be used as the globals and locals respectively:
var = "123"
code = "print(var)"
sandbox_globals = {}
sandbox_locals = {}
exec(code, sandbox_globals, sandbox_locals)
Here, an exception will be raised:
NameError: name 'var' is not defined
Because you've specified the globals and locals to be used.
You can explicitly allow exec access by something like:
sandbox_globals = {'var':var}
But, you haven't fully restricted the sandbox this way.
sandbox_locals = {}
sandbox_globals = {}
code = "print(2**8)"
exec(code, sandbox_globals, sandbox_locals)
This code will work without throwing, printing 256. print works, and comes from somewhere. This is because __builtins__ is automatically added to sandbox_globals if you don't explicitly prevent that:
sandbox_locals = {}
sandbox_globals = {}
code = "print(2**8)"
sandbox_globals['__builtins__'] = {}
exec(code, sandbox_globals, sandbox_locals)
This will raise an Exception, since you're no longer providing print:
NameError: name 'print' is not defined
exec.