Suggestion: add "end" as a keyword for marking end of blocks

Python code blocks are marked by indentation, as we know. When you unindent back to the left, the block is finished.

for x in range(100):
    print(x)

print("Finished.")

So far, so good. But this means that the following code is usual:

i = 0
while < 100:
    if i % 2 == 0:
        i *= 2
        print(i)

        if ... # more conditions
            do_something_else()

    i += 1

For me, that i += 1 is “dangling”, in the sense that it is more difficult than usual to realize that the instruction is still inside the loop.

That’s why I propose an end keyword. Using that imaginary keyword, the code could become:

i = 0
while < 100:
    if i % 2 == 0:
        i *= 2
        print(i)

        if ... # more conditions
            do_something_else()
       end
    end

    i += 1
end

This code is equivalent to the previous one, but obviously it would not work in Python unless you put an ‘#’ before each end, i.e., transforming them in comments.

Proposal summary:

  • Add a new keyword: end. In order for this keyword to not clash with variables, function names, etc., it should only be recognized as such when found sitting by itself in a source code line.
  • Make it optional. If it were mandatory, that would mean it would be a breaking change. Also, since I haven’t found nothing similar (apart from braces, that is) in PEPs or this discussion, it wouldn’t be fair to ask for everybody to change to a new “style”.
  • No compromises: this keyword and its management would not go beyond the parsing phase. No differences to code generation, optimizations, etc.

Objective:

  • Improve readability of source code.

Thanks for reading. I don’t know if this is just a personal taste and nearly everyone would find it weird or just uninteresting. Or maybe there is some real interest.

Why not do just that?

i = 0
while < 100:
    if i % 2 == 0:
        i *= 2
        print(i)
    # end

    i += 1
# end

Or similarly, just define end to be some dummy value:

end = "end"

i = 0
while < 100:
    if i % 2 == 0:
        i *= 2
        print(i)
    end

    i += 1
end

It seems that either of these would give you the code readability effect you want, without requiring you to wait for a PEP to be accepted and Python parsing tools to be updated.

2 Likes

If you want an optional keyword that doesn’t change the effect of the code when inserted, that can be placed in that position, we already have one: pass.

1 Like

Using white-space to indicate blocks is Python.

It’s true, I think, that this can lead to problems – like a having line that is by accident shifted 4 spaces to the right or left, thus leading to bugs that may be quite difficult to pinpoint (at least by simple code inspection). But this also gently encourages people to just not do that: write deeply nested conditionals, using multiple nested blocks. Imo it’s a code smell to have blocks like that.

3 Likes

If you want an optional keyword that doesn’t change the effect of the code when inserted, that can be placed in that position, we already have one: pass.

Yep. Or maybe “…”, but that “does not seem to be it” for me.

Yep. Or maybe “…”, but that “does not seem to be it” for me.

Woah! This works. Not all programming languages would let you put a string literal sitting by itself, but Python does!

It seems that either of these would give you the code readability effect you want, without requiring you to wait for a PEP to be accepted every Python parsing tool to be updated.

Well, that’s true. It still feels like a “hack”, but I think I could live with that. Especially taking into account that nobody seems to find this a terrific idea.

I see. As I said, I guess not many will find this to be a terrific idea.

Thanks!

Any expression can be used as a statement (called, unsurprisingly, expression statements in the grammar). The most common uses of expressions statements are:

  1. Function calls as standalone statements.
  2. Docstrings
  3. ... as a conventional replacement for pass in certain contexts.

But there is no particular reason to complicate the grammar by restricting expression statements to certain expressions.

1 Like

If you wanted to, you could also add a small linter based on the stdlib ast module that enforces that you end all your scopes with an end expression statement, and that you don’t have it anywhere else.

1 Like

Notably, this is true in MANY languages; but if a statement has to be ended with a semicolon, so too does an expression statement. For example, in C, you can write "hello"; and it’ll be perfectly fine. The only difference in Python is that an expression on its own is a valid statement.

Just to be sure. Will all those ends be optimized out?
I guess they will be, but just in case…

No, they’re not optimized out. They still get loaded (and immediately pop’d) at runtime:

