Python Main Guard: 5 Powerful Patterns You Must Know
Imports ran a billing job twice and double-charged users.
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
- ✓Writing and running basic Python scripts
- ✓Importing modules and using pip-installed packages
- ✓Running commands with arguments in a terminal
- The main guard (if __name__ == '__main__':) runs code only when the file executes directly, not when imported as a module
- Three parts: import-safe definitions on top, the guard at the bottom, and a main() function holding CLI logic with argparse inside
- Performance insight: unguarded module-level I/O (DB connects, thread pools) runs on every import — one service paid 900ms per worker fork until guarded
- Production insight: an unguarded billing script executed on import during tests and double-charged 214 users before anyone noticed
- Rule: define on import, execute under the guard, keep main() thin, and never put side effects at module top level
Think of a Python file as a cookbook page with recipes (functions) plus a note at the bottom saying 'start cooking now.' When you open the book to read a recipe (import the file), you don't want the oven preheating. The main guard is a label on that note: 'only start cooking if someone opened this exact page to cook — not if they're just reading the recipe from another page.' Imports read; direct runs cook. The guard tells the difference.
You've written a handy script with a function others want to reuse. Someone imports it — and your script's test run fires off, charges test cards, and prints garbage into their logs. The import worked; everything around it broke.
That's the rite of passage behind Python's most-asked beginner question. Module-level code runs on import, unconditionally. Without a guard, your file can't tell 'borrow my function' from 'run my program.'
It's a two-line fix. You'll learn exactly what __name__ holds in each case, the five patterns that cover scripts to packages, and the multiprocessing trap that fires your guard twice if you're careless.
What __name__ Actually Holds in Each Case
Python sets one variable per module load: __name__. Direct run (python billing.py) → '__main__'. Import (import billing) → 'billing'. Package run (python -m shop.billing) → '__main__' again, but with package context for relative imports.
The guard compares that string. Equal → 'I'm the program, run main().' Unequal → 'I'm a library, just define things.' No magic, no framework — a string comparison executed once at load.
Prove it to yourself in 30 seconds: add print(__name__) at a file's bottom, run it directly (prints __main__), then import it from another shell (prints the filename). That experiment ends more confusion than any docs page.
Pattern 1-2: The Standard Guard + main(argv)
The canonical layout: imports and constants up top, function/class defs in the middle, def main(argv=None) holding program logic, and the two-line guard calling it with sys.exit for proper exit codes. argv=None lets tests inject argument lists without touching sys.argv.
Keep main() thin: parse args, configure logging, call one pipeline function, return an int. Business logic lives in testable functions (charge_all(customers, dry_run)), not inside main. Tests import functions; only humans and cron invoke main.
This shape also fixes the pytest-argparse collision: parse_args runs only under the guard, so collection imports never see CLI parsing. Every script in the repo should converge on this skeleton — consistency is a feature during incidents.
Pattern 3: Multiprocessing Without Double Execution
On spawn platforms (Windows, macOS default), each worker re-imports your module. Pool creation at top level therefore executes once per child — spawning workers that spawn workers, recursively, until the box falls over.
The fix is placement: worker functions at top level (importable by children), Pool/Process creation inside main() under the guard. Children import functions without running the program; only the parent runs main.
Linux fork historically masked this — children inherited memory without re-import. As macOS moved to spawn and Linux code goes cross-platform, unguarded process code is a portability landmine. Guard it now regardless of your current OS.
Pattern 4: Packages, -m, and console_scripts
Packages add a wrinkle: python shop/billing.py breaks relative imports, while python -m shop.billing keeps package context and still triggers the guard. Standardize on -m in docs and runbooks so imports behave identically in dev and prod.
For installed tools, skip the guard dance at the call site: setuptools console_scripts / [project.scripts] generates an entry shim calling shop.billing:main. The guard remains in the module (for direct runs), but production invokes the installed command, which is cleaner in Docker images and cron.
Library modules that are never programs need no guard — but also need no side effects. If a library file grows a __main__ block for demos, keep the demo import-light so importing the library stays instant.
Pattern 5: Import-Time Safety Rules That Stick
Three rules make every module safe to import. No I/O at top level: no DB connects, no file writes, no thread pools outside functions. Constants and logging config are fine; connections are not — one service paid 900ms per gunicorn worker fork for a top-level connect.
No argv parsing at top level: parser.parse_args() belongs in main(argv). No heavy computation: precompute lazily or in main, not at import, so tab-completion and test collection stay instant.
Enforce mechanically: a CI test that imports every module in a subprocess with a network-blocked sandbox. If an import opens a socket or takes over a second, the PR fails. Culture follows tooling — the billing team hasn't had an import side effect since the gate landed.
What the Guard Doesn't Do (Myths to Drop)
The guard doesn't make code faster, thread-safe, or import-cycle-proof. Circular imports fail identically with or without it — the fix is restructuring, not gating. It also doesn't scope variables: names defined under the guard are still module globals after a direct run.
It doesn't replace entry points for installed apps, and it doesn't protect against exec(open().read()) tricks. It's one contract — direct-run versus imported — executed once. Expecting more breeds creative misuses like guarding individual functions (pointless) or nesting business logic three guards deep (unreadable).
Respect its size: two lines, one job. Architecture (idempotency keys, dry-run flags, sandbox tests) carries the reliability load the guard can't.
The Import That Double-Charged 214 Users
charge_all() from billing.py for analysis was considered safe. The billing script's author assumed 'nobody imports a script' — it lived in scripts/, not a package, so no review checked for import safety.- No side effects at module level, ever: imports must be safe, and CI should prove it by importing everything.
- Money operations need idempotency keys per business action — guards prevent repeats, keys survive them.
main() under the guard, and keep worker functions importable at top level.parser.parse_args() inside main(argv=None) and call main() only under the guard; tests call functions directly.| File | Command / Code | Purpose |
|---|---|---|
| billing.py | CHARGE_LIMIT = 10_000 # constants at top level: safe | Pattern 1-2 |
| parallel_jobs.py | from multiprocessing import Pool | Pattern 3 |
Key takeaways
Common mistakes to avoid
4 patternsTop-level program code with no guard
Parsing argparse at module level
main(); tests pass explicit lists, never sys.argv.Creating multiprocessing pools at top level
Pool() inside main() under the guard.Expecting the guard to fix circular imports
Interview Questions on This Topic
What does if __name__ == '__main__' do?
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Written from production experience, not tutorials.
That's Basics. Mark it forged?
3 min read · try the examples if you haven't