未翻訳のページ
このページはまだ日本語に翻訳されていないため、英語の原文を表示しています。 翻訳に協力する
py_compile Module Complexity¶
The py_compile module compiles one Python source file to a bytecode (.pyc) file. A call reads
the whole source, compiles it, and writes the result: nothing is cached between calls, so compiling
a file twice does the work twice.
n is the bytes in the source file being compiled. The parse tree, the code object and the
serialized bytecode are each proportional to it, so a compile holds O(n) while it runs and nothing
afterwards. Filesystem operations - a stat, creating the cache directory, the atomic rename - are
priced at O(1).
Complexity Reference¶
compile¶
| Operation | Time | Space | Notes |
|---|---|---|---|
py_compile.compile(file, cfile=None, dfile=None, doraise=False, optimize=-1, invalidation_mode=None, quiet=0) |
O(n) | O(n) | Returns the path written, or None on a compilation error; doraise=True raises instead unless quiet=2 |
py_compile.main() |
O(total n) | O(largest n) | python -m py_compile; compiles the named files in order, one at a time, and exits 1 at the first failure. The list of names is held as well |
PycInvalidationMode¶
| Operation | Time | Space | Notes |
|---|---|---|---|
py_compile.PycInvalidationMode |
O(1) | O(1) | Enum of the three modes below |
PycInvalidationMode.TIMESTAMP |
O(1) | O(1) | Records the source's mtime and size; the default unless SOURCE_DATE_EPOCH is non-empty |
PycInvalidationMode.CHECKED_HASH |
O(1) | O(1) | Records a hash of the source, checked when import loads the .pyc; hashing adds an O(n) pass, cheap next to compiling. The default when SOURCE_DATE_EPOCH is non-empty |
PycInvalidationMode.UNCHECKED_HASH |
O(1) | O(1) | Records the hash; import does not check it by default |
Exceptions¶
| Operation | Time | Space | Notes |
|---|---|---|---|
py_compile.PyCompileError |
O(e) | O(e) | e = length of the error message, formatted once when the exception is constructed; .msg, .file, .exc_type_name and .exc_value are then O(1) attributes |
Compiling Python Files¶
Basic Compilation¶
By default the bytecode goes to the PEP 3147 location under __pycache__, named for the running
interpreter; cfile puts it exactly where you ask.
import importlib.util
import py_compile
import sys
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as work:
module = Path(work) / 'module.py'
module.write_text('value = 1\n')
written = py_compile.compile(module) # O(n)
tag = sys.implementation.cache_tag
assert written == str(Path(work) / '__pycache__' / f'module.{tag}.pyc')
target = Path(work) / 'module.pyc'
assert py_compile.compile(module, cfile=target) == target # O(n)
assert target.read_bytes()[:4] == importlib.util.MAGIC_NUMBER
Reporting Failures¶
On a compilation error compile() returns None and writes the error message to stderr.
doraise=True raises PyCompileError instead - but only when quiet is below 2: quiet=2
swallows the error entirely, so a build step asking for both gets neither the exception nor the
message. quiet=0 and quiet=1 behave the same inside compile().
doraise governs compilation errors only. A missing source file raises FileNotFoundError, and a
cfile that is a symlink or a non-regular file raises FileExistsError, whatever the flags.
import contextlib
import io
import py_compile
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as work:
broken = Path(work) / 'broken.py'
broken.write_text('def (\n')
stderr = io.StringIO()
with contextlib.redirect_stderr(stderr):
assert py_compile.compile(broken) is None # O(n)
assert 'SyntaxError' in stderr.getvalue()
try:
py_compile.compile(broken, doraise=True)
except py_compile.PyCompileError as error:
assert error.file == broken # the path as passed
assert error.exc_type_name == 'SyntaxError'
else:
raise AssertionError('doraise did not raise')
# quiet=2 wins over doraise: no exception, and nothing written
stderr = io.StringIO()
with contextlib.redirect_stderr(stderr):
assert py_compile.compile(broken, doraise=True, quiet=2) is None
assert stderr.getvalue() == ''
try:
py_compile.compile(Path(work) / 'absent.py', doraise=True, quiet=2)
except FileNotFoundError as error:
assert error.filename.endswith('absent.py')
else:
raise AssertionError('a missing source was compiled')
Choosing How Bytecode Is Invalidated¶
TIMESTAMP records the source's modification time and size, so the import check is a stat. It
misses an edit that changes neither - which a checkout, a restore, or a generator writing
same-length content can produce. CHECKED_HASH records a hash of the source and import re-hashes
the source to check it, which notices any edit. UNCHECKED_HASH records the hash and, by default,
import does not check it - for deployments where the source cannot change.
The default is not fixed: when SOURCE_DATE_EPOCH is set to a non-empty value, compile() uses
CHECKED_HASH, so a reproducible build gets hash invalidation without asking. The mode is
recorded in the flags word of the .pyc header (PEP 552).
import os
import py_compile
import tempfile
from pathlib import Path
modes = py_compile.PycInvalidationMode
def flags(pyc):
return int.from_bytes(Path(pyc).read_bytes()[4:8], 'little')
with tempfile.TemporaryDirectory() as work:
module = Path(work) / 'module.py'
module.write_text('value = 1\n')
for mode, expected in [
(modes.TIMESTAMP, 0), # mtime and size
(modes.CHECKED_HASH, 3), # hash, checked on import
(modes.UNCHECKED_HASH, 1), # hash, not checked
]:
target = Path(work) / f'{mode.name}.pyc'
py_compile.compile(module, cfile=target, invalidation_mode=mode) # O(n)
assert flags(target) == expected
os.environ.pop('SOURCE_DATE_EPOCH', None)
assert flags(py_compile.compile(module)) == 0 # TIMESTAMP by default
os.environ['SOURCE_DATE_EPOCH'] = '1700000000'
assert flags(py_compile.compile(module)) == 3 # CHECKED_HASH by default
Compiling From the Command Line¶
python -m py_compile compiles each named file in turn, or the names read from stdin when the
only argument is -. It stops at the first file that fails, so the files after it are not
compiled.
import subprocess
import sys
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as work:
good = Path(work) / 'good.py'
broken = Path(work) / 'broken.py'
later = Path(work) / 'later.py'
good.write_text('value = 1\n')
broken.write_text('def (\n')
later.write_text('value = 2\n')
result = subprocess.run(
[sys.executable, '-m', 'py_compile', good, broken, later],
capture_output=True, text=True,
) # O(total n) up to the first failure
assert result.returncode == 1
assert 'SyntaxError' in result.stderr
cache = Path(work) / '__pycache__'
assert [p.name.split('.')[0] for p in cache.iterdir()] == ['good']
Common Patterns¶
Compiling Several Files¶
Each call is independent, so a batch costs the sum of its files. compileall does the same over a
directory tree and can spread it across processes.
import py_compile
import tempfile
from pathlib import Path
with tempfile.TemporaryDirectory() as work:
files = []
for name in ('script1.py', 'script2.py', 'module.py'):
path = Path(work) / name
path.write_text('value = 1\n')
files.append(path)
written = [py_compile.compile(f, doraise=True) for f in files] # O(total n)
assert all(Path(pyc).is_file() for pyc in written)
Performance Best Practices¶
✅ Do:
- Pass
doraise=True(withquietbelow 2) in a build step, so a syntax error fails it instead of returningNone - Use
CHECKED_HASHwhere sources can change without moving their mtime; the hash is an extra O(n) pass that compiling already dominates - Use
compileallfor a whole tree rather than walking it yourself
❌ Avoid:
doraise=Truetogether withquiet=2- the error is silently droppedUNCHECKED_HASHfor sources that can change: import keeps serving the old bytecode- Recompiling an unchanged file on every run; nothing is cached, so each call costs O(n) again
Version Notes¶
- All Python 3:
quiet=2suppresses the exception thatdoraise=Trueasks for
Related Modules¶
- compileall - compiles every file in a directory tree
- importlib - the import system that reads and validates
.pycfiles - marshal - the serialization format inside a
.pyc - ast - parsing source without producing bytecode