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 it | sys.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.
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.