Python Basics: Precompile Source Trees with compileall
A packaging check can catch syntax errors before a program is started. compileall compiles Python source into bytecode caches without importing the module. This fixture verifies both that a cache appears and that module-level code was not executed.
Prepare a scratch directory
Tested on Linux with CPython 3.12.14. You need the named runtime, Bash, a text editor, and the standard mktemp, rm, and rmdir utilities. Run Python without -O so the assertions remain enabled. Any generated fixture files live inside an automatically cleaned temporary directory.
Keep using the same terminal. Stop if setup fails, before saving the source:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Run the example
Save this as demonstrate-compileall.py:
import compileall
import contextlib
import io
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
root = Path(directory)
good = root / "good.py"
good.write_text('raise RuntimeError("must not execute")\n', encoding="utf-8")
assert compileall.compile_file(str(good), quiet=2, force=True)
caches = list((root / "__pycache__").glob("good.*.pyc"))
assert len(caches) == 1
print("Valid source: compiled without execution")
print("Bytecode cache count:", len(caches))
bad = root / "broken.py"
bad.write_text("def broken(:\n", encoding="utf-8")
report = io.StringIO()
with contextlib.redirect_stdout(report):
success = compileall.compile_dir(str(root), quiet=1, force=True)
assert success is False
assert "SyntaxError" in report.getvalue()
assert not list((root / "__pycache__").glob("broken.*.pyc"))
print("Directory with invalid syntax:", success)
print("Diagnostic includes SyntaxError:", "SyntaxError" in report.getvalue())Run it:
python3 -B demonstrate-compileall.pyObserved output:
Valid source: compiled without execution
Bytecode cache count: 1
Directory with invalid syntax: False
Diagnostic includes SyntaxError: TrueRead the result
The valid file would raise immediately if imported, but compilation only translates its source. The cache filename includes an interpreter tag, so the program checks the cache count instead of hard-coding a version-specific filename. The second step adds invalid syntax and verifies that the directory result is false. Capturing the diagnostic keeps temporary paths out of the displayed output while still asserting the error type.
Apply it carefully
Successful compilation does not prove imports will resolve, types are correct, or execution will succeed. Keep runtime tests as a separate gate. quiet=1 suppresses routine progress but retains errors; quiet=2 suppresses both. When used from Bash, python3 -m compileall -q path/to/package provides a command exit status suitable for CI. Caches are interpreter-specific build products, not portable executables or protected source code.
The official reference describes the API and its options. The checks above cover the specific fixture, not every possible input or environment.
Clean up
Remove the source and then the empty practice directory:
rm -- demonstrate-compileall.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirIf the directory is not empty, inspect the remaining files rather than deleting unknown contents.
Acknowledgements
This post was created with assistance from AI.