Can "quit" be made to quit?

I was just using Octave and its quit works just fine. Likewise .quit has immediate effect in the sqlite3 repl. But in Python, typing quit doesn’t quit:

>>> quit
Use quit() or Ctrl-D (i.e. EOF) to exit

Would it be reasonable to make quit actually quit?

Perhaps the Quitter.__repr__ could invoke Quitter.__call__. Or perhaps there is a way to leverage a module level __getattr__ or some other technique.

3 Likes

This was previously discussed in https://github.com/python/cpython/issues/88769

And what will be the effect of print(vars(builtins))? Or even just help(builtins)?

repr() with a side effect is evil.

9 Likes

I only skimmed the thread but didn’t see this; has it been considered to put the special-casing into the default sys.displayhook rather than __repr__? That would protect against catching it inside a larger repr, since it would only be triggered if the exact object is being output directly.

1 Like

The issue is still open and probably warrants a discussion here.

I would venture to say that 99.99% of the time where the user sees the current quit message, the user actually wanted to quit but interpreter refuses. The displayed message is annoying and has the feel of “you didn’t say please”. This is not a user friendly behavior.

Also, this isn’t uncharted territory. Many REPLs have a command word for exiting a REPL. Even 45 years ago, I could type BYE to exit BASIC. And today, I can type exit into BASH or ZSH. And from the beginning, IPython has always done the right thing when the user types quit.

2 Likes

FWIW, it always frustrated me as a user that exit printed that message but didn’t actually quit, like it did in anything else I was used to (IPython, bash, cmd, etc). As others have mentioned, though, having quitting be a side effect of merely displaying the repr() of an object seems rather surprising, confusing and odd, but I presume another approach should be possible.

1 Like

I think it would have to be either this, or else hard-coded support built directly into the interpreter. As Serhiy says, having the interpreter quit when doing e.g. print(vars(builtins)) seems like a non-starter. Just imagine the support questions (and complaints) about that.

Hard-coding support would be perhaps inelegant, but perhaps pragmatic. It would also mean that the quit and exit objects could be dropped from the builtins entirely, which in turn would prevent them from being misused in scripts. I’m sure nobody will miss the ability to showcase their knowledge by sitting a beginner in front of the REPL and casting strange, useless but mystical incantation like quit is exit. On the other hand, it would definitely interfere with people trying to use those as variable names at the REPL.

+1 for building support directly in the REPL. I believe that is how IPython does it as well.

And yes, the __repr__ idea was dead on arrival.

3 Likes

Do you happen to know how IPython responds to attempting to use quit or exit as variable names?

It occurs to me that, if there is built-in support, it might also make sense to define quit and exit as keywords (that simply always cause a SyntaxError, since there are no valid grammar rules including them). That would definitely prevent misusing them, although it would require sys.exit to be renamed. To me it would make sense to have e.g. quit = 'custom quit message' report a syntax error at the REPL, since we would now be treating quit as an interpreter-only pseudo-statement.

As far as I can tell, it uses a macro-like mechanism to preprocess the input and inject the parenthesis prior to sending it to the interpreter. This allows you to do exit 1 and it gets rewritten to exit(1) to pass an error code, for instance. However, as you might expect it behaves oddly if you try to use it for something else like a variable assignment, falling into a classic macro pitfall.

Here is the results (on IPython 7.33.0 running inside of QtConsole inside of Spyder)

image

I’m a little surprised that it doesn’t emit a more helpful error message for this particular case.

In any case, I’d assume doing full on macro rewriting like this is not what we ant for the default Python REPL. Instead, the input preprocessing should just specifically check for the exact input text quit or exit (perhaps with trailing whitespace stripped) and call the corresponding function, rather than being too fancy like this.

There would need to be an awfully strong motivation to justify breaking a ton of working code using this as a parameter name, variable name, function name, method name, etc., etc., and update all the code that relied on that code (including, as you mention, sys.exit in the stdlib—that alone is a big enough change to raise a very high bar for acceptance). And I don’t really see a motivation presented at all, other than “to avoid misusing them”.

Presuming the check was precise enough, for just those exact words at the REPL, the chance of misuse seems far lower than the breakage this would cause—and even that, I’d think, could be prevented by checking to see if the exit defined in the current namespace was the exit builtin, and if it isn’t then don’t trigger the above autocall behavior.

Many MANY other REPLs have specific commands (sometimes introduced with a special prefix, sometimes not). I don’t think it would majorly break people’s minds if typing “exit” on its own caused the interpreter to exit, despite the fact that “exit = 1” would be a perfectly legitimate line of code. Here’s Pike’s REPL, called “Hilfe”:

Pike v8.1 release 15 running Hilfe v3.5 (Incremental Pike Frontend)
Ok.
> help
[chomped for brevity]
There are also a few extra commands:
 quit       - Exit Hilfe.
Enter "help me more" for further Hilfe help.
> void help() {write("Hey look, I'm a function!\n");}
Hilfe Warning: Command "help" no longer reachable. Use ".help" instead.
> help;
(1) Result: HilfeInput()->help
> help();
Hey look, I'm a function!
(2) Result: 0
> quit
Exiting.

If we want Python to quit when told to quit, that just needs to bypass the “eval” part of the REPL and handle it before passing it through to the interpreter.

(Incidentally, Pike’s multi-line parsing disables these sorts of commands. Python could do the exact same thing, or could recognize “quit” at the start of a line even if the prompt is “…” - it would be fine either way.)

I made a related suggestion previously, to be implemented in one’s PYTHONSTARTUP script. One complaint was that it doesn’t work when running with -S. Here’s a version that covers that case by defaulting builtins.exit to sys.exit.

import sys
import builtins

