Watch at EaseStories. Answers. Perspective.

Technology · PRACTICAL GUIDE

Python says “bootstrapping phase”? Prove what spawn imports before moving the guard

This RuntimeError means a fresh child interpreter re-imported the entry module while that import was still starting another process. Record the start method and launcher, move every process-creating path behind the real entry boundary, and retest in a fresh process.

Parent and spawned-child import boundary.
Original technical illustration: map the import boundary, guarded entry and fresh-run acceptance. Watch at Ease

This RuntimeError means a fresh child interpreter re-imported the entry module while that import was still starting another process. Record the start method and launcher, move every process-creating path behind the real entry boundary, and retest in a fresh process.

The safe fix is to make the entry module importable without process creation, then prove the original launcher starts one parent and the intended children exactly once.

1. Freeze the failing launch path

Save the complete traceback from the first RuntimeError, not the repeated child copies. Record Python version, operating system, sys.executable, sys.argv, __name__, __file__ and multiprocessing.get_start_method(). Also note whether the program was launched by a terminal, IDE, notebook, web server, test runner, service or frozen executable.

The topmost application frame before multiprocessing.spawn._check_not_importing_main is the useful clue. It shows which import or function call tried to create a child while a spawned interpreter was still importing the entry module.

2. Separate import work from entry-point work

Definitions, constants and import-safe setup may remain at module scope. Move Pool, Process, Manager, ProcessPoolExecutor, client construction and any library call that can create workers into a main() function, then call main() only under if __name__ == "__main__".

Putting the guard inside a worker helper is too late if the entry module calls that helper at import time. The boundary must stop the child interpreter before it reaches every process-creating call chain. Keep worker targets at module scope so they remain importable and picklable.

3. Prove the start method instead of assuming the platform

Windows uses spawn. Modern macOS defaults to spawn, and Python 3.14 changed the POSIX default away from fork to forkserver where supported. A script that seemed safe under fork can expose recursive imports after an environment or Python upgrade.

Do not force fork merely to hide the error: fork is not available everywhere and can be unsafe in multithreaded processes. Use get_context when a library needs an explicit method, and document that requirement rather than changing the global method deep inside an imported module.

4. Treat frozen apps and third-party pools as separate branches

For a normal interpreter run, the standard-library example says freeze_support can be omitted. PyInstaller overrides it because a frozen child launches the same executable; call it at the guarded entry point, and move it earlier only when imported code uses multiprocessing, parses application arguments or initializes heavy GUI state before diversion.

If the traceback enters a library, confirm whether its option such as n_jobs, workers or processes creates a pool. A one-worker setting is a diagnostic control, not proof of a complete fix. The application entry point still needs to be safely importable when parallelism is restored.

5. Run a same-launcher acceptance test

Terminate the old recursive process tree, then start a fresh run through the same launcher and environment. Confirm the guard is reached only in the parent, worker targets execute only in children, output is produced once, and shutdown returns without orphaned processes.

Keep the traceback, launcher, start method, offending call chain, code movement and fresh-run result in one record. Test both source and packaged forms when both ship; a successful terminal run does not prove an IDE, service or frozen executable follows the same entry path.

Entry guard, frozen-app branch and same-launcher acceptance.
Original technical illustration: map the import boundary, guarded entry and fresh-run acceptance. Watch at Ease