Skip to content

py_compile Module

The py_compile module compiles Python source files to bytecode (.pyc files), useful for precompilation and optimization.

Complexity Reference

Operation Time Space Notes
compile() O(n) O(n) n = source length; parses, compiles and writes
invalidation_mode= O(n) O(1) Hashing the source adds about 3% to a compile

Compiling Python Files

Basic Compilation

import py_compile
import tempfile
from pathlib import Path

work = Path(tempfile.mkdtemp())
module = work / 'module.py'
module.write_text('value = 1\n')

# Compile file - O(n). Returns the path it wrote, or None on failure
written = py_compile.compile(module)
print(written)  # .../__pycache__/module.<cache_tag>.pyc (PEP 3147)
# the tag is sys.implementation.cache_tag, so it changes with the version

# Compile with custom output
py_compile.compile(module, cfile=work / 'module.pyc')

Reporting Failures

compile() returns the path of the bytecode it wrote. On a compilation error it returns None and writes a formatted syntax-error diagnostic to stderr - not a traceback. doraise=True raises PyCompileError instead of writing anything, at any quiet below 2.

quiet=2 overrides doraise

The raise is inside an if quiet < 2: branch, so quiet=2 swallows the error entirely and returns None - a build step asking for both gets neither the exception nor the diagnostic. quiet=0 and quiet=1 both raise when doraise is set.

A missing source file raises FileNotFoundError under every combination: doraise governs compilation errors, not reading the file.

import py_compile
import tempfile
from pathlib import Path

work = Path(tempfile.mkdtemp())
broken = work / 'broken.py'
broken.write_text('def (\n')

print(py_compile.compile(broken, quiet=2))  # None, and nothing printed

# doraise raises at quiet=0 and quiet=1 alike; quiet=2 would return None
try:
    py_compile.compile(broken, doraise=True)
except py_compile.PyCompileError as error:
    print(f'failed: {error.file}')

assert py_compile.compile(broken, doraise=True, quiet=2) is None

try:
    py_compile.compile(work / 'absent.py', doraise=True)
except FileNotFoundError as error:
    print(f'no such source: {error.filename}')

Choosing How Bytecode Is Invalidated

TIMESTAMP records the source's modification time and size. That is the cheapest check, and it misses an edit that changes neither - which a checkout, a restore, or a generator writing the same-length content can produce. CHECKED_HASH records a hash of the source instead and costs about 3% more to write (CPython 3.11, one machine; illustrative).

The default is not fixed: py_compile chooses CHECKED_HASH when SOURCE_DATE_EPOCH is set in the environment, and TIMESTAMP otherwise. So a reproducible build already gets hash invalidation without asking.

import py_compile
import tempfile
from pathlib import Path

modes = py_compile.PycInvalidationMode
module = Path(tempfile.mkdtemp()) / 'module.py'
module.write_text('value = 1\n')

# mtime and size, and nothing else - the default without SOURCE_DATE_EPOCH
py_compile.compile(module, invalidation_mode=modes.TIMESTAMP)

# Verify the source hash on every import - immune to mtime games
py_compile.compile(module, invalidation_mode=modes.CHECKED_HASH)

# Record the hash but never check it - for read-only deployments
py_compile.compile(module, invalidation_mode=modes.UNCHECKED_HASH)

Batch Compilation

import py_compile
import tempfile
from pathlib import Path

work = Path(tempfile.mkdtemp())
files = []
for name in ('script1.py', 'script2.py', 'module.py'):
    path = work / name
    path.write_text('value = 1\n')
    files.append(path)

# Compile several - O(k*n) for k files of n bytes
for f in files:
    py_compile.compile(f)  # O(n) per file

# compileall does this over a tree, and can use several processes