To use the Python main function with arguments, define main() with a parameter for the arguments, then call it under if __name__ == "__main__": and pass it sys.argv[1:], the words after the script name. Its return value goes to sys.exit() as the exit code:
import sys
def main(args):
print("You passed:", args)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
I ran each script here on Python 3.12.5 in a Windows Command Prompt, with different arguments each time, and pasted the real output under each one.
In this tutorial, we’ll build that pattern up step by step: the __name__ guard, reading raw arguments, switching to argparse, the testable main(argv=None) form, and exit codes.
What does if __name__ == “__main__” do for a main function?
Python has no built-in main() like C or Java. It’s just a normal function name. What makes it the entry point is the guard at the bottom of the file, which checks how the file was started.
When you run a file directly, Python sets its __name__ variable to "__main__". When another file imports it, __name__ is the module’s name instead. This tiny file prints which case it’s in:
print(f"helpers.py sees __name__ = {__name__!r}")
def main():
print("helpers.main() ran")
if __name__ == "__main__":
main()
Output:
C:\pyguides> python helpers.py
helpers.py sees __name__ = '__main__'
helpers.main() ran
C:\pyguides> python -c "import helpers"
helpers.py sees __name__ = 'helpers'

'__main__' and calls main(); imported, it sees 'helpers' and skips it.So the guard lets one file act as a script and as an importable module. If running files from the terminal is new to you, my guide on how to run a Python script covers the setup.
The import half of that test is the same thing that happens when you import a Python file from the same directory: top-level code runs, but the guarded block doesn’t.
Read command-line arguments in main() with sys.argv
sys.argv is a list of strings. Item 0 is the script name, and everything after it is what you typed after the name, split on spaces. Quotes keep a value with a space together:
import sys
def main():
print(f"Script name: {sys.argv[0]}")
print(f"Arguments: {sys.argv[1:]}")
print(f"Count: {len(sys.argv) - 1}")
if __name__ == "__main__":
main()
Output:
C:\pyguides> python show_args.py
Script name: show_args.py
Arguments: []
Count: 0
C:\pyguides> python show_args.py sales.csv 2026-10-20 --verbose
Script name: show_args.py
Arguments: ['sales.csv', '2026-10-20', '--verbose']
Count: 3
C:\pyguides> python show_args.py "New York" 25
Script name: show_args.py
Arguments: ['New York', '25']
Count: 2

"New York" keeps it as one argument, and 25 arrives as a string.Notice that '25' and '2026-10-20' are strings. sys.argv never converts anything, and a flag like --verbose is just one more string in the list.
For a side-by-side look at sys.argv, argparse and flag handling as a general CLI topic, see Python command-line arguments. Here, I’ll stay focused on how main() receives them.
Pass sys.argv[1:] into main() as a parameter
Reading sys.argv deep inside your code makes it hard to test. A cleaner habit is to give main() a parameter and pass the arguments in from the guard. This order-total script also converts each price and returns an exit code:
import sys
def main(args):
if not args:
print("Usage: python order_total.py PRICE [PRICE ...]")
return 1
prices = [float(a) for a in args]
print(f"Items: {len(prices)}")
print(f"Total: ${sum(prices):,.2f}")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
Output:
C:\pyguides> python order_total.py 19.99 5.25 3
Items: 3
Total: $28.24
C:\pyguides> python order_total.py
Usage: python order_total.py PRICE [PRICE ...]
C:\pyguides> echo %ERRORLEVEL%
1
C:\pyguides> python order_total.py 19.99 ten
Traceback (most recent call last):
File "C:\pyguides\order_total.py", line 13, in <module>
sys.exit(main(sys.argv[1:]))
^^^^^^^^^^^^^^^^^^
File "C:\pyguides\order_total.py", line 7, in main
prices = [float(a) for a in args]
^^^^^^^^
ValueError: could not convert string to float: 'ten'
Three prices give a $28.24 total. With no arguments, main() prints a usage line and returns 1, which echo %ERRORLEVEL% confirms. That’s the exit code Windows saw.
The last run shows the weak spot: float('ten') raises a ValueError traceback. You’d have to validate every value yourself, as in checking if a string is an integer or float, or let argparse do it.
Converting text to numbers is covered in converting a string to a float in Python, and the integer version works the same way with int().
Use argparse in the Python main function for named options
argparse ships with Python. You describe the arguments once, and it converts types, fills in defaults, writes the help text and rejects bad input. Here’s a bill-splitting script with one required value and three options:
import argparse
import sys
def build_parser():
parser = argparse.ArgumentParser(description="Split a restaurant bill, tip included.")
parser.add_argument("bill", type=float, help="bill total in USD, before tip")
parser.add_argument("-t", "--tip", type=int, default=18, help="tip percent (default: 18)")
parser.add_argument("-p", "--people", type=int, default=1, help="number of people (default: 1)")
parser.add_argument("-v", "--verbose", action="store_true", help="also show the tip amount")
return parser
def main(argv=None):
args = build_parser().parse_args(argv)
tip = args.bill * args.tip / 100
share = (args.bill + tip) / args.people
if args.verbose:
print(f"Tip ({args.tip}%): ${tip:.2f}")
print(f"Each of {args.people} pays: ${share:.2f}")
return 0
if __name__ == "__main__":
sys.exit(main())
Output:
C:\pyguides> python tip_split.py 84.50
Each of 1 pays: $99.71
C:\pyguides> python tip_split.py 84.50 --tip 20 --people 3 -v
Tip (20%): $16.90
Each of 3 pays: $33.80

