Read this as a study guide instead

Scripting the system

Argv, exit codes, subprocesses: programs that drive other programs.

Code with its results

A notebook-style walk through the idea — every output shown is the real result of the code above it.

Scripting: the guard, a main(), and an exit code

Structure a runnable script: gate the entry with the __main__ check, put the work behind a main() you call with explicit arguments, and return an exit code.

Real output Every Out block below was produced by running the code above it. You can copy these cells into your own python3 and run them top to bottom to see the same numbers.

A script is a program you launch from the shell. The same .py file can be imported like a library or run directly, and if __name__ == "__main__": is how one file does both: Python sets a module's __name__ to "__main__" only on a direct run, so the guarded block runs then and is skipped on import.

In [1]
for module_name in ["__main__", "toolbox"]:
    if module_name == "__main__":
        print(module_name, "-> run main()")
    else:
        print(module_name, "-> imported, skip")
Out [1]
__main__ -> run main()
toolbox -> imported, skip

A clean script keeps the reusable work in named functions and the orchestration in one main entry point. Calling main with an explicit argument list — rather than reading it from outside — is what makes the same logic runnable here and testable anywhere.

In [2]
def celsius_to_f(c):
    return c * 9 / 5 + 32

def main(readings):
    for c in readings:
        print(celsius_to_f(c))

main([0, 100])
Out [2]
32.0
212.0

A script tells the shell how it went with an exit code: 0 means success and any nonzero value is a specific failure the shell can branch on. Let main return that code; the outermost launcher hands it to the operating system with sys.exit.

In [3]
def main(args):
    if not args:
        print("no input given")
        return 1
    print("handled", len(args), "items")
    return 0

print(main([]))
print(main(["a", "b", "c"]))
Out [3]
no input given
1
handled 3 items
0

The same ideas, as prose

These are the exact fragments the model serves — also available as an ordered study guide.

A program you run, and one that runs others

A script is a program you run to get a job done. You write the steps once in a file, and from then on you carry them out by name instead of by hand like a written errand list you hand to an assistant: the steps get carried out in order, unattended, instead of you doing each one by hand. The work might be renaming a folder of files, fetching and reshaping some data, or kicking off a nightly build — anything you would otherwise do the same way over and over, a computer will do the same way every time, without getting bored on the fortieth run.

The second half of scripting is the more powerful half: a script can drive other programs. It launches them, waits for them, reads what they printed, and checks whether they succeeded — then decides what to do next. That is what the concept's own name points at: programs that drive programs. A build script that runs a compiler, then a test runner, then a packager is not doing the compiling itself; it is the conductor, not the orchestra.

To be a script rather than a loose pile of code, a file needs a few plain conventions: a way to be launched from the shell, a way to read the arguments you hand it, an entry point that runs only when you run the file directly, and a way to report back whether it worked. The rest of this concept is those conventions, one at a time.

From a file to a command

The plainest way to run a Python file is to hand it to the interpreter: python app.py from the shell. The shell finds the python program, starts it, and tells it to execute the file app.py. Nothing about the file has to be special — it is a text file the interpreter reads top to bottom. This is the form to reach for first, because it works the same on every machine that has Python installed.

You can also make the file run on its own, as ./app.py, so it feels like any other command. Two things make that work. The first line of the file is a shebang — #!/usr/bin/env python3 — a line beginning with #! that names the interpreter the system should use; env python3 finds Python on your PATH instead of hard-coding where it lives. The second is the file's executable bit, a permission flag that marks the file as something meant to be run. With both in place, typing ./app.py tells the shell to launch it through the interpreter the shebang named.

The distinction is small but worth holding onto: python app.py runs the file with an interpreter you chose explicitly, while ./app.py runs it with the one baked into the file. Both end in the same place — your code executing — which is where the next conventions pick up.

if __name__ == "__main__"

if __name__ == "__main__" run directly python app.py __name__ = "__main__" imported import app __name__ = "app" true main() runs false main() skipped
Running app.py directly sets __name__ to the string __main__ so the guard is true and main() runs; importing it sets __name__ to the module name so the guard is false and main() is skipped.

Every Python module has a name, kept in the variable __name__. The trick worth knowing is that its value depends on how the file got loaded. When you run a file directly — python app.py — Python sets that file's __name__ to the string "__main__". When some other file imports it instead, __name__ is the module's own name, "app". Same file, two different values, decided entirely by how it was reached.

That single fact is what the line if __name__ == "__main__": is built on. Code inside that block runs only when the file is the one you launched, and is skipped when the file is imported. So a file can be two things at once: an importable library, where another program pulls in its functions and the guarded block stays quiet, and a runnable script, where launching it directly trips the guard and the entry point fires.

Without the guard, any code sitting at the top level of the file would run every time the file is imported — a surprise for anyone who only wanted to borrow one function from it. Putting the "actually do the work" part behind the guard keeps import quiet and side-effect-free, and reserves the launch for the person who typed the file's name on purpose.

Reading the command line

