Currently, Python implies that it allows declaring variable as global anywhere in the function, as long as the variable was not assigned before. In reality, it does not work the way you might expect. The “global” keyword is actually processed before the function is created, not when the corresponding line of code is supposed to be executed. This could lead to some confusion. For example:
a = 1
def f(b):
if b:
a = 2
else:
global a
print(a)
This code would raise SyntaxError, as the assignment is above (“before”) the global declaration, even though it seems like those two code paths should not affect each other.
The programmer might try to switch the order, making it arguably worse:
a = 1
def f(b):
if not b:
global a
else:
a = 2
print(a)
This time, the variable would always be “global”, even if the code never took the first path.
I suggest creating a warning if “global” is not at the top level of the nearest function declaration.
For example:
def x(a):
# Ok
if True:
# Warning
pass
# Ok
def y(b):
# Ok
for c in b:
# Warning
pass
# Ok
# Ok
Eventually, the warning can be replaced with a DeprecationError, as there is no reason to keep this unintuitive behaviour. Maybe even eventually deprecate the global keyword entirely, replacing it with a decorator or a similar syntax, as this is not a real code that is executed somewhere within the function, but rather something affecting its creation.
Similar decisions should be done for other “fake code” declarations working during function creation.
What’s the benefit? If you have an actual problem, you’ll get notified. As it is, you can place the global declaration whereever it makes the most sense.
I agree. Most statements in Python are imperative so it may not be apparent to a Python novice that global and nonlocal are really declarative. In your first example the user is at least given a syntax error, but in the second example the code runs and the novice user may be baffled by the behavior if the code is written with the belief that global is also imperative. A warning would be nice indeed.
global is a directive to the parser. It applies only to code parsed at the same time as the global statement.
To me it would be confusing to understand it in that sense and not to allow it in certain places. What does it matter where in the function the parser directive is?
This would be somewhat equivalent to not allowing yield after an unconditional return.
I can see how it can be confusing to users who do not understand these as parser directives, but muddying the waters and only allowing them in the places where they could be understood as runtime directives seems like it just moves the understanding curve to somewhere else?
Exactly. People will have to learn the exact same thing, either way, so there’s no benefit to preventing people from placing the global statement close to where it makes sense.
By the way:
That’s precisely why it CANNOT be a decorator. It is a statement that affects compilation, and so it has to be part of the compiled code and not a decorator. I’m not sure why “is not real code” should make it a decorator; on the contrary, decorators are much better at adding real code.
I think the OP’s example demonstrates why there can be benefit to those mistakenly treating global as an imperative statement.
But yeah you’re right that restricting global to the top level of a scope would force the declaration to be possibly placed far from its usage, e.g.
def f(b):
global a
if not b:
... # lots of code
a = 2
print(a)
when it would be more readable to declare the global variable close to its usage:
def f(b):
if not b:
... # lots of code
global a
a = 2
print(a)
So yeah the warning/deprecation would not be a good idea unless someone can come up with a trigger rule sophisticated enough to only warn about the OP’s case but not the case above.
Yep. But if you treat it as imperative, it’ll most likely trigger an error, and in the example of putting a global statement into one branch and not another, that’s not really any different from other misunderstandings. Ultimately, there is no way to design a language that doesn’t need to be learned. Requiring all global statements to be at the top of the function won’t reduce the amount that has to be learned.
In my second example, the programmer is not notified, while the program works in an unintended way. It is not obvious that the assignment would affect the global variable.
I think one of the main draws of Python is that many of its syntaxes don’t actually need to be learned since they already read like pseudo code, so if there can be improvements made to the language to help reduce things that must be learned by carefully reading the docs then I’m all for it.
It affects the function as a whole during compilation. The same is true for decorators. They add code, but the addition happens at compilation time, not at runtime.
That’s a great point! I have not considered that. Still, it might be confusing if there are assignments after the “if” statement ends.
Maybe, decorators or new decorator-like syntax would help? If the statement affects the entire function, why shouldn’t it be before the function? In that case, old syntax can be left with a warning just for compatibility.
Thinking a little bit more about that, I found a case where the approach of puting it close to usage might have the opposite effect. After all, it must be before the first assignment, even if it is far away from the main code.
a = 1
def f(x):
# Fallback for a very rare edge case
if not x:
# Some fallback code
global a
a = 2
# More code with functions internally using the variable
# A lot of main code
a = 3
In this case, the programmer might miss the “global” declaration entirely, as that edge case is almost never reached. But the variable would still be global…
Putting “global” declarations at the very beginning of the function would make more sense in this case. But if we are putting it at the start in some cases, we can make it a more universal rule, as the programmers will look there anyways.
It still seems to me that the entire “declaration as code” approach is just an artifact from the times before the decorators were added.
I think a better and still simple rule for global’s placement would then be that it has to be placed in a code block that covers all of the variable’s references in the scope.
This might still be far from where the variables are used, while also lacking the convinience of a single place to look for declarations.
At least that would prevent the situations where the declaration is hidden in an unreachable code path. This is still better than what we have now, but still not quite intuitive.
On the other hand, we can go the opposite way and turn “global” and “nonlocal” into runtime statements. This approach will break compatibility, though…
I know. But “global” declaration works in a unique way, so it will be a compromise anyway. At least, decorators are affecting the entire function and are positioned before it, which would make sense for the “global” declaration. Meanwhile, the current approach of code-like declaration leads to unexpected results, breaking the core principle of Python being easy to understand and meaningful.
For example, this should not work the way it works now:
a = 1
def f():
if False:
global a
a = 2
a = 3
print(f"Local a: {a}")
f()
print(f"Global a: {a}")
In real life, “if False:” might comment out part of the code. Or instead of literal “False” it could be a check for a different platform or debug mode. Why should a statement inside the unreachable code ever affect the result?
This is the case for a lot of things that affect compilation. This is a fundamental difference between those sorts of things, and the sorts of things you can do with a decorator. For example:
def f():
if False:
x = 1
print(x) # UnboundLocalError
def g():
if False:
yield 1
print("Hello, world") # Not called until you pump the generator
def h():
if False:
x=
print("Can't run me") # there's a syntax error above me
“if False” is not the same as commenting something out. It means that the code still affects the function. In contrast, a decorator is (approximately) equivalent to creating the undecorated function, and then calling the decorator and assigning its return value back to the same name. This is very different.
But even if it were possible, I don’t see any benefit to having global as a decorator. You would still need to have it there, you gain nothing compared to just having the statement in the function body.
As to mandating that global statements go at the very top of the function - you’re ALREADY free to demand that, in your own codebase. Nobody is stopping you from making that a rule. If you find that it’s better for you to do this, then you can mandate it for you. Personally, I think that it would make it worse for me, but that just means that I won’t mandate it for me.
Decorator-like syntax would make it more clear that the declaration affects the function as a whole. It would also provide a centralised place to look for global declarations without limiting the functionality.
The current approach makes code less readable, as the “declaration in code” syntax suggests that at least something is happening when the exact code path is reached. What is the point of having the function-wide declaration hidden in nested if statements?
As for your examples, it is obvious that a SyntaxError would prevent the code from running, but other two examples are actually the examples of counterintuitive behaviour, that Python tries to avoid.
Although the decorator-like version probably won’t be doable, I’m curious what you think it would look like. Can you modify one of your examples to use a decorator? I realize it won’t work with current python, I’m just interested in what you expect it to look like. Thanks!
To me, globals that are altered in a function body should be considered part of the function’s signature because they play very similar roles as regular parameters and return value.
So if I were to design a new syntax I’d put the declaration of globals in the function header, among the argument list:
a = 1
def f(b, global a):
if b:
a += 1
print(a)
With this signature it is then clear to a caller that the global variable a of the module is part of the function’s input/output.