skip to content

What does sys.argv hold when you run python tools/report.py versus python -m tools.report?

level: juniorimportance: must knowfreq 58%

answer

  1. The command line is split in two
  2. Slot zero is a name, not an argument
  3. Interpreter switches never reach your list
  4. -m rewrites slot zero to a file path
  5. sys.orig_argv keeps the untouched line

basics

~10 s

sys.argv[0] is the program name: the script path exactly as typed, or, under -m, the full path of the resolved module file. The program's own options always start at sys.argv[1]; interpreter switches never appear.

solid answer

~40 s

CPython splits the command line at the token that says what to run. Everything before it — `-O`, `-X dev`, `-W error` — is consumed by the interpreter and never reaches `sys.argv`; that token and everything after it become the list. For `python tools/report.py --since=7d`, `sys.argv` is `['tools/report.py', '--since=7d']`: index 0 is the path *as typed*, not resolved or made absolute. For `python -m tools.report --since=7d` it is `['/srv/app/tools/report.py', '--since=7d']` — `runpy` replaces the dotted name with the module file's full path. Either way the program's own arguments begin at index 1, which is why argument parsers default to `sys.argv[1:]`. A token after the split is passed through untouched even if it looks like an interpreter switch.

code

python · 5 lines
python
import sys

print("program name:", sys.argv[0])
print("my options:  ", sys.argv[1:])
print("same file?   ", sys.argv[0] == __file__)

go deeper

for a junior

Recall the shape: index 0 is the program name and your own arguments start at index 1. Be able to say what python tools/report.py --since=7d produces without hesitating, and that Python itself does not parse those arguments for you.

for a middle

Explain the split point. Say which side of the command line the interpreter consumes, why -m rewrites index 0 to the module file's path, and why a -O written after the script name is passed straight through to the program instead.

for a senior

Show the operational judgement: argv[0] is a label, not a location, so never build file paths from it; log sys.orig_argv when you need to reproduce how a service was actually started; know that swapping sys.argv in a test changes nothing else about the process.

for a principal

Own the interface decision. Argue when a tool should be launched as a script path versus with -m, given the different first sys.path entry each one produces, and set the convention for how invocations are recorded so an incident report can reproduce them exactly.

