fix: preserve sys.exit() exit codes in non-interactive mode by Sarah-2003 · Pull Request #15158 · ipython/ipython · GitHub
Skip to content

fix: preserve sys.exit() exit codes in non-interactive mode#15158

Draft
Sarah-2003 wants to merge 2 commits into
ipython:mainfrom
Sarah-2003:fix-exit-code-preservation
Draft

fix: preserve sys.exit() exit codes in non-interactive mode#15158
Sarah-2003 wants to merge 2 commits into
ipython:mainfrom
Sarah-2003:fix-exit-code-preservation

Conversation

@Sarah-2003

Copy link
Copy Markdown

Summary

Fixes #15132.

When running ipython -c "import sys;sys.exit(2)", IPython returned exit code 1 instead of 2. Regular Python correctly returns 2.

Root cause: TerminalIPythonApp.start() in ipapp.py hardcoded sys.exit(1) for all failed executions, regardless of the actual exit code passed to sys.exit().

Changes

  • IPython/terminal/ipapp.py -- Extract the original exit code from last_execution_result.error_in_exec when the error is a SystemExit. Falls back to exit code 1 for non-SystemExit failures.
  • IPython/core/shellapp.py -- Add a separate except SystemExit clause in the file execution path of _run_cmd_line_code() to preserve exit codes when running script files.
  • tests/test_shellapp.py -- Add 8 tests covering exit code preservation for -c execution and script file execution (codes 0, 1, 2, 42, no-arg, normal execution).

How it works

When sys.exit(N) is called inside user code, InteractiveShell.run_code() catches the SystemExit and stores it in result.error_in_exec. The fix reads error_in_exec.code to extract the original exit code instead of discarding it.

Edge cases handled:

  • sys.exit(0) and sys.exit() -- exit code 0
  • sys.exit(N) for any integer N -- preserves N
  • sys.exit("message") -- exit code 1 (matches Python behavior)
  • Non-SystemExit failures -- exit code 1 (unchanged)

Test plan

  • ipython -c "import sys; sys.exit(2)" returns code 2 (was 1)
  • ipython -c "import sys; sys.exit(42)" returns code 42 (was 1)
  • ipython -c "import sys; sys.exit(0)" returns code 0
  • ipython -c "print('hello')" returns code 0 (unchanged)
  • Script file with sys.exit(2) returns code 2
  • All 3 modified files pass syntax check

@themavik themavik left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new except SystemExit in _run_cmd_line_code still calls showtraceback before exiting — behavior change vs a quiet propagate; might be noisy for intentional sys.exit in non-interactive runs.

@Sarah-2003

Copy link
Copy Markdown
Author

@Carreau Carreau added this to the 9.12 milestone Mar 25, 2026
@Carreau

Carreau commented Mar 25, 2026

Copy link
Copy Markdown
Member

Thanks for the contribution; I'm not sure why the tests are failing on CI and passing locally

@Carreau

Carreau commented Mar 25, 2026

Copy link
Copy Markdown
Member

@Carreau Carreau marked this pull request as draft March 25, 2026 15:04
@Carreau Carreau modified the milestones: 9.12, 9.13 Mar 27, 2026
@Carreau Carreau modified the milestones: 9.13, 9.14 Apr 22, 2026
@Carreau Carreau removed this from the 9.14 milestone May 29, 2026
When running `ipython -c "import sys;sys.exit(2)"`, IPython incorrectly
returned exit code 1 instead of 2. The exit code was hardcoded to 1 in
the start() method of TerminalIPythonApp regardless of the actual code
passed to sys.exit().

Extract the original exit code from the stored SystemExit exception in
last_execution_result.error_in_exec. Also handle SystemExit separately
in the file execution path in _run_cmd_line_code().

Fixes ipython#15132
sys.exit() should exit quietly, not print a traceback. The bare except
handler below still shows tracebacks for actual errors.
@Carreau Carreau force-pushed the fix-exit-code-preservation branch from 9099d4f to 539c174 Compare June 12, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ipython turns all non-zero exit codes to 1 when script ran with "ipython -c script.py" uses sys.exit()

3 participants