-c is not the prompt¶
Level: 101 → 201 · for Python programmers
One line: python3 -c 'chr(0x20AC)' prints nothing and exits 0, because -c runs its program as a script and only the >>> prompt shows a value you did not print — and the same line without its quotes never reaches Python at all, because the shell reads it first.
At the >>> prompt, chr(0x20AC) answers '€'. Hand the same text to -c and nothing comes back — no output, no error message, and an exit status of 0:
The second line is the mistake this page is built on — made for real, in the terminal session reproduced further down — and it is not an error. The program ran, built the euro sign, and threw it away. -c runs its program the way Python runs a file: as a script, and a script does nothing with the value of a bare expression.
Python compiles every piece of source in one of three modes before running it. 'exec' is a module — a file, the -c argument, a pipe into python3 -. 'eval' is a single expression, whose value goes back to whoever asked. 'single' is one statement typed at >>>, and it is the only mode that hands the value of an expression statement to sys.displayhook, which is what prints it — as repr(), which is why the prompt's answer has quotes on it (repr is not str, section 8). So the prompt is not a place where Python is more helpful. It is a compile mode, and -c never asks for it.
The program below starts a fresh python3 for every command it shows, so each row is what a shell would have got back. It runs every child with -E -s -X utf8 — ignore PYTHON* variables, skip the user site-packages, print UTF-8 — so nothing in your environment can change an answer, and none of the three changes what -c does. Section 3 writes one file, into a temporary folder it deletes afterwards.
Verified output of dash_c_is_not_the_prompt_py.py — regenerated by tools/run_examples.py, never hand-typed.
-c IS NOT THE PROMPT
Each command starts a fresh python3, and the columns are what it wrote.
(Every one also gets -E -s -X utf8, so nothing in your environment can
change an answer. None of the three changes what -c does.)
1. THE SAME LINE, FOUR WAYS IN
command stdout exit
-----------------------------------------------------
python3 -c 'chr(0x20AC)' '' 0
echo 'chr(0x20AC)' | python3 - '' 0
echo 'chr(0x20AC)' | python3 -i -q "'€'\n" 0
python3 -c 'print(chr(0x20AC))' '€\n' 0
The prompt wrote its two '>>> ' to stderr, not stdout: '>>> >>> \n'
All four computed the euro sign, and only the prompt showed it
without being asked. -i starts the prompt even when the input is
a pipe; the other three ran the same line as a SCRIPT, and a
script does nothing with the value of a bare expression. It is
computed, then dropped -- exit 0, because nothing went wrong.
print() is the one spelling that works all four ways.
The prompt's quotes are part of what it printed: it shows repr()
of a value, where print() shows str().
2. THE DIFFERENCE IS A COMPILE MODE
src = 'chr(0x20AC)'
exec(compile(src, '<string>', 'exec')) prints ''
exec(compile(src, '<string>', 'single')) prints "'€'\n"
eval(compile(src, '<string>', 'eval')) returns '€'
code.InteractiveInterpreter().runsource(src) prints "'€'\n"
Python compiles source in one of three modes before running it.
'exec' is a module: a file, the -c argument, a pipe into python3 -.
'eval' is one expression, and hands its value back to whoever
asked. 'single' is one statement typed at >>>, and it is the only
mode that passes the value of an expression statement to
sys.displayhook -- which is what prints it. The standard library's
own prompt, the code module, compiles in 'single' too. -c never does.
3. -c IS A SCRIPT IN EVERY OTHER WAY TOO
python3 -c 'import sys; print(sys.argv)' a -v
['-c', 'a', '-v'] '-c' stands where a script's name would
python3 -c 'print(__name__)'
__main__ the name a script run directly gets
python3 -c 'print("__file__" in globals())'
False no file, so no __file__
python3 -c 'import sys; print(repr(sys.path[0]))'
'' the current directory, searched first
python3 -c 'import sys; print(sys.orig_argv[-1])'
import sys; print(sys.orig_argv[-1])
the program's only copy is this string
Everything after the program belongs to the program: the -v in the
first line went into sys.argv, not to Python's verbose flag.
The empty string in sys.path[0] is the one to remember. In a
folder that holds a file called json.py:
python3 -c 'import json' '(the json.py in this folder ran)\n'
python3 -P -c 'import json' ''
python3 -I -c 'import json' ''
sys.path[0] is the directory you ran the command from, and it is
searched before the standard library. -P, new in 3.11, leaves it
out, and so does -I.
4. ONE LINE HOLDS SIMPLE STATEMENTS ONLY
python3 -c 'import sys; for a in sys.argv[1:]: print(a)' x
exit 1, SyntaxError a for cannot follow a ;
python3 -c 'for c in "xy": print(c); print("-")'
'x\n-\ny\n-\n' both prints are the loop's body
python3 -c 'import sys⏎for a in sys.argv[1:]: print(a)' x -v
'x\n-v\n' a newline where the ; was
A ; joins simple statements: an import, an assignment, a call. A
compound statement -- for, if, with, def -- may START the line, and
then everything after its colon is its body; it may not follow a ;.
The way out is a real newline inside the quotes, the ⏎ above.
5. THE EXIT STATUS IS THE WHOLE REPORT
python3 -c 'chr(0x20AC)' exit 0 nothing printed, nothing wrong
python3 -c '1/0' exit 1 ZeroDivisionError; traceback on stderr
python3 -c 'import sys; sys.exit("bad")' exit 1 stderr 'bad\n'
python3 -c 'raise SystemExit(3)' exit 3 whatever number you raise
python3 -c exit 2 no program: Python refused the line
A shell sees nothing but the exit status. 0 means the program ran
to the end, printed or not. 1 is an uncaught exception, or
sys.exit() with a message. 2 comes from Python itself, before any
of your code has run.
6. THE SHELL READS THE LINE BEFORE PYTHON DOES
A shell cuts the line into words before python3 starts, and -c
takes exactly one of them. shlex, told to treat ( and ) the way a
shell does, cuts the two lines like this:
python3 -c chr(0x20AC)
['python3', '-c', 'chr', '(', '0x20AC', ')']
python3 -c 'print(chr(0x20AC))'
['python3', '-c', 'print(chr(0x20AC))']
Unquoted, the parentheses are not part of any word: they are shell
syntax, so the shell deals with them itself and python3 never
starts. Quoted, the whole program is one word.
shlex.quote() writes that word for you:
print(chr(0x20AC)) -> 'print(chr(0x20AC))' one word again: True
print("it's") -> 'print("it'"'"'s")' one word again: True
A single quote cannot appear inside single quotes, so quote()
closes them, writes the quote inside double quotes, and opens them
again. Or keep the character away from the shell altogether: in a
Python string \x27 is a quote and \x5c is a backslash, and single
quotes pass those four characters through untouched.
python3 -c 'print("it\x27s")' "it's\n"
python3 -c 'print(len("\x5c"))' '1\n'
What the run shows¶
The prompt is a mode, not a place. Section 1 feeds one line to the same interpreter four ways, and only the third shows the value unasked: python3 -i reading a pipe, which is the interactive loop with nobody typing. Its prompts go to stderr and its answer to stdout, "'€'\n" — the quotes are repr()'s. Section 2 takes the loop away and gets the same answer from compile() and exec(), where the one word 'single' is the whole difference. The standard library's own prompt, the code module ↗, compiles in 'single' as well, which is why its runsource() prints what >>> would.
The difference is literally one instruction. Disassemble the two compilations of chr(0x20AC) and they match until the value has been computed; then 'exec' pops it and 'single' prints it first:
after the CALL, 'exec' does after the CALL, 'single' does
3.11 POP_TOP PRINT_EXPR
3.12–3.14 POP_TOP CALL_INTRINSIC_1 (INTRINSIC_PRINT), then POP_TOP
In every other way, too, -c is a script. sys.argv[0] is '-c' where a script's name would be, __name__ is '__main__', and there is no __file__: the program exists only as a string, which sys.orig_argv[-1] hands back. Words after the program belong to the program, so the -v in section 3 went into sys.argv instead of switching on Python's verbose mode. The line to remember is sys.path[0] == ''. The directory you ran the command from is searched before the standard library, so a stray json.py beside you is the json your one-liner imports; -P (new in 3.11) and -I both take that entry away. It is the same rule that lets a script's own folder shadow the standard library, and it is at its most surprising here, because a one-liner has no folder of its own for you to suspect.
One line holds simple statements only. import sys; for a in sys.argv[1:]: print(a) is a SyntaxError, because a compound statement — for, if, with, def — may begin a line but may not follow a ;. Once it begins one, everything after its colon is its body, which is why section 4's print("-") runs twice. The way out is a real newline inside the quotes, which all three shells below accept:
Indent that second line and the versions split. Python 3.14 dedents the program before running it ↗; every earlier release reads the indent as an error, which is why this is not in the answer key:
python3 -c '
print(1)
'
3.11, 3.12, 3.13 IndentationError: unexpected indent exit 1
3.14 1 exit 0
The exit status is the whole report. It is all a shell ever sees, so section 5 is what && and if act on: 0 for a program that ran to its end, printed or not; 1 for an uncaught exception or sys.exit('a message'); 2 from Python itself, refusing the command line before any of your code ran. Section 5 prints an exception's name rather than its traceback because 3.13 began quoting the -c source line inside tracebacks — the same failure in a different shape, and a key that could only pass on one side of the change.
The shell reads it first¶
The session this page comes from had a second line. Here it is as fish printed it, with only the prompt shortened:
~> python3 -c 'chr(0x20AC)'
~> python3 -c chr(0x20AC)
fish: Unknown command: 0x20AC
in command substitution
fish: Unknown command
python3 -c chr(0x20AC)
^~~~~~~^
~ [127]>
None of that came from Python. In fish, bare parentheses are command substitution — what bash and zsh spell $(…) — so fish tried to run a program called 0x20AC, found none, and abandoned the whole line. The 127 is the shell's own "command not found"; Python never chose it. Every shell rejects the unquoted line, and each for a different reason:
shell what it made of (0x20AC) the error exit python3 ran?
fish a command substitution Unknown command: 0x20AC 127 no
zsh a glob qualifier unknown file attribute: 0 1 no
bash a ( where none may go syntax error near unexpected token `(' 2 no
Section 6 shows the mechanism with shlex: a shell cuts the line into words before python3 starts, unquoted parentheses belong to no word, and -c takes exactly one. So wrap the whole program in single quotes, and use double quotes for the strings inside it. Single quotes are the one kind inside which no shell expands a variable or runs a command — inside double quotes a $ is a variable in all three, which is how "print('costs $5')" prints costs everywhere. The cases that still bite:
the command fish zsh bash what happened
python3 -c 'print(chr(0x20AC))' € € € the rule
python3 -c "print(chr(0x20AC))" € € € also fine: nothing in it for a shell to expand
python3 -c "print('costs $5')" costs costs costs $5 expanded, to nothing, in all three
python3 -c 'print("it's")' error error error the ' in it's ends the quoting
python3 -c 'print("it\'s")' it's error error \' inside single quotes is fish-only
python3 -c 'print("it'"'"'s")' it's it's it's what shlex.quote() writes
python3 -c 'print("it\x27s")' it's it's it's a Python escape, and no shell sees a quote
python3 -c 'print(len("\\"))' SyntaxError 1 1 fish turns \\ into one backslash first
python3 -c 'print(len("\x5c"))' 1 1 1 the backslash, spelled so no shell touches it
python3 - <<'EOF' … EOF error € € fish has no here-documents
a program with newlines in '…' works works works the portable way to write several lines
Two rules fall out of that table. Spell a quote or a backslash as \x27 or \x5c: those are escapes in a Python string and plain characters inside single quotes, so the program survives every shell unchanged. And when Python is the one starting Python, leave the shell out. The example program never quotes anything — it hands each program to subprocess.run() as one item of a list, and no shell ever reads it. shlex.quote() is for the day you must build a command line as a string, and it quotes for a POSIX shell: a backslash in the program still needs the \x5c spelling in fish.
Who warns about a dropped value¶
Nothing on this page raised a warning, and nothing will: Python has no warning for a value that is computed and dropped, not even under -W error. That is a decision, and other languages made it differently:
a dropped call a dropped literal or sum
Python silent silent, even under -W error
ruff (B018) silent on chr(0x20AC) "useless expression" on 0x20AC
Rust warns on char::from_u32(0x20AC); warns on 1 + 1; silent on '€';
because from_u32 is #[must_use]
C (clang) silent on f(); warns on 0x20AC; -Wunused-value, on by default
A call is the hard case everywhere, because a call is usually the point: print(x) and items.append(x) are expression statements too, and nobody wants a warning on them. So ruff's B018 ↗ flags a bare 0x20AC and skips chr(0x20AC) on purpose — its documentation lists calls as expressions "commonly used for their side effects" — and clang says nothing about f();. The same documentation exempts the last expression of a Jupyter cell, because a notebook displays that one: a notebook cell is a prompt too, which is how code that showed its answer in a notebook prints nothing once it is pasted into a script. Rust answers it the only way it can be answered, by asking the function's author: char::from_u32 is marked #[must_use], so dropping its result is flagged at every call site — see What an attribute is ↗ in the Rust library. Python has no such marker, which leaves the prompt as the only place a dropped value is ever shown.
The interpreters with a one-liner flag split the same way. Perl and Ruby will tell you if you ask: under -w, perl -we '"x"' reports a useless use of a constant in void context, and ruby -we '"x"' a possibly useless use of a literal — though Ruby, like ruff, stays quiet about a method call. And Node is the one with a flag that prints: node -p '"x"' prints x where node -e prints nothing. -p is the flag this page's mistake assumed -c was. (Perl 5.42.0, Ruby 2.6.10 and Node 20.20.2, measured 2026-09-10; not machine-checked.)
If you are coming from ABAP¶
ABAP has neither a -c nor a prompt, and so no gap between them to fall into: every ABAP program is already a program, and nothing appears unless the code writes it — WRITE to a classic list, out->write( ) in a class run from the ADT console, or cl_demo_output=>display( ). The mistake also needs a bare expression to be a legal statement, and in ABAP it is not one: 1 + 1. fails the syntax check, where Python accepts chr(0x20AC) and does nothing with it. What does transfer is section 5. A shell reads a program's exit status the way ABAP code reads sy-subrc after a statement — 0 is success, anything else is a reason to look — and like sy-subrc it is overwritten by the very next command, so check it at once or copy it somewhere. (Not machine-checked — CI cannot run ABAP.)
Try it¶
- Run the unquoted mistake in your own shell —
python3 -c chr(0x20AC)— and match what it prints against the table above. Then runecho $statusin fish orecho $?in bash or zsh: the number is your shell's, not Python's. - List the files in a project folder of yours that would shadow a standard-library module:
python3 -c 'import sys, pathlib; print(sorted(set(sys.stdlib_module_names) & {p.stem for p in pathlib.Path().glob("*.py")}))'. Every name it prints is imported instead of the real module by anypython3 -crun from that folder. Pick one and import it with-Pand without. - Find a
python3 -cline in your shell history —history | grep 'python3 -c'in bash or zsh,history search 'python3 -c'in fish — and check its quoting against the table. Would it survive a$, a'or a\in the program, and would it survive the other two shells? - Take a one-liner of yours with a
;in front of a loop or anif, and rewrite it with a real newline inside the quotes. Then indent the second line and run it on 3.14 and on any olderpython3you have. - If any code of yours builds a command line as a string in order to run Python, change it to pass a list to
subprocess.run()— and delete whatever it was doing about quotes.
Practice¶
Eight one-liners, and the only one that prints quotes. For each line, write down what it prints to stdout and its exit status before running any of them.
python3 -c 'chr(0x20AC)'python3 -c 'x = chr(0x20AC); x'echo 'x = chr(0x20AC); x' | python3 -i -qecho 'print(chr(0x20AC))' | python3 -i -qpython3 -c 'import sys; print(sys.argv[1:])' -v xpython3 -c 'for c in "ab": print(c); print("-")'python3 -c 'import sys; for c in "ab": print(c)'python3 -c 'import sys; sys.exit("bad")'
Then: only two of the eight reached a prompt. Which two — and why does only one of them print quotes?
Answers
Verified output of dash_c_is_not_the_prompt_kata_py.py — regenerated by tools/run_examples.py, never hand-typed.
1. python3 -c 'chr(0x20AC)'
stdout '' exit 0 computed, then dropped
2. python3 -c 'x = chr(0x20AC); x'
stdout '' exit 0 a bare name is an expression too
3. echo 'x = chr(0x20AC); x' | python3 -i -q
stdout "'€'\n" exit 0 the prompt shows it -- as repr()
4. echo 'print(chr(0x20AC))' | python3 -i -q
stdout '€\n' exit 0 and never shows the None print() returned
5. python3 -c 'import sys; print(sys.argv[1:])' -v x
stdout "['-v', 'x']\n" exit 0 after the program, -v is the program's
6. python3 -c 'for c in "ab": print(c); print("-")'
stdout 'a\n-\nb\n-\n' exit 0 both prints are the loop's body
7. python3 -c 'import sys; for c in "ab": print(c)'
SyntaxError exit 1 a for cannot follow a ;
8. python3 -c 'import sys; sys.exit("bad")'
stdout '' exit 1 and 'bad' went to stderr
THE RULE
Only lines 3 and 4 reached the prompt, and only the prompt shows a
value you did not print -- as repr(), which is where line 3's
quotes come from. It never shows None, which is why line 4 has no
second line. Everywhere else a bare expression is computed and
dropped: lines 1 and 2 print nothing, and exit 0 because nothing
went wrong.
THE ONE THAT LOOKS LIKE A FLAG
Line 5's -v comes after the program, and everything after the
program belongs to it. Put -v before -c and it is Python's.
See also¶
repris notstr— section 8 calls the realsys.displayhook, which is why the prompt's answer arrives in quotes- String literals —
\x27and\x5c, and where an escape stops pyproject.toml— its first Try it is a-cone-liner in double quotes, which works because nothing in that program is special to a shell- A character and its bytes on one line ↗ — fish's
( )is command substitution, the same fact from the terminal's side printfwrites bytes ↗ — the same three shells disagreeing about a different command- The
-coption ↗,compile()↗ andsys.displayhook↗ — the reference, including 3.14's dedent - fish: quotes ↗ and command substitution ↗ — the two escapes fish keeps inside single quotes, and what bare
( )means there