7  Script Entry Points and Exit Behaviour

Introduction

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 argparse

def 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 parser

def main(args):
    print(f"Ready to scan {args.domain}")
    print(f"Wordlist: {args.wordlist}")
    return 0

parser = 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:

if __name__ == "__main__":
    parser = build_argument_parser()
    raise SystemExit(main(parser.parse_args()))

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.

import errno
import os
from pathlib import Path

def check_wordlist(path):
    if not Path(path).is_file():
        print(os.strerror(errno.ENOENT))
        return errno.ENOENT
    return 0

exit_code = check_wordlist("missing-wordlist.txt")
print(f"Exit code: {exit_code}")
1
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 argparse

def 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 parser

def 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.

Answer

References