Skip to content
System nominalUTC --:--:--Articles 041
MODULE 01 Lesson 02 / 18 30 MIN

Getting a program to run

Running a File and Using the REPL

Run a file, reach for the REPL when you are unsure, and understand why the if __name__ == "__main__" guard exists and what turns an import into the wrong module.

Your environment works and you trust the interpreter. Now you have two ways to hand code to it: save a file and run it, or type at the prompt and get an answer immediately. Most beginners pick one and use it for everything, which is why they spend an hour inserting print calls into a file to inspect one dictionary.

The two modes exist for different questions. A file answers "does this program work?". The prompt answers "what is this value?". When you use the wrong one, debugging turns into a slow loop of edit, save, run, read output, edit again. When you use the right one, half the things you would have written a program to find out are answered in seconds.

There is also a third thing going on, less visible than either mode. A script is a module that Python decided to run. Whichever file you name becomes the entry point, and that decision changes where imports are looked up, what __name__ holds, and what happens when two files want the same name.

Running a file executes it top to bottom once

Save this as greet.py:

def greet(name):
    return f"Hello, {name}."


print(greet("world"))

Then name the file at the prompt:

python3 greet.py
Hello, world.

The interpreter reads the file, compiles it to bytecode in memory, and executes every statement in order. Three consequences follow.

First, defining a function does not run it. def greet binds a name to a function object; the body executes when something calls it. Second, top-level statements run at import time as well, so the print above fires whenever the file is loaded, not only when it is named on the command line. Third, execution stops at the bottom of the file. There is no prompt afterwards, so nothing you typed in the file survives anywhere you can reach it.

That last point is the one that costs time. If the program printed a number you did not expect, you cannot ask it why, because the process is gone. Printing intermediate values into the file is one way to find out. The better way, most of the time, is the prompt.

The __main__ guard separates "run me" from "import me"

Here is the same idea as a small tool, saved as stats_tool.py:

import sys


def word_count(text):
    return len(text.split())


def main(path):
    with open(path) as handle:
        print(word_count(handle.read()))


if __name__ == "__main__":
    main(sys.argv[1])

Run it and it counts the words in a file you name. Import it from another file and nothing is printed. word_count and main are available, and the line that reads sys.argv[1] never executes. That is the entire purpose of the guard, and it explains why the two lines are written the way they are.

Python sets one variable, __name__, in every module it loads. For the file being executed it is the string "__main__". For a file loaded by import, it is the module's name without the .py suffix. So the guard reads as: if I am the program, do the program's job; if I am a library, stay quiet. Verify it rather than taking my word for it:

python3 -c "import stats_tool; print(stats_tool.__name__)"
stats_tool

Without the guard, an import runs main(sys.argv[1]) immediately. With no argument on the command line that raises IndexError: list index out of range from inside a module nobody asked to execute. Read the traceback and you get two frames: the import line that looks wrong, and below it the main(sys.argv[1]) line that actually did it. This is the failure you will meet in a test run, where a test file imports your module and the whole suite dies before a single test starts.

The guard has a second benefit that becomes obvious when the module grows. Program code that lives in main() takes arguments instead of reading globals, which makes it callable from anywhere, including a test.

The REPL answers questions that do not deserve a file

Start the prompt with no arguments:

python3

You get a banner and >>>. Whatever you type is compiled and executed on the spot, and the value of the expression is printed for you. That last part is the difference from a script, where an expression that is not assigned or passed to print disappears silently.

The reason to use it is not that it is fast to type. It is that you can inspect a value before you write code around it. Suppose you need a word frequency count and you are not sure whether Counter keeps insertion order or sorts by count:

>>> from collections import Counter
>>> counts = Counter("abracadabra")
>>> counts.most_common(2)
[('a', 5), ('b', 2)]
>>> type(counts)
<class 'collections.Counter'>

Two lines, and the question is settled. You know the argument most_common(2) returns a list of tuples and that the ranking is by count, so nothing in the program you are about to write has to guess.

Names defined at the prompt stay defined for the session: define a function, test it on three inputs, fix it, test it again. The drawback is that the session dies when you close it and nothing is saved, which is fine for a question and bad for anything you want to keep.

-i and -c cover the two awkward cases

Sometimes a program already did the work and you want to look around inside it. Run the file with -i and the interpreter executes it, then drops you into the prompt with everything still alive:

$ python3 -i stats_tool.py note.txt
8
>>> word_count
<function word_count at 0x107eb31c0>
>>> __name__
'__main__'

This is the tool for the situation where a program printed something surprising and exited: you keep the state and interrogate it. Note that __name__ is '__main__' here even though you are now at a prompt, because the file was the program being run.

-c goes the other way: it runs a string and exits. It is the right choice for one-off jobs that do not deserve a file, and it is worth being precise about what it does to the argument list.

python3 -c "import sys; print(sys.argv)" one two
['-c', 'one', 'two']

Compare that with a file, where the script's own path occupies the first slot:

python3 args.py one two
['args.py', 'one', 'two']

sys.argv[0] is not "the program name" in any abstract sense. It is whatever the interpreter decided to put there: the script path when there is a script, -c when the program arrived as a string, and - when it came in on standard input. If your code reads sys.argv[0] to write a usage message, that is the value your reader will see.

The prompt is not a debugger

For anything with a call stack worth inspecting, python3 -m pdb script.py or your editor's debugger will tell you more than -i can. Use the prompt for values and shapes; use a debugger for flow.

An import resolves against a list you can read

Imports look tidy in source and are decided at runtime. The interpreter walks a list of directories in order and takes the first file that matches. That list is sys.path, and its first entry depends on how you started Python:

How you started itsys.path[0]
python3 script.py The directory containing script.py
python3 -c "..." An empty string, meaning the current working directory
python3 at the prompt An empty string, meaning the current working directory

The script row is the one people get wrong. It is the script's directory that goes on the path, not the directory you were standing in when you typed the command. Run python3 /home/ana/tools/report.py from /etc and /home/ana/tools is searched first, while /etc is not searched at all.

Two things follow. Your own modules import cleanly no matter where the process is started from, which is convenient. And any file in that directory can beat a standard library module to its own name, which is not. Set up a directory containing math.py and mathcheck.py:

# math.py
def square(n):
    return n * n
# mathcheck.py
import math

print(math.sqrt(16))
# running in /home/ana/lab
python3 mathcheck.py
AttributeError: module 'math' has no attribute 'sqrt'
(consider renaming '/home/ana/lab/math.py' since it has the same name as the
standard library module named 'math' and prevents importing that standard library
module)

Nothing is broken. The import succeeded, because a module named math was found on the path, and it happens to be yours. The error is that your module is not the standard library, and the traceback says so once you read past the first line. Recent versions add the hint in brackets; on an older interpreter you get the attribute error alone, which is why the shape of the message is worth recognising rather than memorising.

I have seen this with random.py, stat, token.py and email.py, and the pattern is always the same. The fix is to rename your file, not to reorder the imports. A name from the standard library is not available to you as a module of your own, and choosing one costs you more than the name is worth.

When you are unsure which file an import picked, ask:

python3 -c "import math; print(math.__file__)"

The path it prints is the answer, and it is worth running the moment an import behaves in a way the source does not explain.

Pick the mode that matches the question

My rule, after enough years of getting it wrong: reach for the prompt first, and save a file when you have something you want to keep.

Use the prompt to find out what a function returns, what shape a value has, and whether a method exists. Use a file when the logic has to survive the session, when it runs more than once, and when somebody else has to read it. A subtle case sits in the middle: if you type the same four lines into the prompt for the third time, that is a file.

What the prompt cannot do is tell you whether a program works. Interactive code is tested by the person typing it, one expression at a time, and those tests exist only in your memory once it moves into a file. Writing something at the prompt is a way to learn, not a way to verify.

Lab 25 minutes

Write wordcount.py in a new directory. It should define top_words(text, limit=3), which returns the most common words in the text as a list of (word, count) tuples, comparing words in lower case. Run directly it should read the file named on the command line and print the result; imported it should do nothing.

Put a file with a few paragraphs of text next to it, and check both modes:

python3 wordcount.py notes.txt
python3 -c "import wordcount; print(wordcount.top_words('a b a'))"

Then break it on purpose. Remove the guard and run the second command again, and write down the exception and the line it points at. Put the guard back and confirm the import is silent once more.

Finally, ask the prompt two things before you trust the function: what str.split() does with a line that contains a punctuation mark attached to a word, and what your function returns for an empty string.

Show the solution and the reasoning

A working version, using only the standard library:

import sys
from collections import Counter


def top_words(text, limit=3):
    counts = Counter(text.lower().split())
    return counts.most_common(limit)


def main(path):
    with open(path) as handle:
        for word, count in top_words(handle.read(), limit=5):
            print(count, word)


if __name__ == "__main__":
    main(sys.argv[1])

Import it and the module is silent, which is the whole point of the guard. Run it and you get a ranked list. Remove the guard and the second command fails with IndexError: list index out of range, raised on the main(sys.argv[1]) line: there is no argument because you never passed one, and the import now runs the program's entry point. Notice where the traceback points. The top frame is import wordcount, which is the line that looks wrong, and the bottom frame is inside a file that was never meant to run.

The rest of the design has reasons too. top_words returns a list rather than printing, so main decides the format and a test can check the return value without capturing output. The default limit=3 keeps the interactive call short. Reading the file is left out of top_words because the function is about text, not about paths, and passing a string in the prompt is then trivial.

The two prompt questions are the ones people skip. str.split() with no argument splits on runs of whitespace and knows nothing about punctuation, so "end." and "end" are two different words and your counts will be quietly wrong on real prose. top_words("") returns [], because Counter.most_common does not raise on an empty counter. Both answers take one line at the prompt.

Progress

Mark this lesson done and it stays marked in this browser.