Deprecate incorrect usage of "global"

But the global statement is only needed if the global name is assigned to, not if it’s only read. In your example:

the name a is declared as global on account of it being written to; but the name print is also global, without being declared so, because it’s never written to. Python doesn’t have “output parameters”, so it seems rather odd to have written-to globals listed among the function’s inputs.

1 Like

Good point. Implicitly readable global names are as much an input to a function as explicitly writable global names, so they should also be part of a function signature by my own logics. I have no good alternatives then. Placing the global declarations anywhere else in the function header looks odd to me.

I would agree with you from a function analysis point of view, but not from a function authoring point of view. So maybe the best way to look at this would be as an editor/IDE feature that shows you, at the top of a function, every global that it reads or writes? That’s actually quite easy to get - f.__code__.co_names for any function that you can compile. (Might be harder if it’s not syntactically valid yet.)

An IDE feature sounds plausible. I’m not sure why you brought up f.__code__.co_names though since it’s available only at runtime and contains attribute names too.

Could a compound statement version make things clearer?

a = 1
def f(b):
    if b:
        a = 2  # local
        print(a)
    else:
        global a:
            print(a) # only global in this scope

However Python doesn’t usually have compound statement scoping, so maybe not.


the novice user may be baffled

I suspect the right solution is anyway to just recommend “avoid global”, especially to novice users.

That would require “global” declaration to be done at runtime, not during parsing. This might be a good idea, but it would break a lot of things for compatibility.

Here is the code with all variants I like at once:

a = 1

@global a  # Fake decorator, just like "global" currently is a fake runtime code line
!global a  # Decorator-like syntax, but a different character to signify a parser statement
#^global a  # Parser statements are not runtime code, so it might be a comment, like the one specifying the encoding at the beginning of the file
def f() global a:  # Clearly not a function argument, but still part of function declaration
    $global a  # Another variant of special syntax, but placed inside at the beginning like a docstring
    global a  # Just force the current syntax to be at the beginning
    a = 2

Special symbols can be different, I just picked a couple that are not commonly used in Python

a = 1
def f(b):
    c="global a"
    if b:
        a = 2
    else:
        exec(c)
    print(a)

f(True)

but f(False) Run Error UnboundLocalError

This makes “a” global inside exec, not f. Works as intended.

We’re unlikely to deprecate or restrict the current global syntax, as it works just fine for most users.

I do agree though that some of the examples in this thread read confusingly. Linters could do a useful job of flagging such cases. I’d suggest a rule that every global declaration must be (a) before any code in the function that uses the variable and (b) at at most the level of indentation of the least indented use of the variable. If such a rule is widely deployed in linters, maybe we can talk about deprecating non-conforming usage in Python itself, but as the experience of PEP 760 shows, it’s unlikely we’d actually make a change in the language core.

12 Likes

I think enforcing indentation isn’t quite enough when the variable reference is at the same indentation level as the global declaration but in a different code block, so the following code would pass your check but should IMHO be warned against:

a = 1
def f(b):
    if b:
        global a
        a += 1
    else:
        a = 0

So I still prefer my aforementioned rule that every global declaration has to be placed in a code block that covers/encloses all of the variable’s references (and yes, also before them).

3 Likes

I’d like to +1 @blhsing’s proposal.

As per @Jelle’s suggestion, it’d be good to focus on making this a linter rule at first, as this’d be much less controversial than a change in Python itself.

However, I’d still be in favor of eventually changing Python to outright reject instances that fail the rule, if this ever became a realistic possibility.

3 Likes

I think that the path to change something in the core should start with creating an alternative syntax, that reflects the special status od parser declarations. After that, warnings could be issued when the declaration is in a wierd place, both warning about the effects and promoting new syntax. In that case, a full deprecation of old syntax might happen by the time Python 4 would be created.

As for PEP 760, it provided a change to enforce best practices, while this is more about making python “global” syntax aligned with its behaviour. And having a good alternative beforehand would also help. Also, it might be a good idea to perform such a change outside of a standart deprecation lifecycle, giving people more time to adapt.

1 Like

By “special status”, you mean that they are statements, rather than using generalized constructs such as decorators and function calls?

I think they mean that they affect code generation, no matter where in the function they occur (even an unreachable branch). Similarly “yield”.

Ah. Then, that they are directives, not executable statements.

That definitely does require syntax, and @vladko312 if you want to design your own language, you have a number of choices as to how to do it. But there’s not a lot of point in inventing a new syntax for Python to use.

It is not useful to talk about “runtime” behavior of global and nonlocalsince they don’t have any. global and nonlocal are not actually imperative statements to be executed, instead they are declarations. The wording in the documentation does not make this clear as it makes too much assumption on the user’s knowledge on the working of the parser.

