In this lesson, you’ll give the command-line script a clean entry point and a simple success-or-failure signal. The parser should collect input, the main() function should do the work, and the script entry point should join those pieces together.
This is where the command centre starts to look like a real .py file instead of a collection of notebook examples. Before we run that code in a script, we want to put another ✨ magical ✨ function-shaped pattern at the very bottom of the file: the __name__ guard.
Concepts Covered
main() functions
The if __name__ == "__main__" guard
Returning exit codes
Friendly error messages for expected failure paths
Why This Matters
A tool that exits clearly is easier to use in terminals, shell scripts, CI jobs, and troubleshooting notes. It also becomes easier to test because parsing and behaviour are separated.
Objective
Wrap the parser in a small entry-point pattern and return 0 when the tool succeeds.
Keep Parsing Separate from Running
A useful script shape is build_argument_parser() plus main(args). The parser handles command-line text, while main() receives already-parsed values and focuses on behaviour.
The last thing we need to do is create a variable for the parsed information to be stored in, then pass that object into main(). In a notebook we make the input explicit with parse_args(["example.test"]); in a script, parse_args() can read the user’s real command-line input.
import argparsedef build_argument_parser(): parser = argparse.ArgumentParser(prog="bernauer") parser.add_argument("domain", help="domain to test, e.g. example.test") parser.add_argument("-w", "--wordlist", default="data/wordlist.txt")return parserdef main(args):print(f"Ready to scan {args.domain}")print(f"Wordlist: {args.wordlist}")return0parser = build_argument_parser()args = parser.parse_args(["example.test"])exit_code = main(args)print(f"Exit code: {exit_code}")
1
Accept parsed arguments instead of reading terminal input inside main().
2
Return 0 to signal that the program completed successfully.
Ready to scan example.test
Wordlist: data/wordlist.txt
Exit code: 0
Here’s what’s happening: main() now receives a tidy object with the values it needs. It doesn’t need to know whether those values came from a terminal, a notebook example, or a test. That separation looks small now, but it keeps future tool code much easier to reason about.
The __name__ Guard
In a real .py file, use the __name__ guard to run the script only when the file is executed directly:
When the Python interpreter reads a source file, it configures a number of special variables, most of which we don’t need to worry about. In this case we care about __name__. When your file is the main program, Python sets __name__ to the hard-coded string "__main__". When the same file is imported by another module, Python uses the module name instead.
Run your application from the terminal. If everything went the way we expected, you probably got your normal printed output followed by an exit status of 0. raise SystemExit(...) turns the value returned by main() into the process exit code.
Clear Failure Codes
Exit codes don’t need to be elaborate at this stage. Start with 0 for success and a small non-zero code for an expected failure, such as a missing input file.
On POSIX systems like Linux and macOS, the standard exit code is 0 for success and a number from 1 to 255 for anything else. That doesn’t mean we need to invent 255 different kinds of sadness, because errno, Python’s standard error number system, has conveniently done it for us!
Consistent error numbers are useful because they give humans, shell scripts, tests, and other programs a shared way to understand what went wrong. If a missing file always maps to the same value, the caller can respond predictably: print a helpful message, skip a step, retry later, or fail fast without needing to scrape whatever text happened to land in the terminal.
Check the expected failure before trying to use the file.
2
Return a stable non-zero code for the missing-file case.
No such file or directory
Exit code: 2
Here’s what’s happening: missing files are expected in this tool, so when one doesn’t exist we need to provide a failure path. Returning a clear code lets the rest of the program stop cleanly instead of stumbling forward without input.
Checkpoint
Refactor your script so it has three pieces:
build_argument_parser()
main(args)
an if __name__ == "__main__" block
For now, main(args) can print the parsed domain and wordlist, then return 0. Run the script normally, then run it with -h or --help. What output comes from your code, and what output does argparse generate for you?
import argparsedef build_argument_parser(): parser = argparse.ArgumentParser(prog="bernauer") parser.add_argument("domain", help="domain to test, e.g. example.test") parser.add_argument("-w", "--wordlist", default="data/wordlist.txt")return parserdef main(args):# Print the parsed values, then return 0.pass# In a .py file, finish with:# if __name__ == "__main__":# raise SystemExit(main(build_argument_parser().parse_args()))
Extension Activity
Return 2 when the configured wordlist path doesn’t exist. Keep the success path returning 0. If you’re feeling curious, look up what errno.ENOENT means and why it’s a nice little named constant instead of a magic number.