-v to print the tip line.A few things are going on. bill is a positional argument, so it’s required. --tip and --people are options with short forms -t and -p, and type=int converts them for you.
action="store_true" makes -v an on/off flag: it’s False unless you type it. The parsed values come back as attributes, like args.bill and args.people.
Defaults on the command line work like default parameters on a function, which I cover in Python functions with optional arguments.
The free –help page and error messages
You didn’t write a help screen, but argparse built one from the help= strings. It also refuses a bill that isn’t a number, before your code runs:
import argparse
import sys
def build_parser():
parser = argparse.ArgumentParser(description="Split a restaurant bill, tip included.")
parser.add_argument("bill", type=float, help="bill total in USD, before tip")
parser.add_argument("-t", "--tip", type=int, default=18, help="tip percent (default: 18)")
parser.add_argument("-p", "--people", type=int, default=1, help="number of people (default: 1)")
parser.add_argument("-v", "--verbose", action="store_true", help="also show the tip amount")
return parser
def main(argv=None):
args = build_parser().parse_args(argv)
tip = args.bill * args.tip / 100
share = (args.bill + tip) / args.people
if args.verbose:
print(f"Tip ({args.tip}%): ${tip:.2f}")
print(f"Each of {args.people} pays: ${share:.2f}")
return 0
if __name__ == "__main__":
sys.exit(main())
Output:
C:\pyguides> python tip_split.py --help
usage: tip_split.py [-h] [-t TIP] [-p PEOPLE] [-v] bill
Split a restaurant bill, tip included.
positional arguments:
bill bill total in USD, before tip
options:
-h, --help show this help message and exit
-t TIP, --tip TIP tip percent (default: 18)
-p PEOPLE, --people PEOPLE
number of people (default: 1)
-v, --verbose also show the tip amount
C:\pyguides> python tip_split.py lunch
usage: tip_split.py [-h] [-t TIP] [-p PEOPLE] [-v] bill
tip_split.py: error: argument bill: invalid float value: 'lunch'
C:\pyguides> echo %ERRORLEVEL%
2

--help lists every argument with its help text; the word lunch is rejected as an invalid float and the exit code is 2.argparse exits with code 2 for usage errors and 0 after --help. Both happen inside parse_args() by raising SystemExit, so your main() code after that line never runs.
The main(argv=None) pattern: a Python main function you can test
Look again at def main(argv=None) in the tip script. parse_args(None) reads sys.argv[1:], so the command line works as usual. But you can also pass a list of strings, exactly as if they’d been typed:
from tip_split import build_parser, main
args = build_parser().parse_args(["120", "--people", "4"])
print("Parsed:", vars(args))
print("Exit code:", main(["120", "--people", "4"]))
assert main(["60", "-t", "15"]) == 0
print("Tests passed")
Output:
Parsed: {'bill': 120.0, 'tip': 18, 'people': 4, 'verbose': False}
Each of 4 pays: $35.40
Exit code: 0
Each of 1 pays: $69.00
Tests passed
Importing tip_split didn’t start the program, thanks to the guard. Then I called main() twice with my own argument lists, and the assert checked its return value. No subprocess, no terminal needed.
That’s why I write almost every script this way. Tests stay plain function calls, and an assert or a pytest test can check the result directly.
Keeping build_parser() in its own function helps too: a test can par