Combined with Python’s (undeserved) reputation of “hard-to-understand scoping rules” you get people giving out advice on avoiding the use of global and nonlocal.

IMO the existing syntax does not need deprecation, but it can lead to hard-to-read code. Syntax deprecations are not usually warranted unless the syntax leads to subtly wrong code or if it poses a large obstacle to future development (say if implementations would be moving away from the tokenizer-parser model to something else). If we can find some examples where the existing global and nonlocal syntax has caused hard-to-detect errors in real code, we can make a case for its deprecation. Right now global and nonlocal misuses tend to fail loudly with NameErrors , I believe linters rules and better documentation should suffice in improving readability.

5 Likes

Although it’s certainly not real code, the OP’s second example was enough to convince me that we ought to do something: it strikes me as a pattern that could easily arise in real code. The innocent hypothetical author’s intent here is clearly “if not b, print the value of the global variable a, otherwise print 2”, but the function is instead capable of overwriting the global, clearly not the author’s intent.

[07:10:48] $ python3
Python 3.11.2 (main, Apr 28 2025, 14:11:48) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 1
>>> def f(b):
...     if not b:
...         global a
...     else:
...         a = 2
...     print(a)
... 
>>> 
>>> a
1
>>> f(False)
1
>>> a
1
>>> f(True)
2
>>> a
2
>>> 

I would also call this a hard-to-detect error. You need to understand how Python interprets global in order to understand what’s going on here.

I can certainly recall times when I’ve ended up with something like this - it’s not something I’d write in “proper” code that’s going to survive a peer review, but sometimes you just need something quick-and-dirty:

...
def foo(...):
  ...
  if something:
    import a # or maybe `from x import a`, but it's the same idea
  else:
    a = bar()
  ...
  use(a)

The only difference here is that it uses import rather than global or nonlocal, and it only works because the effects of import take place when that line of code is reached, rather than declaratively. I could easily see myself as landing on the broken global example one day, if I hadn’t seen this thread and the necessary circumstances arose.

There’s absolutely no reason to allow this construction with global/nonlocal, which is why I suggested making Ben’s version of the rule (which would prohibit this example) into a linter rule with a view to potentially making it a full SyntaxError in future.

2 Likes

Agreed. I won’t call global broken, but the “statement” syntax is not useful. The current syntax might have stemmed from an implementation detail of the parser but got codified in the language reference after the years. If the syntax were designed now, I thinkglobal,nonlocal (and we’ll probably need a local) should be made into proper imperative statements that actually switch scopes of the names concerned.

In any case, the documentation on the scoping story needs major revision. The docs aren’t even consistent on what name binds to what scope.

import per documentation binds to the local scope (but it actually doesn’t always, if you have global or nonlocalbefore it. And I just reported this as a docs bug), making it somewhat similar to an assignment statement. This is required for common patterns like conditional import code

try:
    # regex is the PyPI drop-in replacement of re
    import regex as re_engine
except ImportError:
    # if regex is not installed, use the stdlib fallback
    import re as re_engine

if sys.version_info[0:2] >= (3, 14):
    from foo import Baz
else:
    # Suppose bar.Baz is deprecated in 3.14
    from bar import Baz

to work. Lazy imports tend to be less welcome but they are sometimes used to speedup startup times (imports are mostly a one-time cost). The rationale for putting import in if or try blocks is usually quite clear.

On the other hand, there is no actual utility in putting global within an if block. A name conditionally binding to a global or a local is not currently allowed. The presence of a (non-globaled, non-nonlocaled) name on the left side of an assignment statement makes it a local, subsequent reads must see it as a bound local. If one wants to conditionally assign to a global, then the local variables should be named appropriately to avoid conflict with the global. Catching the UnboundLocalError is a valid but sloppy code.

I doubt it’s possible to exhaustively deprecate and prohibit all of the confusing cases in scoping. The current documentation does not make any of this scoping story clear. The documentation is supposed to serve as a documentation of current behavior upon which linters and IDEs can rely, and more importantly people can read to learn. Instead a lot of the story needs to be inferred from the implementation and from the myriad of ways people write (good and bad) code.

Python as a widely-used educational medium in introductory courses should do better than telling learner drivers to “forget reading the traffic code, just drive like your average taxi driver in town“.

1 Like

That is an extraordinary claim that requires extraordinary evidence. An implementation detail? Highly unlikely.

If it conditionally binds to a global, or doesn’t do anything with that name at all, it makes much better sense to have the global statement right next to the assignment. This is actual utility.

Which is why it’s better to leave this up to programmers and linters.