you type python (not in argv) greet.py Ada 3 str str still str sys.argv = argv[0] "greet.py" argv[1] "Ada" argv[2] "3"
The command python greet.py Ada 3 becomes sys.argv: argv[0] is the script name greet.py and argv[1] and argv[2] are the arguments Ada and 3, every element a string; the interpreter python is not part of argv.

A script earns its keep by taking input, and the first place input arrives is the command line itself. When you type python greet.py Ada 3, the shell splits that line on spaces into separate words before Python even starts. Python hands them to your code as a list called sys.argv: the first element, sys.argv[0], is the script's own name, and the rest — sys.argv[1:] — are the arguments you passed, in order. Every element is a string, so a numeric argument like 3 arrives as the text "3", not the number; converting it is your job.

Reading sys.argv by hand is fine for one or two arguments, but it gets unpleasant quickly: you count positions, check how many were given, and produce your own error messages when someone gets it wrong. The argparse module in the standard library exists to take that over. You declare the arguments and options a script accepts — their names, types, and defaults — and argparse reads sys.argv for you, converts the values, fills in defaults, rejects bad input with a usage message, and even generates a --help listing.

The payoff is a script that documents itself and fails clearly. A caller who forgets a required argument gets a specific complaint instead of a confusing crash deep in your code, and the next person to read the script can see, at the top, exactly what it expects. This is code you would run on your own machine to feel it; in the browser there are no command-line arguments to read, so treat the argv and argparse snippets here as things to try in a real terminal.

How a script reports success

main() returns a code int sys.exit (code) to OS shell $? = 0 success not 0 failure
main() returns an exit code that sys.exit hands to the operating system; the shell reads it as $? and branches: 0 takes the success path, any nonzero value the failure path.

When a program ends, it hands its parent a small integer: the exit code. The convention is old and universal — 0 means success, and any nonzero value means a specific kind of failure like a short stamped verdict left on a finished job — a plain "done", or a specific problem code — that the next step reads before deciding whether to go on. A script that finishes its work returns 0; one that could not find its input file might return 1, and one that was given bad arguments something else. The number is the whole message, so a caller can tell not just that something failed but, if the script is careful, which thing.

The shell keeps the last command's exit code in the variable $?, and it uses that code to decide what happens next. Chaining with && runs the second command only if the first returned 0; || runs it only if the first failed. This is why exit codes matter beyond your own program: they are how one script's success or failure becomes another script's branch. A build that stops the moment a step fails is just exit codes being read and obeyed.

In Python, returning a value from a function does not set the exit code by itself. The tidy pattern is to have your entry point return the code as an ordinary integer, and let the outermost line hand it off: sys.exit(main()). sys.exit(0) ends the program successfully, and sys.exit(1) ends it with a failure the shell will see. Keeping the decision inside main and the hand-off at the very edge is what keeps a script both testable and honest about how it went.

Programs that drive programs

The moment a script needs to do something Python does not do itself — compile code, convert a video, query a database with a vendor's own tool — it stops working alone and starts driving another program. The standard-library subprocess module is how. subprocess.run(["ls", "-l"]) launches ls as a child process, waits for it to finish, and comes back with a result you can inspect. The program you called runs with its own process, exactly as if you had typed it into the shell yourself.

What comes back is the useful part. The result object carries the child's exit code in its returncode field — the same 0-is-success convention, now readable inside your own program — so your script can branch on whether the other program worked. Ask for it and you also get the child's captured output as text, which your script can parse and act on. Driving a program is a full conversation: you hand it arguments, it runs, and it reports back a status and some output you read.

This is the shape behind every tool that orchestrates other tools. A deployment script that runs a test suite, checks its exit code, and only then calls the packager is doing nothing more exotic than launching child processes and reading what they return. Because every launched program is its own process, the ideas from working with processes carry straight over: your script is the parent, and the tools it runs are its children.

The glue that automates the boring parts

A script that grows past a few lines wants a shape, and the shape is the same one every time. Keep the reusable work in named functions, put the orchestration — the order things happen in — inside a single main, and let the if __name__ == "__main__": guard call main when the file is run directly. That structure is what keeps a script from calcifying into a wall of top-level code: the functions can be imported and tested on their own, main can be called with made-up arguments to check it, and the guard reserves the actual launch for a real run.

This is why scripting sits under so many builds. An agent pipeline is, at bottom, a runner script: a main that reads its configuration, calls each step in turn, checks whether each one succeeded, and exits with a code that says whether the whole run held together. A remote-control car's controller is the same skeleton wearing different clothes — a main loop that reads sensor input, computes the next throttle and steering values, and drives the hardware, wrapped in a script you launch on the device. Neither is a new idea; both are functions, a main, a guard, and exit codes.

Scripting is the glue layer, and glue is underrated. It is the difference between a set of capabilities that each work and a system that runs itself — the errand run without you standing over it, the pipeline that reports its own success, the job that fails loudly instead of silently. Get the conventions in this concept solid and most of the automation you will ever write is just these few moves, combined.