Skip to content

REPL: exit when the user types exit instead of asking them to explicitly type exit() #88769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
theacodes mannequin opened this issue Jul 12, 2021 · 59 comments
Closed

REPL: exit when the user types exit instead of asking them to explicitly type exit() #88769

theacodes mannequin opened this issue Jul 12, 2021 · 59 comments
Labels
3.11 only security fixes stdlib Python modules in the Lib dir type-feature A feature request or enhancement

Comments

@theacodes
Copy link
Mannequin

theacodes mannequin commented Jul 12, 2021

BPO 44603
Nosy @terryjreedy, @gpshead, @merwok, @stevendaprano, @eryksun, @vedgar, @pganssle, @pablogsal, @asmeurer, @FFY00, @theacodes, @jdevries3133, @tlalexander, @DiddiLeija
PRs
  • bpo-44603: Exit the interpreter if the user types "exit" #27096
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = None
    created_at = <Date 2021-07-12.03:15:45.559>
    labels = ['type-feature', 'library', '3.11']
    title = 'REPL: exit when the user types exit instead of asking them to explicitly type exit()'
    updated_at = <Date 2021-10-07.08:08:05.232>
    user = 'https://github.com/theacodes'

    bugs.python.org fields:

    activity = <Date 2021-10-07.08:08:05.232>
    actor = 'terry.reedy'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = ['Library (Lib)']
    creation = <Date 2021-07-12.03:15:45.559>
    creator = 'theacodes'
    dependencies = []
    files = []
    hgrepos = []
    issue_num = 44603
    keywords = ['patch']
    message_count = 55.0
    messages = ['397273', '397275', '397276', '397293', '397294', '397296', '397297', '397301', '397302', '397305', '397306', '397307', '397308', '397309', '397310', '397311', '397313', '397314', '397315', '397316', '397317', '397331', '397334', '397372', '397373', '397374', '397377', '397379', '397382', '397403', '397404', '397422', '397425', '397427', '397458', '397461', '397462', '397505', '397507', '397512', '397516', '397685', '397691', '397698', '397699', '402590', '402605', '402620', '402623', '402644', '402655', '402664', '402667', '402701', '403359']
    nosy_count = 14.0
    nosy_names = ['terry.reedy', 'gregory.p.smith', 'eric.araujo', 'steven.daprano', 'eryksun', 'veky', 'p-ganssle', 'pablogsal', 'asmeurer', 'FFY00', 'theacodes', 'jack__d', 'tlalexander', 'DiddiLeija']
    pr_nums = ['27096']
    priority = 'normal'
    resolution = None
    stage = 'patch review'
    status = 'open'
    superseder = None
    type = 'enhancement'
    url = 'https://bugs.python.org/issue44603'
    versions = ['Python 3.11']

    @theacodes
    Copy link
    Mannequin Author

    theacodes mannequin commented Jul 12, 2021

    Presently, when using REPL if a user types simply "exit", they are greeted with a message instructing them to do it "correctly":

    >>> exit
    Use exit() or Ctrl-Z plus Return to exit

    It comes across as a little surprising that (1) the program knows what I meant and (2) the program told me it wouldn't do it unless I request it in a very specific way. This isn't very user-friendly behavior.

    Further surprising is the inconsistent behavior of the other built-ins described on interpreter start-up. "copyright" and "credits" work fine without being invoked as a function, whereas "help" and "license" rebuff the user.

    I know there are compelling technical reasons for the current behavior, however, this behavior is a bit off-putting for newcomers. Knowing the technical reasons behind this behavior made me *more* frustrated than less frustrated.

    Python is a lovely language and I think we should do what we can to be friendlier to users. I propose a few changes to fix this specific issue:

    (1) Include "exit" in the interpreter startup message, making it: Type "help", "copyright", "credits" or "license" for more information, and type "exit" to quit Python.

    (2) Make the interactive interpreter exit when the user types simply "exit" or "quit.

    To address some possible edge cases an objections:

    • if (2) proves too controversial, we should at least do (1) with the slight modification of "exit" to "exit()".
    • if there is a concern about accidentally exiting the interpreter, there are several strategies we can use. First, we can only activate this behavior if, and only if, Python is being used as an interactive interpreter. From what I can tell, main() is already distinguishing this case. Second, if absolutely necessary we could ask the user to confirm that they want to exit. For example:
    >>> exit
    Are you sure you want to exit Python? (yes/no): 

    For what it's worth, I am willing to do this work, however, I might need a little guidance to find the right bits.

    @pablogsal
    Copy link
    Member

    Thanks, Stargirl for opening this issue and for the thorough description and proposals. I am sympathetic with the idea and the general proposal and (unsurprisingly) I agree with (1).

    For (2) there are some challenges here to consider. The most important one is that the mechanism to show those messages is really the repr() of the exit() built-in the one showing the message:

    >>> x = exit
    >>> repr(x)
    'Use exit() or Ctrl-D (i.e. EOF) to exit'

    There is no other mechanism in the interpreter that triggers anything when the user inputs that. The most straightforward way is to raise SystemExit from the repr() of the built-in but that has some obvious problems. As printing anything where the exit function lives will also raise SystemExit (for instance printing the builtins module or objects in the GC).

    For these reasons I propose to go with (1) with the slight modification of "exit" to "exit()".

    @pablogsal
    Copy link
    Member

    For reference, this behaviour lives here:

    class Quitter(object):
    def __init__(self, name, eof):
    self.name = name
    self.eof = eof
    def __repr__(self):
    return 'Use %s() or %s to exit' % (self.name, self.eof)
    def __call__(self, code=None):
    # Shells like IDLE catch the SystemExit, but listen when their
    # stdin wrapper is closed.
    try:
    sys.stdin.close()
    except:
    pass
    raise SystemExit(code)

    @FFY00 FFY00 added type-bug An unexpected behavior, bug, or error labels Jul 12, 2021
    @vedgar
    Copy link
    Mannequin

    vedgar mannequin commented Jul 12, 2021

    Of course, the "license" mention should be changed in the same way (in the same message).

    @tlalexander
    Copy link
    Mannequin

    tlalexander mannequin commented Jul 12, 2021

    Hello all. Curious issue. Thanks Stargirl for opening it.

    Would it be possible for the __repr__ function to examine the calling commands and determine if the origin is the special case where exit is typed in the REPL? Then only when Quitter repr is called would the special case be checked. I’m not too familiar with Python internals but I know for example when an exception occurs a stack trace would include information like that. Probably performance of Quitter repr isn’t critical we just don’t want it to have the wrong behavior. But if there’s any way to determine in that call if we’re in this one special case it seems it would be safe to exit.

    @pablogsal
    Copy link
    Member

    Unfortunately, I don't know how that can help because the stack trace is the same in these two cases:

    >>> import traceback
    >>> class A:
    ...   def __repr__(self):
    ...      traceback.print_stack()
    
    >>> A()
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 4, in __repr__
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: __repr__ returned non-string (type NoneType)
    
    >>> print([A()])
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 4, in __repr__
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: __repr__ returned non-string (type NoneType)

    This also becomes a bit tricky if __repr__ is called from C code or from native threads.

    In general, the underlying problem is that __repr__ should not have side effects.

    @tlalexander
    Copy link
    Mannequin

    tlalexander mannequin commented Jul 12, 2021

    Makes sense. Thanks for taking a look.

    @stevendaprano
    Copy link
    Member

    I strongly oppose this change. Merely printing an object should not have a side-effect of this magnitude. Standard Python behaviour is that an object's repr should return a useful string, not exit the interpreter.

    This is a backwards-incompatible change: right now, it is perfectly safe to print the builtins namespace in the interactive interpreter:

        vars(builtins)

    Doing so gives you are rather large dict, but it is safe and useful. If we make exit actually exit the interpreter instead of print a human readable string, printing the builtins will no longer be safe. It will surprisingly exit in the middle of printing the dict.

    exit is not magic syntax, it is an actual object. It exists in the builtins namespace. We can put it in lists, we can introspect it and treat it like any other object.

    And most importantly, we can print it and get a useful string.

    It is not user-friendly to introduce magical special cases. To call a function, Python always requires parentheses. In the builtins and stdlib there are no special cases of functions that magically perform some task just by trying to view it in the interactive interpreter.

    (Of course third party code can do anything. If somebody wants the + operator to exit the interpreter, or len(obj) to erase their hard drive, Python will allow it.)

    Making a handful of objects -- help, exit (quit), license -- behave differently to everything else is not user friendly. Consistency is more important than convenience, especially when it comes to something like exit where the side-effect is to exit the interpreter.

    What of copyright and credits? I think their design is a mistake, especially copyright. With 11 lines of output, the copyright object seriously uglifies printing the builtins. But at least it doesn't exit the interpreter.

    @stevendaprano
    Copy link
    Member

    This is a backwards-incompatible change, at the very least it needs an okay from the core devs (and possibly even a PEP) not just a patch.

    Stargirl Flowers suggested:

    we could ask the user to confirm that they want to exit

    Please, no, that is the very opposite of a user-friendly change! If I call exit() in the REPL, I want to exit.

    @stevendaprano stevendaprano added 3.11 only security fixes labels Jul 12, 2021
    @theacodes
    Copy link
    Mannequin Author

    theacodes mannequin commented Jul 12, 2021

    I don't think we should completely write off the possibility of doing this just because the *current* implementation is counter-intuitive. As I expressed in the original post, the explanation of this behavior is rather unsatisfying to newcomers.

    Also @steven.daprano, please do not confuse one recommendation for implementation for the concept.

    I agree that printing the Quitter object should not exit the interpreter. However, I disagree that "exit" should not be a special case. Specifically, when using the interactive interpreter the behavior (regardless of implementation strategy) would ideally be:

    >>> exit
    (interpreter exit)
    >>> exit()
    (interpreter exit)
    >>> print(exit)
    Call "exit()" to quit Python. When using the interactive interpreter you can simply type "exit".

    This behavior closely matches IPython's behavior, and even a cursory search reveals not only individual users running into this and being frustrated, but even threads where this behavior has reached "meme status": https://twitter.com/0xabad1dea/status/1414204661360472065?s=19

    @theacodes
    Copy link
    Mannequin Author

    theacodes mannequin commented Jul 12, 2021

    Also, if a PEP is recommended, I will be happy to author it.

    @pablogsal
    Copy link
    Member

    However, I disagree that "exit" should not be a special case.

    But a special case of *what*? How would you implement this in a backwards-compatible way?

    @pablogsal
    Copy link
    Member

    IPython and other reprs are an entire abstraction layer on top of Python, which allows them to do a lot of extra things like implement new commands and alters a lot of behaviours, but the CPython REPL is just the interpreter evaluating commands, and this is very coupled with the regular machinery, at to the point that is the tokenizer the one consuming input (lazily!) from standard input.

    Unless I am missing anything, the only way to do the desired behaviour is to re-architect part of how interactive mode works or to directly make exit a keyword, which is backwards incompatible.

    I may be missing simpler ways of course, but in general, my opinion is that anything that involves modifying the compiler pipeline to somehow special case this is too much cost for the advantage.

    @pablogsal
    Copy link
    Member

    For reference, IPython has an entire interception + filtering mechanism to auto call certain functions:

    https://github.com/ipython/ipython/blob/0e4d6390b2174fb1b352a082b72ad387ae696e87/IPython/core/prefilter.py#L414-L425

    where exit is one instance of this:

    https://github.com/ipython/ipython/blob/81b87f20aa8836b42fbb2b1dee7b662d8d5d46c6/IPython/core/autocall.py#L51-L57

    As you can see, this requires an entirely different execution abstraction and a new layer in the middle that filters/intercepts the user input **after** it has been transformed into some intermediate representation.

    @pganssle
    Copy link
    Member

    If we want to confine the behavior to just the repl, we could possibly have the repl set an environment variable or something of that nature for interactive sessions, so that __repr__ of exit can tell the difference between being invoked in a REPL and not — though I suppose it could cause some pretty frustrating and confusing behavior if some library function is doing something like this behind the scenes:

    def get_all_reprs():
        return {
          v: repr(obj) for v, obj in globals()
        ]
    

    You could invoke some function and suddenly your shell quits for no apparent reason. And if it only happens when triggered in a REPL, you'd be doubly confused because you can't reproduce it with a script.

    I do think the "type exit() to exit" is a papercut. The ideal way to fix it would be in the REPL layer by special-casing exit, but I realize that that may introduce unnecessary complexity that isn't worth it for this one thing.

    Second, if absolutely necessary we could ask the user to confirm that they want to exit.

    A thought occurs: we could simply re-word the message to make it seem like we're asking for confirmation:

    >>> exit
    Do you really want to exit? Press Ctrl+Z to confirm, or type exit() to exit without confirmation.
    

    Then it won't seem as much like we know what you meant to do but aren't doing it, despite the fact that the behavior is exactly the same 😅.

    @pablogsal
    Copy link
    Member

    would be in the REPL layer by special-casing exit

    Unfortunately, there is no REPL *layer* as my previous comments mentioned.

    There is a few details that change for interactive mode but fundamentally the pipeline is the same as reading from a file, except that the file is stdin and it has some special logic in the parser to do that in a lazy way and fail fast. But there is no semantic information that separates REPL for non-REPL.

    @pablogsal
    Copy link
    Member

    One thing we *could* do which is not super invasive, is to match a single AST node of type NAME at the end of Python run. This seems to work but is a bit inelegant:

    >>> print(exit)
    Use exit() or Ctrl-D (i.e. EOF) to exit
    >>> [exit]
    [Use exit() or Ctrl-D (i.e. EOF) to exit]
    >>> exit
    bye!

    I have opened a draft with this in PR27096

    @theacodes
    Copy link
    Mannequin Author

    theacodes mannequin commented Jul 12, 2021

    I do want to be cautious of saying that we can't do it because of the way the REPL is currently implemented- which appears to be an implementation driven by convenience more than necessity.

    I also find pushing against special-case behavior in the REPL strange. The REPL already has special-case behavior: printing the header, the interactivehook that configures readline, heck, the >>> are unique the REPL and plainly copy-pasting a REPL session into a file won't work.

    @pablogsal
    Copy link
    Member

    I do want to be cautious of saying that we can't do it because of the way the REPL is currently implemented- which appears to be an implementation driven by convenience more than necessity.

    Apologies if I have not been clear on this. Is not that we can't do it, is just the balance between complexity and the benefits of the change.

    I also find pushing against special-case behavior in the REPL strange. The REPL already has special-case behavior: printing the header, the interactivehook that configures readline, heck, the >>> are unique the REPL and plainly copy-pasting a REPL session into a file won't work.

    But that is just printing, not semantic behaviour. What we are discussing here is to give a different semantic behaviour to exit NAME only in interactive mode. This is fundamentally different that just printing or make the parser show ">>>" every time it asks for a new line, as those are not changing the *meaning* of Python code.

    @theacodes
    Copy link
    Mannequin Author

    theacodes mannequin commented Jul 12, 2021

    Fair point about semantic behavior and complexity, but hopefully we can come up with a solution that's easier for users.

    I do like the PR suggested.

    @pganssle
    Copy link
    Member

    I'm +1 for Pablo's approach. That's approximately what I meant by "special-case it in the REPL layer" anyway.

    Are there any downsides to doing it this way? It seems tightly scoped and with minimal overhead.

    @pablogsal
    Copy link
    Member

    Are there any downsides to doing it this way? It seems tightly scoped and with minimal overhead.

    We also need to support quit(), if we go this route.

    It makes parsing in the REPL a bit slower because it needs to check for this at every command and is a bit "floating" in the middle of the parser and the compiler (but that's a consequence that we don't have any defined layer for this). We also need to check that this also works with piping input.

    Other than that, only arguments based on the purity of the language, but I think having this working is far more important.

    @pablogsal
    Copy link
    Member

    Since Paul is +1 if another core dev (or devs) are +1 as well with the approach in PR27096 I would feel confident to proceed with this. Alternatively, we could discuss this more generally in python-dev if someone feels that we should have a more open discussion about the tradeoffs.

    @stevendaprano
    Copy link
    Member

    Please don't do this.

    On Mon, Jul 12, 2021 at 02:19:58PM +0000, Pablo Galindo Salgado wrote:

    >>> exit
    bye!

    This is a user-hostile and unfriendly UI for Python. The Python REPL is
    not a shell like bash etc, it should be safe to evaluate any builtin
    object at the interactive interpreter to view its repr without
    side-effects.

    Especially not major side-effects such as exiting the interpreter with
    its total loss of program state.

    The only motivation of this change is pure laziness to avoid typing
    parentheses when you want to call an object. I know that the creator of
    Perl famously says that laziness and hubris are virtues for programmers,
    but I disagree. Pandering to laziness in language design is not a
    virtue.

    This does not add any new and improved functionality, or make the
    language better, or more friendly for beginners exploring things at the
    REPL. It is a useability regression, making it more hostile and
    unfriendly for people expecting to be able to view objects by entering
    them at the REPL without catastrophic side-effects, and the only benefit
    is to save two characters.

    Having said that Pablo, I don't dislike your hack to make the exit repr
    pretend to be a confirmation message anywhere near as much. I don't
    think it is necessary, I think it looks even stranger when displaying
    the builtin namespace dict, but at least it is not a useability
    regression.

    @stevendaprano
    Copy link
    Member

    Other than that, only arguments based on the purity of the language,
    but I think having this working is far more important.

    Having this "working" is not important at all. This is precisely the
    sort of user-hostile anti-feature that we should avoid, not compromise
    the execution model just to save typing two characters.

    I mean, seriously, we're talking about typing *two characters*. If
    people care so much about minimizing the amount of typing when they exit
    the REPL, they can use Ctrl-D or just close the terminal window.

    @pablogsal
    Copy link
    Member

    Thanks Steven for your input and your comments and for expressing your concerns. I will hold the PR then until there is consensus on how to proceed and all concerns are addressed (eventually closing it if there isn't consensus).

    I'm any case, I think we should at least proceed with the uncontroversial part of the proposal:

    (1) Include "exit" in the interpreter startup message, making it: Type "help()", "copyright", "credits" or "license" for more information, or type "exit()" to quit Python.

    This is, including exit in the message and using the form exit() and help() instead of exit and help.

    @gpshead gpshead added type-feature A feature request or enhancement and removed type-bug An unexpected behavior, bug, or error labels Jul 14, 2021
    @stevendaprano
    Copy link
    Member

    On Tue, Jul 13, 2021 at 01:58:30AM +0000, Taylor Alexander wrote:

    I would push back against the idea that this is about laziness. It
    sounds like this is about reducing user confusion.

    Users aren't *confused* by the instructions, which are clear and simple:
    call the object using parentheses. Nobody says that they don't
    understand what it means to "use exit() to exit".

    They are *annoyed* that they have to type the parentheses. This is not
    confusion, and it is disengenious to claim that people are "confused".

    Especially when people have said that they understand the technical
    reasons for why exit behaves as it does, and that makes them "more
    frustrated". So they understand the reasoning why having repr(exit) kill
    the interpreter is a bad idea, and rather than satisfying them, they get
    even more annoyed.

    @jdevries3133
    Copy link
    Mannequin

    jdevries3133 mannequin commented Jul 14, 2021

    I wonder if the middle ground here is to let it be a teachable moment, and to inform the user by having the string returned by __repr__ be a bit more descriptive. Currently, it is:

    Use exit() or Ctrl-Z plus Return to exit

    I propose:

    exit is the function that closes Python when called. To call a Python function, add parenthesis! For example, "exit()".

    To share a personal anecdote, Python was my first programming language. I can remember this specific case of REPL-stubbornness being instrumental in teaching me about referencing versus calling a function. Special cases cause confusion, and a shortcut that removes two characters at the expense of skirting past an essential understanding is not the right choice. The place we should be *most* careful about breaking language idioms are in the spots that are exposed to beginners and newcomers to the language.

    @asmeurer
    Copy link
    Mannequin

    asmeurer mannequin commented Jul 14, 2021

    When talking about making exit only work when typed at the interpreter, something to consider is the confusion that it can cause when there is a mismatch between the interactive interpreter and noninteractive execution, especially for novice users. I've seen beginner users add exit() to the bottom of Python scripts, presumably because the interpreter "taught" them that you have to end with that.

    Now imagine someone trying to use exit as part of control flow

    if input("exit now? ") == "yes":
        exit

    Unless exit is a full blown keyword, that won't work. And the result is yet another instance in the language where users become confused if they run across it, because it isn't actually consistent in the language model.

    There are already pseudo-keywords in the language, in particular, super(), but that's used to implement something which would be impossible otherwise. Exiting is not impossible otherwise, it just requires typing (). But that's how everything in the language works. I would argue it's a good thing to reinforce the idea that typing a variable by itself with no other surrounding syntax does nothing. This helps new users create the correct model of the language in their heads.

    @mdickinson
    Copy link
    Member

    the confusion that it can cause when there is a mismatch between the interactive interpreter and noninteractive execution

    I've witnessed similar confusion when teaching, using IPython. After discovering that you can do

    In [1]: import pandas as pd
    In [2]: cd datafiles
    /Users/mdickinson/Desktop/datafiles
    In [3]: df = pd.read_csv("experiment0023.csv")
    

    it's then a common error to copy those lines to a script and expect them to work. We learned to recommend that IPython's automagic always be turned off, so that at least we could easily explain that "if it starts with a %, it's a magic command interpreted by the IPython layer; otherwise it's passed to Python".

    If Python grew a similar interpreter layer (which seems like one possible solution here), I think we'd have the same issue of making it easy for users to distinguish "commands" intended for the interactive interpreter from those interpreted by the Python core. I could imagine ending up with users typing "%exit" or "!exit" to exit, but one you're adding an extra sigil to distinguish your commands from plain old references to Python objects it doesn't seem so different from having to type "exit()".

    @stevendaprano
    Copy link
    Member

    On Wed, Jul 14, 2021 at 08:10:51PM +0000, Aaron Meurer wrote:

    There are already pseudo-keywords in the language, in particular,
    super()

    super is not a pseudo-keyword. It's a regular builtin object that
    interacts with some (quite clever) compiler magic that occurs when
    classes are created.

    https://stackoverflow.com/questions/19608134/why-is-python-3-xs-super-magic

    The big difference here is that the magic behind super helps to prevent
    serious bugs in user code. super's magic isn't to reduce typing, it is
    to solve a number of real problems with the way people use inheritence.

    @gpshead
    Copy link
    Member

    gpshead commented Jul 14, 2021

    Thanks Mark, that's a good real world experience example from the IPython side.

    @terryjreedy
    Copy link
    Member

    This has been proposed and rejected before. So I think a pydev discussion and steering council decision would be needed to change.

    The current rule in interactive mode is that typing an expression statement echoes the representation of the resulting object. The proposal is make a rather startling exception to the rule. The premise is that "the program knows what I meant".

    However, that is a too-successful illusion. 'The program' has no idea what one meant other than to print the repr(ob). Some coredevs had the idea to *guess* what was meant and to give two functions an exceptional representation with a message that would be correct when the guess is correct.

    In the other hand, special-casing 'quit\n' and 'exit\n' could be seen as analogous to special-casing '^Z\n'. And the patch restricts the special casing, without complicated code, to exactly those sequences of keystrokes.

    @vedgar
    Copy link
    Mannequin

    vedgar mannequin commented Jul 17, 2021

    In the other hand, special-casing 'quit\n' and 'exit\n' could be seen as analogous to special-casing '^Z\n'

    Terry, there is a big difference between special-casing 'exit\n' and special-casing '^Z\n': 'exit' is a perfectly valid identifier (and people use it regularly), ^Z is not. Especially if 'exit\n' exited unconditionally, I think many people would be very frustrated and surprised when trying to inspect the variable called 'exit'.

    I'd have no objection if something like '!exit' was special-cased, but then there is not much difference between adding a bang before and adding the parentheses after. Alternatively, exit can be proclaimed a keyword, but I really think this is overkill.

    And please don't think this process that you're starting now will stop at these two words. Much more beginners, according to my experience, try to type pip install something inside Python REPL. If we do this, it will be a powerful precedent, and almost surely we will have the "magic mess" later.

    @terryjreedy
    Copy link
    Member

    I agree that turning 'exit' and 'quit' into semi-keywords is not acceptible. I added this to my PR review.

    >> exit = 3
    >> exit

    f:\dev\3x>

    @terryjreedy
    Copy link
    Member

    Another issue: exit() and quit() work unconditionally when called, regardless of the context: "a = (3, exit(), 'abc')". The abbreviated versions will not.

    An alternative change is to revise the representation. Perhaps tell the truth first by giving the standard representation -- <class '_sitebuiltins.Quitter'> -- so that people might recognize that they are just seeing a printed string. Then add on the next line, "If you want to exit, enter 'exit' ...".

    @FFY00
    Copy link
    Member

    FFY00 commented Sep 24, 2021

    One technical argument on why it would be beneficial to have custom handling like this for exit is that exit is a site-builtin, not a builtin.

    $ python -S
    Python 3.9.7 (default, Aug 31 2021, 13:28:12)
    [GCC 11.1.0] on linux
    >>> exit()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    NameError: name 'exit' is not defined
    >>> import sys
    >>> sys.exit()

    @vedgar
    Copy link
    Mannequin

    vedgar mannequin commented Sep 25, 2021

    why it would be beneficial to have custom handling like this for exit is that exit is a site-builtin, not a builtin.

    In my view, that's exactly why it _shouldn't_ have a special treatment. After all, site can add many more builtins. Do you want all of them to have autocall?

    @FFY00
    Copy link
    Member

    FFY00 commented Sep 25, 2021

    In my view, that's exactly why it _shouldn't_ have a special treatment. After all, site can add many more builtins. Do you want all of them to have autocall?

    No, and I did not suggest anything of the sort. I just want the exit because of its integral job of... exiting the REPL. Typing import sys; sys.exit() every time I want to test something quick on the REPL is awful UX.

    @pablogsal
    Copy link
    Member

    Typing import sys; sys.exit() every time I want to test something quick on the REPL is awful UX.

    Without disagreeing with the general sentiment, just note that you can always do Ctrl-D.

    @eryksun
    Copy link
    Contributor

    eryksun commented Sep 25, 2021

    Running the REPL with -S is unusual, so having to use sys.exit() or raise SystemExit in that case shouldn't be an issue.

    A user who wants custom behavior for exit could override sys.displayhook() in the PYTHONSTARTUP file. For example:

        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

    just note that you can always do Ctrl-D.

    For the Windows console, Ctrl-D is not usually supported. It's supported when pyreadline is installed. Otherwise one has to type Ctrl-Z and enter. In IDLE it's Ctrl-D even in Windows, in which case the exit repr is wrong, as determined by setquit() in Lib/site.py.

    @stevendaprano
    Copy link
    Member

    Typing import sys; sys.exit() every time I want to test something
    quick on the REPL is awful UX.

    It truly is awful. So why do you do it that way?

    There are at least four other ways to cleanly exit the REPL.

    1. raise SystemExit

    2. exit()

    3. quit()

    4. Ctrl-D (posix systems) or Ctrl-Z ENTER (Windows systems)

    Even if you are running the REPL without the site module (so that exit
    and quit are not available) the import sys solution is surely the worst
    of the lot, UX-wise.

    @FFY00
    Copy link
    Member

    FFY00 commented Sep 26, 2021

    Without disagreeing with the general sentiment, just note that you can always do Ctrl-D.

    That is true, but there are a couple setups where that doesn't work (those keypresses are consumed by something else). I may not be a good data point though.

    Running the REPL with -S is unusual, so having to use sys.exit() or raise SystemExit in that case shouldn't be an issue.

    Yes, it is unusual, however I have found myself asking people to do that when debugging, and I always have to tell people "oh, btw, exit() doesn't work, you have to do ...", which is not nice.

    Even if you are running the REPL without the site module (so that exit
    and quit are not available) the import sys solution is surely the worst
    of the lot, UX-wise.

    Two of the solutions you gave (exit and quit) don't work with -S, and Ctrl-D/Ctrl-Z doesn't work in all setups. raise SystemExit is the only real alternative I would consider when I am explaining things to people and want to avoid issues. I standardized in import sys; sys.exit() as it's generally easier for people starting out, even though it's 5 more keypresses.

    ---

    Anyway, my point is simply that exiting on python -S does not have a great UX, something that I think should be considered when looking at this proposal. If you think the use-case is relevant enough to not warrant a change like this, that is a valid opinion.

    My personal opinion is that we should optimize the UX for the people who are not that knowledgeable, as those are the ones that will have most trouble, while keeping it usable for the rest of users.
    I think this change is a net positive considering those parameters, and I think the arguments against this proposal have not properly taken them into account.

    FWIW, I consider myself a reasonably knowledgeable user, but still end up making this mistake myself a staggering amount of times.

    $ rg 'exit\(\)$' ~/.python_history | wc -l
    553
    $ rg 'exit$' ~/.python_history | wc -l
    132

    @stevendaprano
    Copy link
    Member

    That is true, but there are a couple setups where that doesn't work
    (those keypresses are consumed by something else). I may not be a good
    data point though.

    Can you give an example of a setup where Ctrl-D is consumed but "import
    sys ENTER sys.exit() ENTER" is not?

    @vedgar
    Copy link
    Mannequin

    vedgar mannequin commented Sep 27, 2021

    Just wanted to say that
    "raise SystemExit" is shorter than
    "import sys; sys.exit()", has no special characters (just letters and space) and is really much quicker to write. Yes, it doesn't work if someone rebound SystemExit, but if that's your problem, you have weird coworkers. ;-)

    @terryjreedy
    Copy link
    Member

    Steven's list left out the standard way of closing *any* windowed app -- click the close button on the title bar. Works on all major systems.

    Its does a little too much when python is started on a command line (by closing the console), but beginners, at least on Windows, usually click an icon to start Python.

    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    @terryjreedy
    Copy link
    Member

    If 'help' were replaced with 'help()', then the message displayed with the latter should note the help(object) alternative at the beginning, and not just when exiting, as well as how to exit at the help> prompt - enter a blank line. Additional oddity:
    help>exit displays Help on Quitter in module _sitebuiltins object: and several more lines.
    help>quit exits. Confusing.

    There have been other proposals to exceptionally not require ()s to call a function -- all so far rejected. I largely agree with Steven about the virtue of consistency.

    Any wider discussion, which I agree would be needed for approval, should now go to the Ideas section of discuss.python.org instead of python-dev.

    @merwok
    Copy link
    Member

    merwok commented Nov 3, 2022

    Interactive pydoc uses a custom helper (maybe it’s older than cmd.Cmd) that doesn’t check for exit (which makes sense to me, it’s not a common keyword for little REPLs). This can be changed easily here to avoid displaying the help for the site built-in exit: https://github.com/python/cpython/blob/main/Lib/pydoc.py#L2029

    @pablogsal
    Copy link
    Member

    This will be handled as part of the work in #111201

    @erlend-aasland
    Copy link
    Contributor

    This will be handled as part of the work in #111201

    Great; let's close this as superseded :)

    @erlend-aasland erlend-aasland closed this as not planned Won't fix, can't repro, duplicate, stale May 1, 2024
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    3.11 only security fixes stdlib Python modules in the Lib dir type-feature A feature request or enhancement
    Projects
    None yet
    Development

    No branches or pull requests

    10 participants