Right, so you’ve met exec and eval, the two party animals of dynamic execution. They’re flashy, they get all the attention, and they’re a bit messy. But behind them, there’s a quieter, more methodical function doing the real work: compile(). This is the function that takes your raw string of code and turns it into a proper, runnable code object. Think of it as the stage manager who sets everything up before the actors (exec/eval) even step on stage.

The signature tells you everything you need to know about its job: compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)

It takes your source string, a filename (which is mostly for error messages—make it something helpful like ‘’ or ‘my_snippet.py’), and a critical mode flag. This mode is where you decide the destiny of this compiled code:

  • 'exec': For compiling suites of statements (think a module or a script). This is what exec uses.
  • 'eval': For compiling a single expression. This is what eval uses. Feed it more than an expression and it will yell at you.
  • 'single': This one’s a bit weird. It’s for interactive statements. If the last statement is an expression, it prints its result, just like the Python REPL does. It’s less commonly used but fun to play with.

Let’s see it in action, separating the compilation from the execution.

# Compile a simple expression in 'eval' mode
expr_code = compile('2 * 21 + 3', '<string>', 'eval')
result = eval(expr_code)
print(result)  # Output: 45

# Compile a full statement suite in 'exec' mode
statement_code = compile("""
def greet(name):
    print(f"Oh, hi {name}! Didn't see you there.")
greet('Mark')
""", '<string>', 'exec')
exec(statement_code)  # Output: Oh, hi Mark! Didn't see you there.

Why Bother Compiling Separately?

You might be wondering, “Why wouldn’t I just use eval or exec directly?” Performance and reusability. If you have a chunk of code you need to execute repeatedly, compiling it once into a code object and then executing that object is significantly faster than making Python parse and compile the same string over and over again. It’s the difference between baking a cake from a mix every time you want a slice versus baking it once and then just grabbing a slice when you need it.

The Weird Cousin: ‘single’ Mode

I told you 'single' was weird. Let’s give it a proper introduction. It’s designed to mimic the behavior of the interactive interpreter.

# Notice the difference between 'exec' and 'single'
single_mode_code = compile("x = 1; x + 1", '<string>', 'single')
exec(single_mode_code)  # No output. It executed the assignment, but the expression result is lost.

single_mode_code = compile("x = 1; x + 1", '<string>', 'single')
# Now let's run it again in a fresh namespace to see the 'interactive' magic
fresh_ns = {}
exec(single_mode_code, fresh_ns)  # Output: 2
# Wait, what? It printed '2'! That's 'single' mode in action.

See? In 'single' mode, if the last statement is an expression, its value (if not None) is printed. This is purely a behavior of the mode itself when executed; it’s not something you’d use in production code, but it’s essential for building interactive tools like REPLs.

Flags and the optimize Parameter

This is where you get into the nitty-gritty. The flags control which future features (like annotations) are enabled. You can bitwise OR them together (e.g., flags=__future__.annotations.compiler_flag).

The optimize parameter is more practically useful. It controls the level of optimization the compiler applies, just like the -O command-line flag:

  • optimize=0 (-O0): No optimization. Asserts are kept. __debug__ is True.
  • optimize=1 (-O1): Asserts are stripped out. __debug__ is False.
  • optimize=2 (-O2): Same as -O1, but also removes docstrings from the compiled bytecode.

This is powerful. You can compile a version of your code with asserts for debugging and a leaner version for production, all from the same source string.

# Compile with and without optimizations
debug_code = compile("assert True, 'This is a test'; print('Debug build')", '<string>', 'exec', optimize=0)
production_code = compile("assert True, 'This is a test'; print('Production build')", '<string>', 'exec', optimize=1)

exec(debug_code)   # Will run the assert and print 'Debug build'
exec(production_code) # The assert statement is completely removed. Only prints 'Production build'

The Gotchas: It’s Not All Rainbows

compile() is powerful, but it’s not magic. It has limits. The biggest one: the code you compile must be syntactically complete at the moment you compile it. You can’t compile a function that refers to a variable that doesn’t exist yet—that’s a runtime error, which happens later when you exec it. But you can’t compile a function that has a syntax error, like a missing colon. That’s a SyntaxError at compile time.

Also, remember that compile() itself is a function that executes. If you’re compiling untrusted user input, you’re already on the highway to disaster. compile() can be a resource hog for large code strings, and it can theoretically crash the interpreter with a MemoryError or a RecursionError if the input is crafted maliciously or is just absurdly large. The security rules for exec and eval apply tenfold here, because you’re doing even more work with the untrusted code.

So, use compile() when you need the performance boost of reusing code objects or when you need fine-grained control over the compilation process (like optimization levels). For one-off executions, letting exec or eval handle the compilation for you is just fine. It’s the difference between buying a pre-made sandwich and carefully assembling your own from ingredients you prepared earlier. Both get you fed, but one is decidedly more chef-like.