Python’s built-in exec()
function allows you to execute arbitrary Python code from a string or compiled code input.
The exec()
function can be handy when you need to run dynamically generated Python code, but it can be pretty dangerous if you use it carelessly. In this tutorial, you’ll learn not only how to use exec()
, but just as importantly, when it’s okay to use this function in your code.
In this tutorial, you’ll learn how to:
- Work with Python’s built-in
exec()
function - Use
exec()
to execute code that comes as strings or compiled code objects - Assess and minimize the security risks associated with using
exec()
in your code
Additionally, you’ll write a few examples of using exec()
to solve different problems related to dynamic code execution.
To get the most out of this tutorial, you should be familiar with Python’s namespaces and scope, and strings. You should also be familiar with some of Python’s built-in functions.
Sample Code: Click here to download the free sample code that you’ll use to explore use cases for the exec() function.
Getting to Know Python’s exec()
Python’s built-in exec()
function allows you to execute any piece of Python code. With this function, you can execute dynamically generated code. That’s the code that you read, auto-generate, or obtain during your program’s execution. Normally, it’s a string.
The exec()
function takes a piece of code and executes it as your Python interpreter would. Python’s exec()
is like eval()
but even more powerful and prone to security issues. While eval()
can only evaluate expressions, exec()
can execute sequences of statements, as well as imports, function calls and definitions, class definitions and instantiations, and more. Essentially, exec()
can execute an entire fully featured Python program.
The signature of exec()
has the following form:
exec(code [, globals [, locals]])
The function executes code
, which can be either a string containing valid Python code or a compiled code object.
Note: Python is an interpreted language instead of a compiled one. However, when you run some Python code, the interpreter translates it into bytecode, which is an internal representation of a Python program in the CPython implementation. This intermediate translation is also referred to as compiled code and is what Python’s virtual machine executes.
If code
is a string, then it’s parsed as a suite of Python statements, which is then internally compiled into bytecode, and finally executed, unless a syntax error occurs during the parsing or compilation step. If code
holds a compiled code object, then it’s executed directly, making the process a bit more efficient.
The globals
and locals
arguments allow you to provide dictionaries representing the global and local namespaces in which exec()
will run the target code.
The exec()
function’s return value is None
, probably because not every piece of code has a final, unique, and concrete result. It may just have some side effects. This behavior notably differs from eval()
, which returns the result of the evaluated expression.
To get an initial feeling of how exec()
works, you can create a rudimentary Python interpreter with two lines of code:
>>> while True:
... exec(input("->> "))
...
->> print("Hello, World!")
Hello, World!
->> import this
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
...
->> x = 10
->> if 1 <= x <= 10: print(f"{x} is between 1 and 10")
10 is between 1 and 10
In this example, you use an infinite while
loop to mimic the behavior of a Python interpreter or REPL. Inside the loop, you use input()
to get the user’s input at the command line. Then you use exec()
to process and run the input.
This example showcases what’s arguably the main use case of exec()
: executing code that comes to you as a string.
Note: You’ve learned that using exec()
can imply security risks. Now that you’ve seen the main use case of exec()
, what do you think those security risks might be? You’ll find the answer later in this tutorial.
You’ll commonly use exec()
when you need to dynamically run code that comes as a string. For example, you can write a program that generates strings containing valid Python code. You can build these strings from parts that you obtain at different moments in your program’s execution. You can also use the user’s input or any other input source to construct these strings.
Once you’ve built the target code as strings, then you can use exec()
to execute them as you would execute any Python code.
In this situation, you can rarely be certain of what your strings will contain. That’s one reason why exec()
implies serious security risks. This is particularly true if you’re using untrusted input sources, like a user’s direct input, in building your code.
In programming, a function like exec()
is an incredibly powerful tool because it allows you to write programs that generate and execute new code dynamically. To generate this new code, your programs will use information available at runtime only. To run the code, your programs will use exec()
.
However, with great power comes great responsibility. The exec()
function implies serious security risks, as you’ll learn soon. So, you should avoid using exec()
most of the time.
In the following sections, you’ll learn how exec()
works and how to use this function for executing code that comes as strings or as compiled code objects.
Running Code From a String Input
The most common way to call exec()
is with code that comes from a string-based input. To build this string-based input, you can use:
- Single lines of code or one-liner code snippets
- Multiple lines of code separated by semicolons
- Multiple lines of code separated by newline characters
- Multiple lines of code within triple-quoted strings and with proper indentation
A one-liner program consists of a single line of code that performs multiple actions at once. Say that you have a sequence of numbers, and you want to build a new sequence containing the sum of squares of all the even numbers in an input sequence.
To solve this problem, you can use the following one-liner code:
>>> numbers = [2, 3, 7, 4, 8]
>>> sum(number**2 for number in numbers if number % 2 == 0)
84
In the highlighted line, you use a generator expression to compute the square value of all the even numbers in the input sequence of values. Then you use sum()
to compute the total sum of squares.
To run this code with exec()
, you just need to transform your one-liner code into a single-line string:
>>> exec("result = sum(number**2 for number in numbers if number % 2 == 0)")
>>> result
84
In this example, you express the one-liner code as a string. Then you feed this string into exec()
for execution. The only difference between your original code and the string is that the latter stores the computation result in a variable for later access. Remember that exec()
returns None
rather than a concrete execution result. Why? Because not every piece of code has a final unique result.
Python allows you to write multiple statements in a single line of code, using semicolons to separate them. Even though this practice is discouraged, nothing will stop you from doing something like this:
>>> name = input("Your name: "); print(f"Hello, {name}!")
Your name: Leodanis
Hello, Leodanis!
You can use semicolons to separate multiple statements and build a single-line string that serves as an argument to exec()
. Here’s how:
>>> exec("name = input('Your name: '); print(f'Hello, {name}!')")
Your name: Leodanis
Hello, Leodanis!
The idea of this example is that you can combine multiple Python statements into a single-line string by using semicolons to separate them. In the example, the first statement takes the user’s input, while the second statement prints a greeting message to the screen.
You can also aggregate multiple statements in a single-line string using the newline character, \n
:
>>> exec("name = input('Your name: ')\nprint(f'Hello, {name}!')")
Your name: Leodanis
Hello, Leodanis!
The newline character makes exec()
understand your single-line string as a multiline set of Python statements. Then exec()
runs the aggregated statements in a row, which works like a multiline code file.
The final approach to building a string-based input for feeding exec()
is to use triple-quoted strings. This approach is arguably more flexible and allows you to generate string-based input that looks and works like normal Python code.
It’s important to note that this approach requires you to use proper indentation and code formatting. Consider the following example:
>>> code = """
... numbers = [2, 3, 7, 4, 8]
...
... def is_even(number):
... return number % 2 == 0
...
... even_numbers = [number for number in numbers if is_even(number)]
...
... squares = [number**2 for number in even_numbers]
...
... result = sum(squares)
...
... print("Original data:", numbers)
... print("Even numbers:", even_numbers)
... print("Square values:", squares)
... print("Sum of squares:", result)
... """
>>> exec(code)
Original data: [2, 3, 7, 4, 8]
Even numbers: [2, 4, 8]
Square values: [4, 16, 64]
Sum of squares: 84
In this example, you use a triple-quoted string to provide the input to exec()
. Note that this string looks like any regular piece of Python code. It uses appropriate indentation, naming style, and formatting. The exec()
function will understand and execute this string as a regular Python code file.
You should note that when you pass a string with code to exec()
, the function will parse and compile the target code into Python bytecode. In all cases, the input string should contain valid Python code.
If exec()
finds any invalid syntax during the parsing and compilation steps, then the input code won’t run:
>>> exec("print('Hello, World!)")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1
print('Hello, World!)
^
SyntaxError: unterminated string literal (detected at line 1)
In this example, the target code contains a call to print()
that takes a string as an argument. This string isn’t properly ended with a closing single quotation mark, so exec()
raises a SyntaxError
pointing out the issue and doesn’t run the input code. Note that Python pinpoints the error at the start of the string rather than at the end, where the closing single quotation mark should go.
Running code that comes as a string, like you did in the example above, is arguably the natural way of using exec()
. However, if you need to run the input code many times, then using a string as an argument will make the function run the parsing and compilation steps every time. This behavior can make your code inefficient in terms of execution speed.
In this situation, the most convenient approach is to compile the target code beforehand and then run the resulting bytecode with exec()
as many times as needed. In the following section, you’ll learn how to use exec()
with compiled code objects.
Executing Compiled Code
In practice, exec()
can be quite slow when you use it to process strings containing code. If you ever need to dynamically run a given piece of code more than once, then compiling it beforehand will be the most performant and recommended approach. Why? Because you’ll be running the parsing and compilation steps only once and then reusing the compiled code.
To compile a piece of Python code, you can use compile()
. This built-in function takes a string as an argument and runs a one-time bytecode compilation on it, generating a code object that you can then pass to exec()
for execution.
The signature of compile()
has the following form:
compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)
In this tutorial, you’ll only use the three first arguments to compile()
. The source
argument holds the code that you need to compile into bytecode. The filename
argument will hold the file from which the code was read. To read from a string object, you’ll have to set filename
to the "<string>"
value.
Note: To dive deeper into the rest of the arguments to compile()
, check out the function’s official documentation.
Finally, compile()
can generate code objects that you can execute using either exec()
or eval()
, depending on the mode
argument’s value. This argument should be set to "exec"
or "eval"
, depending on the target execution function:
>>> string_input = """
... def sum_of_even_squares(numbers):
... return sum(number**2 for number in numbers if number % 2 == 0)
...
... print(sum_of_even_squares(numbers))
... """
>>> compiled_code = compile(string_input, "<string>", "exec")
>>> exec(compiled_code)
>>> numbers = [2, 3, 7, 4, 8]
>>> exec(compiled_code)
84
>>> numbers = [5, 3, 9, 6, 1]
>>> exec(compiled_code)
36
Compiling often-repeated code up front with compile()
can help you slightly improve your code’s performance by skipping the parsing and bytecode compilation steps on each call to exec()
.
Running Code From Python Source Files
You can also use exec()
to run code that you’ve read from a reliable .py
file in your file system or somewhere else. To do this, you can use the built-in open()
function to read the file’s content as a string, which you can then pass as an argument to exec()
.
For example, say that you have a Python file named hello.py
containing the following code:
# hello.py
print("Hello, Pythonista!")
print("Welcome to Real Python!")
def greet(name="World"):
print(f"Hello, {name}!")
This sample script prints a greeting and a welcome message to the screen. It also defines a sample greet()
function for testing purposes. The function takes a name as an argument and prints a customized greeting to the screen.
Now get back to your Python interactive session and run the following code:
>>> with open("hello.py", mode="r", encoding="utf-8") as hello:
... code = hello.read()
...
>>> exec(code)
Hello, Pythonista!
Welcome to Real Python!
>>> greet()
Hello, World!
>>> greet("Pythonista")
Hello, Pythonista!
In this example, you first open the target .py
file as a regular text file using the built-in open()
function in a with
statement. Then you call .read()
on the file object to read the file’s content into the code
variable. This call to .read()
returns the file’s content as a string. The final step is to call exec()
with this string as an argument.
This example runs the code and makes the greet()
function and objects that live in hello.py
available in your current namespace. That’s why you can use greet()
directly. The secret behind this behavior has to do with the globals
and locals
arguments, which you’ll learn about in the next section.
Using the technique in the above example, you can open, read, and execute any file containing Python code. This technique may work when you don’t know up front which source files you’ll be running. So, you can’t write import module
, because you don’t know the module’s name when you’re writing the code.
Note: In Python, you’ll find safer ways to obtain a similar result. You can use the import system, for example. To dive deeper into this alternative, check out Dynamic Imports.
If you ever choose to use this technique, then make sure that you only execute code from trusted source files. Ideally, the most reliable source files are those that you have consciously created to run dynamically. You must never run code files that come from external sources, including your users, without inspecting the code first.
Using the globals
and locals
Arguments
You can pass an execution context to exec()
using the globals
and locals
arguments. These arguments can accept dictionary objects that’ll work as the global and local namespaces that exec()
will use to run the target code.
These arguments are optional. If you omit them, then exec()
will execute the input code in the current scope, and all the names and objects in this scope will be available to exec()
. Likewise, all the names and objects that you define in the input code will be available in the current scope after the call to exec()
.
Consider the following example:
>>> code = """
... z = x + y
... """
>>> # Global names are accessible from exec()
>>> x = 42
>>> y = 21
>>> z
Traceback (most recent call last):
...
NameError: name 'z' is not defined
>>> exec(code)
>>> # Names in code are available in the current scope
>>> z
63
This example shows that if you call exec()
without providing specific values to the globals
and locals
arguments, then the function runs the input code in the current scope. In this case, the current scope is the global one.
Note that after you call exec()
, the names defined in the input code are also available in the current scope. That’s why you can access z
in the final line of code.
If you only provide a value to globals
, then that value must be a dictionary. The exec()
function will use this dictionary for both global and local names. This behavior will restrict access to most names in the current scope:
>>> code = """
... z = x + y
... """
>>> x = 42
>>> y = 21
>>> exec(code, {"x": x})
Traceback (most recent call last):
...
NameError: name 'y' is not defined
>>> exec(code, {"x": x, "y": y})
>>> z
Traceback (most recent call last):
...
NameError: name 'z' is not defined
In the first call to exec()
, you use a dictionary as the globals
argument. Because your dictionary doesn’t provide a key holding the y
name, the call to exec()
doesn’t have access to this name and raises a NameError
exception.
In the second call to exec()
, you provide a different dictionary to globals
. In this case, the dictionary contains both variables, x
and y
, which allows the function to work correctly. However, this time you don’t have access to z
after the call to exec()
. Why? Because you’re using a custom dictionary to provide an execution scope to exec()
rather than falling back to your current scope.
If you call exec()
with a globals
dictionary that doesn’t contain the __builtins__
key explicitly, then Python will automatically insert a reference to the built-in scope or namespace under that key. So, all the built-in objects will be accessible from your target code:
>>> code = """
... print(__builtins__)
... """
>>> exec(code, {})
{'__name__': 'builtins', '__doc__': "Built-in functions, ...}
In this example, you’ve provided an empty dictionary to the globals
argument. Note that exec()
still has access to the built-in namespace because this namespace is automatically inserted into the provided dictionary under the __builtins__
key.
If you provide a value for the locals
argument, then it can be any mapping object. This mapping object will hold the local namespace when exec()
is running your target code:
>>> code = """
... z = x + y
... print(f"{z=}")
... """
>>> x = 42 # Global name
>>> def func():
... y = 21 # Local name
... exec(code, {"x": x}, {"y": y})
...
>>> func()
z=63
>>> z
Traceback (most recent call last):
...
NameError: name 'z' is not defined
The call to exec()
is embedded in a function in this example. Therefore, you have a global (module-level) scope and a local (function-level) scope. The globals
argument provides the x
name from the global scope, and the locals
argument provides the y
name from the local scope.
Note that after running func()
, you don’t have access to z
because this name was created under the local scope of exec()
, which isn’t available from the outside.
With the globals
and locals
arguments, you can tweak the context in which exec()
runs your code. These arguments are pretty helpful when it comes to minimizing the security risks associated with exec()
, but you should still make sure that you’re running code from trusted sources only. In the following section, you’ll learn about these security risks and how to deal with them.
Uncovering and Minimizing the Security Risks Behind exec()
As you’ve learned so far, exec()
is a powerful tool that allows you to execute arbitrary code that comes to you as strings. You should use exec()
with extreme care and caution because of its ability to run any piece of code.
Typically, the code that feeds exec()
is dynamically generated at runtime. This code may have many sources of input, which could include your program user, other programs, a database, a stream of data, and a network connection, among others.
In this scenario, you can’t be entirely sure what the input string will contain. So, the probability of facing an untrusted and malicious source of input code is pretty high.
The security issues associated with exec()
are the most common reason why many Python developers recommend avoiding this function altogether. Finding a better, faster, more robust, and safer solution is almost always possible.
However, if you must use exec()
in your code, then the generally recommended approach is to use it with explicit globals
and locals
dictionaries.
Another critical issue with exec()
is that it breaks a fundamental assumption in programming: the code that you’re currently reading or writing is the code that you’ll be executing when you fire up your program. How does exec()
break this assumption? It makes your programs run new and unknown code that’s dynamically generated. This new code can be hard to follow, maintain, or even control.
In the following sections, you’ll dive into a few recommendations, techniques, and practices that you should apply if you ever need to use exec()
in your code.
Avoiding Input From Untrusted Sources
If your users can provide your programs with arbitrary Python code at runtime, then issues can arise if they enter code that violates or breaks your security rules. To illustrate this problem, get back to the Python interpreter example that uses exec()
for code execution:
>>> while True:
... exec(input("->> "))
...
->> print("Hello, World!")
Hello, World!
Now say that you want to use this technique to implement an interactive Python interpreter on one of your Linux web servers. If you allow your users to pass arbitrary code into your program directly, then a malicious user might provide something like "import os; os.system('rm -rf *')"
. This code snippet will probably remove all the content of your server’s disk, so don’t run it.
To prevent this risk, you can restrict access to the import
system by taking advantage of the globals
dictionary:
>>> exec("import os", {"__builtins__": {}}, {})
Traceback (most recent call last):
...
ImportError: __import__ not found
The import
system internally uses the built-in __import__()
function. So, if you forbid access to the built-in namespace, then the import
system won’t work.
Even though you can tweak the globals
dictionary as shown in the above example, one thing that you must never do is to use exec()
for running external and potentially unsafe code on your own computer. Even if you carefully clean up and validate the input, you’ll risk being hacked. So, you’re best avoiding this practice.
Restricting globals
and locals
to Minimize Risks
You can provide custom dictionaries as the globals
and locals
arguments if you want to fine-tune the access to global and local names when running code with exec()
. For example, if you pass empty dictionaries to both globals
and locals
, then exec()
won’t have access to your current global and local namespaces:
>>> x = 42
>>> y = 21
>>> exec("print(x + y)", {}, {})
Traceback (most recent call last):
...
NameError: name 'x' is not defined
If you call exec()
with empty dictionaries for globals
and locals
, then you forbid access to global and local names. This tweak allows you to restrict the names and objects available when you’re running code with exec()
.
However, this technique doesn’t guarantee a safe use of exec()
. Why? Because the function still has access to all of Python’s built-in names, as you learned in the section about the globals
and locals
arguments:
>>> exec("print(min([2, 3, 7, 4, 8]))", {}, {})
2
>>> exec("print(len([2, 3, 7, 4, 8]))", {}, {})
5
In these examples, you use empty dictionaries for globals
and locals
, but exec()
can still access built-in functions like min()
, len()
, and print()
. How would you prevent exec()
from accessing built-in names? That’s the next section’s topic.
Deciding on Allowed Built-in Names
As you’ve already learned, if you pass a custom dictionary to globals
without a __builtins__
key, then Python will automatically update that dictionary with all the names in the built-in scope under a new __builtins__
key. To restrict this implicit behavior, you can use a globals
dictionary containing a __builtins__
key with an appropriate value.
For example, if you want to forbid access to built-in names completely, then you can call exec()
like in the following example:
>>> exec("print(min([2, 3, 7, 4, 8]))", {"__builtins__": {}}, {})
Traceback (most recent call last):
...
NameError: name 'print' is not defined
In this example, you set globals
to a custom dictionary containing a __builtins__
key with an empty dictionary as its associated value. This practice prevents Python from inserting a reference to the built-in namespace into globals
. This way, you ensure that exec()
won’t have access to built-in names while executing your code.
You can also tweak your __builtins__
key if you need exec()
to access certain built-in names only:
>>> allowed_builtins = {"__builtins__": {"min": min, "print": print}}
>>> exec("print(min([2, 3, 7, 4, 8]))", allowed_builtins, {})
2
>>> exec("print(len([2, 3, 7, 4, 8]))", allowed_builtins, {})
Traceback (most recent call last):
...
NameError: name 'len' is not defined
In the first example, exec()
successfully runs your input code because min()
and print()
are present in the dictionary associated with the __builtins__
key. In the second example, exec()
raises a NameError
and doesn’t run your input code because len()
isn’t present in the provided allowed_builtins
.
The techniques in the above examples allow you to minimize the security implications of using exec()
. However, these techniques aren’t entirely foolproof. So, whenever you feel that you need to use exec()
, try to think of another solution that doesn’t use the function.
Putting exec()
Into Action
Up to this point, you’ve learned how the built-in exec()
function works. You know that you can use exec()
to run either string-based or compiled-code input. You also learned that this function can take two optional arguments, globals
and locals
, which allow you to tweak the execution namespace for exec()
.
Additionally, you’ve learned that using exec()
implies some serious security issues, including allowing users to run arbitrary Python code on your computer. You studied some recommended coding practices that help minimize the security risks associated with exec()
in your code.
In the following sections, you’ll code a few practical examples that’ll help you spot those use cases in which using exec()
can be appropriate.
Running Code From External Sources
Using exec()
to execute code that comes as strings from either your users or any other source is probably the most common and dangerous use case of exec()
. This function is the quickest way for you to accept code as strings and run it as regular Python code in the context of a given program.
You must never use exec()
to run arbitrary external code on your machine, because there’s no secure way to do it. If you’re going to use exec()
, then use it as a way to let your users run their own code on their own machines.
The standard library has a few modules that use exec()
for executing code provided by the user as a string. A good example is the timeit
module, which Guido van Rossum originally wrote himself.
The timeit
module provides a quick way to time small pieces of Python code that come as strings. Check out the following example from the module’s documentation:
>>> from timeit import timeit
>>> timeit("'-'.join(str(n) for n in range(100))", number=10000)
0.1282792080000945
The timeit()
function takes a code snippet as a string, runs the code, and returns a measurement of the execution time. The function also takes several other arguments. For example, number
allows you to provide the number of times that you want to execute the target code.
At the heart of this function, you’ll find the Timer
class. Timer
uses exec()
to run the provided code. If you inspect the source code of Timer
in the timeit
module, then you’ll find that the class’s initializer, .__init__()
, includes the following code:
# timeit.py
# ...
class Timer:
"""Class for timing execution speed of small code snippets."""
def __init__(
self,
stmt="pass",
setup="pass",
timer=default_timer,
globals=None
):
"""Constructor. See class doc string."""
self.timer = timer
local_ns = {}
global_ns = _globals() if globals is None else globals
# ...
src = template.format(stmt=stmt, setup=setup, init=init)
self.src = src # Save for traceback display
code = compile(src, dummy_src_name, "exec")
exec(code, global_ns, local_ns)
self.inner = local_ns["inner"]
# ...
The call to exec()
in the highlighted line executes the user’s code using global_ns
and local_ns
as its global and local namespaces.
This way of using exec()
is appropriate when you’re providing a tool for your users, who will have to provide their own target code. This code will run on the users’ machines, so they’ll be responsible for guaranteeing that the input code is secure to run.
Another example of using exec()
to run code that comes as a string is the doctest
module. This module inspects your docstrings in search of text that looks like a Python interactive session. If doctest
finds any interactive-session-like text, then it executes that text as Python code to check if it works as expected.
For example, say that you have the following function for adding two numbers together:
# calculations.py
def add(a, b):
"""Return the sum of two numbers.
Tests:
>>> add(5, 6)
11
>>> add(2.3, 5.4)
7.7
>>> add("2", 3)
Traceback (most recent call last):
TypeError: numeric type expected for "a" and "b"
"""
if not (isinstance(a, (int, float)) and isinstance(b, (int, float))):
raise TypeError('numeric type expected for "a" and "b"')
return a + b
# ...
In this code snippet, add()
defines a docstring with several tests that check how the function should work. Note that the tests represent calls to add()
in a hypothetical interactive session using valid and invalid argument types.
Once you have these interactive tests and their expected outputs in your docstrings, then you can use doctest
to run them and check if they issue the expected result.
Note: The doctest module provides an amazing and useful tool that you can use for testing your code while you’re writing it.
Go to your command line and run the following command in the directory containing your calculations.py
file:
$ python -m doctest calculations.py
This command won’t issue any output if all the test works as expected. If at least one test fails, then you’ll get an exception pointing out the problem. To confirm this, you can change one of the expected outputs in the function’s docstring and run the above command again.
The doctest
module uses exec()
to execute any interactive docstring-embedded code, as you can confirm in the module’s source code:
# doctest.py
class DocTestRunner:
# ...
def __run(self, test, compileflags, out):
# ...
try:
# Don't blink! This is where the user's code gets run.
exec(
compile(example.source, filename, "single", compileflags, True),
test.globs
)
self.debugger.set_continue() # ==== Example Finished ====
exception = None
except KeyboardInterrupt:
# ...
As you can confirm in this code snippet, the user’s code runs in an exec()
call, which uses compile()
to compile the target code. To run this code, exec()
uses test.globs
as its globals
argument. Note that the comment right before the call to exec()
jokingly states that this is the place where the user’s code runs.
Again, in this use case of exec()
, the responsibility of providing secure code examples is on the users. The doctest
maintainers aren’t responsible for ensuring that the call to exec()
doesn’t cause any damage.
It’s important to note that doctest
doesn’t prevent the security risks associated with exec()
. In other words, doctest
will run any Python code. For example, someone could modify your add()
function to include the following code in the docstring:
# calculations.py
def add(a, b):
"""Return the sum of two numbers.
Tests:
>>> import os; os.system("ls -l")
0
"""
if not (isinstance(a, (int, float)) and isinstance(b, (int, float))):
raise TypeError('numeric type expected for "a" and "b"')
return a + b
If you run doctest
on this file, then the ls -l
command will successfully run. In this example, the embedded command is mostly harmless. However, a malicious user could modify your docstring and embed something like os.system("rm -rf *")
or any other dangerous command.
Again, you always have to be careful with exec()
and with tools that use this function, like doctest
does. In the specific case of doctest
, as long as you know where your embedded test code is coming from, this tool will be pretty safe and useful.
Using Python for Configuration Files
Another situation in which you can use exec()
to run code is when you have a configuration file that uses valid Python syntax. Your file can define several configuration parameters with specific values. Then you can read the file and process its content with exec()
to build a dictionary object containing all your configuration parameters and their values.
For example, say that you have the following configuration file for a text editor app that you’re working on:
# settings.conf
font_face = ""
font_size = 10
line_numbers = True
tab_size = 4
auto_indent = True
This file has valid Python syntax, so you can execute its content using exec()
as you’d do with a regular .py
file.
Note: You’ll find several better and safer ways to work with configuration files than using exec()
. In the Python standard library, you have the configparser
module, which allows you to process configuration files that use the INI file format.
The below function reads your settings.conf
file and builds a configuration dictionary:
>>> from pathlib import Path
>>> def load_config(config_file):
... config_file = Path(config_file)
... code = compile(config_file.read_text(), config_file.name, "exec")
... config_dict = {}
... exec(code, {"__builtins__": {}}, config_dict)
... return config_dict
...
>>> load_config("settings.conf")
{
'font_face': '',
'font_size': 10,
'line_numbers': True,
'tab_size': 4,
'auto_indent': True
}
The load_config()
function takes the path to a configuration file. Then it reads the target file as text and passes that text into exec()
for execution. During the exec()
run, the function injects the configuration parameters into the locals
dictionary, which is later returned to the caller code.
Note: The technique in this section is probably a safe use case of exec()
. In the example, you’ll have an app running on your system, specifically a text editor.
If you modify the app’s configuration file to include malicious code, then you’ll only damage yourself, which you most likely wouldn’t do. However, there’s still a possibility that you could accidentally include potentially dangerous code in the app’s configuration file. So, this technique could end up being unsafe if you’re not careful.
Of course, if you’re coding the app yourself and you release a configuration file with malicious code, then you’ll harm the community at large.
That’s it! Now you can read all your configuration parameters and their corresponding values from the resulting dictionary and use the parameters to set up your editor project.
Conclusion
You’ve learned how to use the built-in exec()
function to execute Python code from a string or bytecode input. This function provides a quick tool for executing dynamically generated Python code. You also learned how to minimize the security risks associated with exec()
and when it’s okay to use the function in your code.
In this tutorial, you’ve learned how to:
- Work with Python’s built-in
exec()
function - Use Python’s
exec()
to run string-based and compiled-code input - Assess and minimize the security risks associated with using
exec()
Additionally, you’ve coded a few practical examples that helped you better understand when and how to use exec()
in your Python code.
Sample Code: Click here to download the free sample code that you’ll use to explore use cases for the exec() function.