Running your first Python program end to end means four things: putting the file where the terminal can find it, invoking the right interpreter, reading what comes back, and knowing what to do when something breaks. Most beginners can do the first two within a minute, then stall on the third and fourth. This lesson walks through the whole loop, including the parts of it that nobody mentions until the day you need them.
How do you run a Python file?
Save your code in a file with a .py extension, then hand that file to the interpreter from a terminal. On Windows and Linux the command is python your_file.py; on macOS and Linux, python3 your_file.py is the safer spelling, because python may point at the Xcode tools or may not exist at all.
Three details cause most of the early frustration.
- Your working directory is not where your file is. The terminal starts in your home folder or your profile folder, and relative paths are resolved from there. If you see "No such file or directory", print your location with
python -c "import os; print(os.getcwd())"and navigate to the folder that actually holds the file. - The file name becomes part of your command. A file called
my script.pyneeds quoting, and a script namedtest.pycan collide with other tooling. - Two interpreters, two results. Running
pythonandpython3may execute different versions with different packages installed. Stick to one spelling once you have picked an interpreter.
Two shortcuts are worth knowing. python -c "print(2 ** 10)" runs a one-liner without creating a file, which is perfect for checking a value or a flag. And python -m module_name runs a module as if it were a script, which is how you invoke a package that ships with a __main__ entry point.
Editors do exactly the same thing behind a button. A "run" command in an editor is a terminal command that launches the interpreter with your file, so the behaviour you get is identical; the only difference is that the editor usually opens the terminal for you and, crucially, lets you step through the code. When a program fails, being able to look at its state at the moment it failed is often faster than adding print statements and running it again.
What does a Python program look like from top to bottom?
A script you intend to reuse has a shape worth copying: imports at the top, constants next, functions that do one job each, and a single entry point at the bottom. The entry point is guarded by a condition that looks odd the first time you meet it.
"""A small report generator you can also import from other scripts."""
from datetime import date
APP_NAME = 'daily report'
def build_rows(records):
"""Turn a list of dictionaries into printable rows."""
return [f"{item['name']:<20}{item['count']:>6}" for item in records]
def render(records, today=None):
today = today or date.today()
lines = [f"{APP_NAME} - {today.isoformat()}", '-' * 26]
lines.extend(build_rows(records))
lines.append(f"{'total':<20}{sum(r['count'] for r in records):>6}")
return '\n'.join(lines)
def main():
records = [{'name': 'apples', 'count': 12}, {'name': 'pears', 'count': 7}]
print(render(records))
if __name__ == '__main__':
main()
When you run this file directly, Python sets the module's __name__ to '__main__', so the guard passes and main() runs. When another file does import report, the same file is executed, but __name__ is 'report', the guard fails, and nothing happens except the definitions being created. That single line is what lets other scripts import your work without accidentally running it.
Two more built-in names are useful in the same breath. __file__ holds the path of the script being run, which is how you find files that sit next to it. And sys.argv is the list of command-line arguments, where the first entry is the script name and the rest are what the user typed.
import sys
# sys.argv[0] is the script path; everything after it is user input.
arguments = sys.argv[1:]
name = arguments[0] if arguments else 'world'
print(f'hello, {name}!')
print('raw argv was:', sys.argv)
Run that with no arguments and it greets the world; run it as python greet.py Ada and it greets Ada instead. Notice that the script never crashed when the argument was missing, because it asked the list before indexing into it.
How do you make sense of a Python traceback?
When a program fails, Python prints a traceback: a list of frames, each showing the file, the line number, the source line, and the expression that failed. Beginners read the traceback from the top and get lost in the first frame, which is usually library code nobody needs to touch. Read it from the bottom instead. At the very bottom you find the exception type and its message, and the line above it points at the exact expression that raised it.
import traceback
def divide(numerator, denominator):
return numerator / denominator
try:
divide(10, 0)
except ZeroDivisionError:
# format_exc() gives you the same report an uncaught error would print.
report = traceback.format_exc()
print('caught it. the traceback looked like this:')
print(report.rstrip())
The report ends with ZeroDivisionError: division by zero, and the frame immediately above it shows the line that caused the problem. Once you internalise bottom-up reading, tracebacks become a map rather than a wall of text.
Three error types are worth recognising on sight. NameError means you used a name that does not exist in scope, usually a typo or a missing import. TypeError means the types do not work together, such as adding a string to a number. IndentationError means the block structure is wrong, and it is a very common first-week error because a single stray space changes what Python thinks the block is.
One more category is worth separating from the rest. AssertionError comes from a failed assert statement, which you write yourself to state an expectation that should always hold. Seeing one means the program caught itself breaking its own contract, which is a very different situation from an unexpected crash.
What do exit codes mean, and why should you care?
A process that finishes successfully exits with status 0. Any other status means something went wrong, and the exact number is a convention you control. Shell scripts, task schedulers, and CI pipelines decide whether a job succeeded by reading that number, not by scraping the output text. If your script prints an error and then exits 0, every automated system believes it worked.
import sys
def main():
print('doing the work...')
# Real programs would call sys.exit(1) here when something failed.
return 0
status = main()
print('this process will exit with status:', status)
sys.exit(status)
Calling sys.exit(1) ends the program immediately with a failure status. Calling sys.exit('could not open file') prints that message to the error stream and exits with status 1, which is a convenient way to fail with an explanation attached.
Your turn: run it, break it, then fix it
- Create a folder called
python-lab, put the first program from this page inside it asreport.py, and open a terminal in that folder. Runpython report.pyand confirm the report prints. - From the same folder, run
python -c "import os; print(os.getcwd())"to prove where the terminal is sitting. - Now break it on purpose: change
item['count']toitem['counts']inbuild_rows. Run the script again and read the traceback from the bottom line upwards, naming the error type and the offending expression out loud. - Fix the typo, then deliberately divide by zero in a new line and read the new traceback. Notice that the frame order did not change.
- Finally, import your script from a second file with
import reportand callreport.render([{'name': 'test', 'count': 1}]). It must print the report without running the whole script, which proves the__main__guard is doing its job.
Key takeaways
- Run a file with
python your_file.pyfrom the directory that contains it, and checkos.getcwd()when the file "does not exist". - Guard the entry point with
if __name__ == '__main__':so your script stays importable. - Read a traceback bottom-up: the last line is the error type and message, the line above it is the cause.
- Exit status 0 means success; anything else means failure, and automation trusts that number.
sys.argvholds your command-line arguments, with the script path in position zero.