`sys.argv` is a plain list of `str` that CPython fills in during startup, before the first line of your code runs. Element 0 is the **program name**; everything from index 1 onward is the argument vector your program is expected to interpret for itself. Python never parses those arguments, never validates them, and does not care whether they look like switches — it builds the list and steps out of the way. ## The command line has two halves The interpreter reads the command line left to right and consumes its **own** options — `-O`, `-B`, `-u`, `-X dev`, `-W error` and friends — until it meets the thing that says *what to run*: a script path, `-m`, `-c`, or `-`. That token, and everything after it, belongs to your program. So for `python -O -X dev tools/report.py --since=7d`, the interpreter keeps `-O` and `-X dev`, and your program sees `['tools/report.py', '--since=7d']`. The corollary catches people out: a token after the split point is passed through **even if it is spelled like an interpreter option**. `python tools/report.py -O` gives you `['tools/report.py', '-O']` and runs with assertions enabled — the `-O` is yours, not Python's. ## What lands in `sys.argv[0]` The program-name slot depends on how you launched: * **Script path** — `python tools/report.py` puts `'tools/report.py'` in `argv[0]`: the string exactly as typed, relative if you typed it relative. It is not normalised, not resolved through symlinks, not made absolute. Note the asymmetry with `__file__`, which *is* an absolute path. * **`-m`** — `python -m tools.report` does **not** leave the dotted name there. Once `runpy` has located the module it rewrites `argv[0]` to the module file's full path, so the running code sees something like `/srv/app/tools/report.py`. (While the module is still being located, `argv[0]` is briefly the literal `'-m'`; that only matters to import-time hooks.) * **`-c`** — `argv[0]` is the literal string `'-c'`; the code itself is never in the list. * **stdin** — `python - --since=7d` gives `'-'`; piping a program in with no `-` gives `''`, the same empty string you see in an interactive session. In every one of those cases the program's own options start at index 1, which is why argument parsers in the standard library default to reading `sys.argv[1:]` rather than the whole list. ## The companion effect on `sys.path` The launch mode also decides the first entry of `sys.path`, and the two are easy to conflate. Running a script prepends **the script's directory** (an absolute path since 3.11); running with `-m` prepends the **current working directory**, which is what makes `python -m tools.report` importable from the project root while `python tools/report.py` often cannot import its own siblings. `argv[0]` and `sys.path[0]` are set by the same decision but answer different questions: one is a label, the other is an import root. ## Recovering what the interpreter swallowed Because `sys.argv` is the post-split view, it cannot tell you which interpreter switches were used. Three places can. `sys.orig_argv` (added in 3.10) holds the untouched original command line, starting with the executable itself. `sys.flags` exposes the parsed switch state as a named tuple — `sys.flags.optimize`, `sys.flags.dev_mode` and so on. `sys._xoptions` holds the `-X` settings as a dict. Reach for `sys.flags` when you want the effective state and `sys.orig_argv` when you want to reproduce or log the exact invocation. ## Consequences worth knowing `sys.argv` is an ordinary mutable list, and nothing in the interpreter re-reads it after startup. That is why a test can assign a fabricated list to `sys.argv` around a call to a `main()` function and have the program under test behave as if launched that way; mutating it changes nothing else about the process, and `sys.orig_argv` is a separate list that stays as it was. Do **not** use `argv[0]` to find your program's files on disk. It is whatever the caller typed, and the caller might have used a relative path, a symlink, or a wrapper of a different name entirely; a program launched through a shell alias or a multi-call binary may see a name that does not correspond to any file. Use `__file__` or the packaging-aware resource APIs for that. The one legitimate use of the name is the opposite direction: reading it deliberately to change behaviour, the way a single executable can dispatch on the basename it was invoked as, or the way a usage message wants to echo back the command the user actually typed. Finally, `argv[0]` is a label rather than a promise. It tells you what the process was *called*, not reliably where it lives or how it was configured — and knowing which of the two questions you are asking is most of the value of understanding this list.

  • What is sys.argv[0] if you launch code with python -c, or start the REPL with no script at all?
    With `-c` it is the literal string `'-c'` — the code you passed is not in the list at all, and any trailing words become `sys.argv[1:]`. Reading a program from standard input as `python -` puts `'-'` there. With no script name of any kind, including an interactive session, `sys.argv[0]` is the empty string. Code that does `sys.argv[0]` arithmetic on the assumption it is always a filename breaks in all three cases.
  • sys.argv drops the interpreter's own switches. How do you find out which ones were used?
    Three places. `sys.orig_argv`, added in 3.10, is the untouched command line beginning with the executable, so it reproduces the exact invocation. `sys.flags` is a named tuple of the parsed switch state — `sys.flags.optimize`, `sys.flags.dev_mode`, `sys.flags.dont_write_bytecode`. `sys._xoptions` is a dict of the `-X` settings. Use `sys.flags` when you want the effective state and `sys.orig_argv` when you want to log or re-run the command.
  • Can a program assign to sys.argv, and does anything downstream notice?
    Yes — it is an ordinary mutable list, and the interpreter never re-reads it after startup. Tests routinely swap in a fabricated list around a call to a `main()` function so the program behaves as if launched with those arguments. Nothing else about the process changes: the real command line is still in `sys.orig_argv`, and `sys.path[0]` was already decided at startup and is unaffected.

Think of the interpreter as a receptionist who reads the envelope, keeps the postage instructions meant for it, and hands you the letter with the sender's label still on the front.

saying these in an interview costs you the question

  • Thinks sys.argv[0] is the first user-supplied argument
  • Expects interpreter switches like -O to show up in sys.argv
  • Assumes sys.argv[0] is always an absolute, resolved path
  • Believes -m leaves the dotted module name in argv[0]
  • Thinks options placed after the script name are eaten by Python
  • Uses argv[0] instead of __file__ to locate the program's data files

context