def displayhook(obj, prev_displayhook=sys.displayhook):
    exit = getattr(builtins, 'exit', None)
    if obj is exit and callable(exit):
        exit()
    else:
        prev_displayhook(obj)

sys.displayhook = displayhook

if not hasattr(builtins, 'exit'):
    builtins.exit = sys.exit
1 Like

I can’t even remember when I used quit() or exit() last time, except for testing that they work. In the REPL it is easier to press Ctrl-D, and in scripts the code is usually well structured and does not need to exit suddenly from the middle of module-level code.

There are problems with all proposed ideas.

One problem is that Python does not have commands. There were print and exec statements in the past, but they now are regular functions, harmonized with the rest of the language. Introducing the quit statement looks like a step back. Also, making exit a keyword would mean getting rid of os.exit and other uses of exit as variable, function or parameter name.

Special casing it in displayhook would require special support in any custom implementation of displayhook. It is a code duplication, the source of bugs and inconsistent behavior between interpreters. Making exit() simply raising a SystemExit was an elegant solution, and so you want to get rid of its advantages.

And the main problem is that if exit in the REPL an in a script will have different behavior, it will raise much large wave of questions from beginners and will cause many errors. The current behavior serves educational goal first at all. It teaches you that you should call a function to make it executed. It prevents you from one of common beginner’s errors (and we still have a lot of reports about a code in which a user forgot to add () for function call).

6 Likes

That’s true of Python as a language, but there’s no reason that the REPL can’t have commands in it. As mentioned above, other REPLs often do (perhaps introduced with a specific leading character such as a dot or slash). This is a REPL-specific concept (it’s not a replacement for sys.exit() in general), so it doesn’t have to be a keyword in the normal sense.

True, but the REPL also adds prompts in front of text, has different rules about blank lines, and has other small distinctions. It’s pretty easy to make code that can both run as a script and be pasted into the REPL, but there are always going to be odd edge cases.

Point of note: The asyncio REPL has other distinctions from normal asyncio code (including await outside of a function), so we already have some disconnects with the regular REPL and with regular asyncio. Practicality beats purity.

Perhaps it’s time to add slash-commands to the Python REPL. Those can be added in a backwards compatible way AFAICT and without needing any hacks:

  • /exit … exit REPL
  • /help function … run help(function)
  • /debug … import pdb; pdb.pm()

etc.

The advantage of such commands is that they clearly don’t look like Python program code, won’t get in the way with variables, function names, module names, etc.

People also won’t expect them to work as part of Python programs.

I’m sure most here are aware, but for those that aren’t, presumably the most relevant prior art here are the IPython magics, for which % is used. There’s also ? (in the form of ?object or object?) which gets help on an object, !command to run system command (including fairly sophisticated built-in shell functionality) and %% to run commands and scripts in other interpreters (e.g. %%perl); see here for a summary. / would be a reasonable choice as it doesn’t conflict with any of the special characters used by IPython, though compatibility of the commands themselves with REPLs like IPython (which already have far more complete feature sets in this department) would need to be investigated.

So now the Python ecosystem is going to have two distinct “magic” systems?

  • slash magic /cmd in CPython
  • IPython magics %cmd %%cmd ?object !command in IPython

Doesn’t sound confusing at all

I admit that I wouldn’t mind some of the IPython features like multiple levels of “last result” (_, _2, _3 etc) and I am partial to the ?object syntax, but if we open the door to magic interpreter commands you know the sky is the limit.

It will also push other interpreters to feel obliged to offer their own /cmds.

I’m not necessarily saying this is a good thing or a bad thing. But we should be clear about the long-term results of what we are encouraging.

And if people use PostgreSQL, they’ll also have to understand \ as a command prefix, and if they use Twitch, no less than three - /.! - each with different purposes and meanings.

People will cope. Information can be added to the repr for the Quitter, a line could be added to the REPL’s greeting, or whatever else, but people will cope. These are different command interpreters and they have different rules.

45 years ago I could type commands like DATA READ NEW SAVE LIST etc in my BASIC session too. BASIC’s editing facilities were very limited compared to modern dedicated editors, but at least it was built in. Should Python offer the same?

In the shell, I can type cd ls etc, and almost never need to type parentheses.

These are radically different UIs to the Python REPL, with advantages and disadvantages. When was the last time you had shell-like quoting issues in Python?

IPython introduces an entire system of “magic” commands in order to support quit automatically quiting, bringing in shell-like commands such as cd some/location with it. It does a really good job of it too. Should we say that because the shell is a REPL, then the CPython REPL is also a shell and we should support parentheses-free commands?

It starts with exit and quit. There have already been complaints that we should bring back parentheses-free print. How about help? 99% of the time I type help I want to start up the interactive help, not get the help object repr. (But then there is that remaining 1% of the time…)

Why is it so complicated to change directories and get a file listing in Python? If the Python REPL is a shell, it should be easier. In IPython I can just say ls some/directory and it Just Works.

The slope is really not as slippery as all that. There is a clear distinction here: in this model, exit and quit are commands to the interpreter, not part of the language. Currently, they can be used in scripts, even though they very much aren’t intended for that. This proposal would be taking them away from the language. Reintroducing print-as-a-statement or anything else like that would be completely different (and wrong-headed for a wide variety of reasons, and lose huge amounts of useful flexibility).

Sure. Proposals like that can be considered one at a time. But the point is, if the Python REPL is a shell, then it supports things that by definition are not part of the language. Even in 2.x when print was a statement, it was not a command.

There aren’t a lot of reasons help couldn’t work that way either, IMO. When was the last time you had a good reason for a script to invoke it? Maybe if you’re implementing an IDE in Python or something?

1 Like