>>> import dis
>>> dis.dis("""
... end = "end"
... 
... i = 0
... while i < 100:
...     if i % 2 == 0:
...         i *= 2
...         print(i)
...     end
... 
...     i += 1
... end
... """)
  0           0 RESUME                   0

  2           2 LOAD_CONST               0 ('end')
              4 STORE_NAME               0 (end)

  4           6 LOAD_CONST               1 (0)
              8 STORE_NAME               1 (i)

  5          10 LOAD_NAME                1 (i)
             12 LOAD_CONST               2 (100)
             14 COMPARE_OP               2 (<)
             18 POP_JUMP_IF_FALSE       34 (to 88)

  6     >>   20 LOAD_NAME                1 (i)
             22 LOAD_CONST               3 (2)
             24 BINARY_OP                6 (%)
             28 LOAD_CONST               1 (0)
             30 COMPARE_OP              40 (==)
             34 POP_JUMP_IF_FALSE       13 (to 62)

  7          36 LOAD_NAME                1 (i)
             38 LOAD_CONST               3 (2)
             40 BINARY_OP               18 (*=)
             44 STORE_NAME               1 (i)

  8          46 PUSH_NULL
             48 LOAD_NAME                2 (print)
             50 LOAD_NAME                1 (i)
             52 CALL                     1
             60 POP_TOP

  9     >>   62 LOAD_NAME                0 (end)
             64 POP_TOP

 11          66 LOAD_NAME                1 (i)
             68 LOAD_CONST               4 (1)
             70 BINARY_OP               13 (+=)
             74 STORE_NAME               1 (i)

  5          76 LOAD_NAME                1 (i)
             78 LOAD_CONST               2 (100)
             80 COMPARE_OP               2 (<)
             84 POP_JUMP_IF_FALSE        1 (to 88)
             86 JUMP_BACKWARD           34 (to 20)

 12     >>   88 LOAD_NAME                0 (end)
             90 POP_TOP
             92 RETURN_CONST             5 (None)

Thanks for answering.
Yeah, I was fiddling myself with the dis module and was reaching to the same conclusion.

This is unfortunate. :frowning:

For something that does get optimized out, you could use a variable annotation:

>>> def f():
...     end: some_annotation
... 
>>> dis.dis(f)
  1           0 RESUME                   0
              2 RETURN_CONST             0 (None)

Type checkers may not like that, however.

2 Likes

The annotation alone doesn’t define a name; you need an assignment for that. Without the assignment, end will produce an UnboundLocalError if you try to use the name later as the block marker.

1 Like

I guess the only real possibility left is to use “# end”.

About a possible PEP, I guess the proposal would not have traction and would be rejected first thing, so there’s no point in wasting time on it.

You could also solve your problem with tooling. Editors could easily insert a visual marker for where a block ends, along with its indentation level. Most editors have moderately powerful plugin systems, so I imagine it would be fairly easy to extend your editor of choice, with something that appeals to you.

That doesn’t help with code reviews on GitHub etc. [1], but it’s at least worth considering if that would make more sense for your use-case.


  1. although if you were really determined you could probably write a browser plugin to insert visual markers there as well ↩︎

1 Like

In my experience, if you find the need to have an # end marker, then it is most likely that the code is too nested or too complicated, so it could benefit from refactoring.

There’s room for complicated code when performance is important (at least save the cost of function calls), but in most cases refactoring to functions, methods, and objects makes the code more pythonic and easier to read.

5 Likes

If you’re unhappy with the lack of block closing markers, and the performance cost of an unused expression statement matters to you, then honestly, I think you might find another language better suits you. Significant indentation, and prioritising developer time over raw performance[1], are fairly fundamental to what Python is…


  1. That’s not to say performance doesn’t matter, we just prefer the idea of “make it right, then make it fast”. Which, by the way, means that it’s not impossible that the bare end variables might get optimised out in future Python versions… ↩︎

6 Likes

Is it really more difficult? I don’t think so, for me your ends don’t help and are purely a space-wasting distraction. Maybe it’s an issue of being used to meaningful indentation? How long have you been using Python (and programming in general)?

If they at least were endif and endwhile, then they’d carry some useful information…

I thought of a possibility which would merge the best of both the worlds (the proposal by @BrianSchubert and yours).

Instead of creating a string constant, and since ... is just an object of the Ellipsis class, assign end to the ellipsis so it becomes a ellipsis constant. It should be even easier to see that the expression should not produce any code.

end = ...

i = 0
while < 100:
    if i % 2 == 0:
        i *= 2
        print(i)
    end

    i += 1
end

This works perfectly (it wouldn’t with pass), and one would assume that, being end the ellpisis, it would not produce code. Unfortunately, it does.

import dis

dis.dis("""
end = ...

i = 0
while < 100:
    if i % 2 == 0:
        i *= 2
        print(i)
    end

    i += 1
end
""")

  3           0 RESUME                   0
...

  7     >>   84 LOAD_GLOBAL              4 (end)
             96 POP_TOP
             98 JUMP_BACKWARD           34 (to 32)

  8     >>  100 LOAD_GLOBAL              4 (end)
            112 POP_TOP

I guess the problem is that end is, after all, a constant, not an alias or a macro. So the possible alternatives are still # end or ....