Compile Python 3 Files Safely with py3compile
A slow first request after deploy is often just Python compiling itself on the fly; py3compile does that work up front instead. This is a repeatable way to byte-compile Python 3 source into its normal cache locations, check what came out, and avoid turning a routine build step into an accidental system-wide change. The examples use py3compile from python3-minimal 3.12.3-0ubuntu2.1, as installed on this machine.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about ten minutes. You need a shell, a readable Python source tree, and write access to wherever the caches will land. Ordinary project files need no sudo. A package-wide compile can touch a lot of files, so run it in a proper maintenance or build window and keep a way to identify what changed.
1. Check the installed command
Start read-only, to confirm you are using the Debian wrapper rather than assuming some other Python installation shares its options:
$ command -v py3compile
/usr/bin/py3compile
$ py3compile --version
py3compile 3.12.3-0ubuntu2.1
$ dpkg-query -W -f='${Package} ${Version}\n' python3-minimal
python3-minimal 3.12.3-0ubuntu2.1
Your version may differ. The important point: py3compile is a Debian utility wrapping Python's py_compile module, not the same interface as python -m py_compile or compileall. Script against the options the installed command actually shows you.
Checkpoint
If command -v finds nothing, install or repair the package through your normal system-management process. Do not copy a script from another host into /usr/bin.
2. Compile one known file
Pass a file or directory as the positional argument. This example compiles a single module:
$ py3compile /path/to/project/app.py
$ printf 'exit status: %s\n' "$?"
exit status: 0
On a normal Python 3 install, the cache lands beside the source in a __pycache__ directory, tagged with the interpreter version, such as app.cpython-312.pyc. Check the actual result instead of trusting a remembered suffix:
$ find /path/to/project/__pycache__ -maxdepth 1 -type f -name 'app*.pyc' -print
/path/to/project/__pycache__/app.cpython-312.pyc
The exact tag depends on the Python version on the machine. A bytecode file is a cache, not a replacement for the source: keep the .py file and test the application that will actually use the cache.
3. Compile a whole directory, then check the scope
Give it a directory when several files need compiling:
$ py3compile /path/to/project
$ find /path/to/project -type f -path '*/__pycache__/*.pyc' -print | sort
/path/to/project/__pycache__/app.cpython-312.pyc
/path/to/project/lib/__pycache__/helpers.cpython-312.pyc
Do not read that output as proof that every file under the directory got compiled. Excluded paths, files with unexpected suffixes, syntax errors, permissions and the wrapper's own package rules can all change the result. Use find to check the caches that actually exist against the source tree you meant to process.
There is no need to run as root just because a cache is missing. Check ownership and permissions first:
$ ls -ld /path/to/project /path/to/project/__pycache__
$ test -w /path/to/project && echo 'project directory is writable'
project directory is writable
If the directory belongs to a package or service account, run the operation as that account, or use your distribution's package tooling. A broad sudo py3compile can leave root-owned caches that later stop the application user from updating them.
4. Rebuild stale caches, but only on purpose
By default, up-to-date bytecode is left alone. Add --force when you deliberately need a rebuild even though source timestamps look current:
$ py3compile --force /path/to/project
$ printf 'exit status: %s\n' "$?"
exit status: 0
Use this after a controlled deployment, or on a filesystem with unreliable timestamps. It still writes into the source tree, so inspect the target first. --verbose prints diagnostics such as parsed arguments and selected options:
$ py3compile --verbose --force /path/to/project/app.py
D: py3compile:... argv: [...]
D: py3compile:... options: {... 'force': True, ...}
Line numbers and the full argument dump vary between versions. If the command fails, keep the diagnostic output and the exit status; a quiet terminal is not proof that anything got rebuilt.
5. Exclude files that should never be compiled
Use --exclude, or -X, with a regular expression matched against candidate paths. You can repeat it, so anchor each expression carefully or an exclusion can silently swallow more of the tree than intended:
$ py3compile --verbose \
--exclude '/tests/' \
--exclude '/__init__\.py$' \
/path/to/project
That example is illustrative: swap the expressions for rules that match your own layout. Verify the scope by checking that a cache is genuinely absent under a path you meant to skip, and by reading verbose output if your version reports candidate processing. A pattern that matches a parent directory excludes every child under it too.
Do not use --exclude to paper over a syntax error or a permissions problem. Exclude only files that are genuinely out of scope, such as tests inside a production-only image. Rerun without the pattern if you later need those files back; nothing persistent gets changed, so there is no separate undo step.
6. Understand version selection and package mode
The -V option selects a Python 3 version range for private modules, not an arbitrary interpreter pick. The manual's own examples: 3.1 for one version, 3.1- for that version or newer, 3.1-3.3 for an interval, and -4.0 for every supported 3.x version. Adapt the numbers to what you actually have installed:
$ py3compile -V 3.12 /path/to/project
$ printf 'exit status: %s\n' "$?"
exit status: 0
Without other options, -V can also select public modules matching the range, which is a much wider operation than compiling one project. Check the command's help and your installed Python versions before pointing this at a system tree.
-p, or --package, names a Debian package whose files should be compiled:
$ py3compile --package PACKAGE_NAME
$ printf 'exit status: %s\n' "$?"
exit status: 0
Replace PACKAGE_NAME with a real installed package. This can affect files outside your project and may need write access to system directories, so treat it as administrative work: review the package, use a proper maintenance window, and never guess the name. There is no rollback switch; removing generated caches afterwards is its own, potentially disruptive job.
7. Diagnose failures without hiding them
Stick with default output while you are investigating. --quiet is useful once a script is trustworthy, but it is a distraction on your first pass. --verbose is the better choice when you need to see how arguments were actually interpreted.
If no cache appears, work through the input path, source readability, destination writability, exclusions and the interpreter versions selected by -V. A successful exit status is evidence about that one invocation, not a guarantee that every file in a large tree got processed. Keep the source tree available while you test imports or start the service.
Snapshot the relevant project through your normal release process before replacing a live deployment. The bytecode cache is disposable; overwriting a source tree, package ownership or a service's live files is not a safe experiment. If a test has only changed caches, remove __pycache__ through your project's documented clean command, or restore it from the deployment artefact. Do not delete caches from a shared system tree without checking who else relies on them.
Done means
- Confirmed the version. You checked the installed
py3compileandpython3-minimalversions. - Verified the output. You compiled the intended file or directory and checked the resulting
__pycache__paths. - Used force deliberately. Only for an intentional rebuild, knowing it changes cache files.
- Scoped exclusions carefully. Your exclusion expressions were narrow and checked against the real tree.
- Treated system-wide options as admin work.
-pand broad version ranges got a maintenance window, not a guess. - Kept a way back. Source files, ownership and a rollback path stayed intact.