3

I often define variables that will never change, i.e. constants, at the top of a script/module. But recently I've been wondering if it makes sense to define them at the function scope level if they are only used by a single method.

LIST_OF_THINGS = [('a', 1), ('b', 2), ('c', 3)]
    
def some_op():
    for thing in LIST_OF_THINGS:
        do_something_with(thing)

While Python has no notion of constants, in this case LIST_OF_THINGS is never modified and only called by a single method, ever. If LIST_OF_THINGS was ever modified, it would be a hardcoded modification in a new release. Now while this is a simple case, I recently had the need for a data structure that later references other methods:

LIST_OF_OPS = {'foo': _call_foo, 'bar': _call_bar}  # Python throws a fit here
    
def _call_foo(): pass
    
def _call_bar(): pass
    
def some_op():
    for op in LIST_OF_OPS:
        LIST_OF_OPS[op]()

So I had two options:

  1. Lower the location of the "constant"' below the referenced methods
  2. Place the "constant" inside some_op

Again, a simple case, but when the structure of a constant or the number of constants is large, they can make the body of a function larger than it should be; this is really the only merit I see in having them defined at the function scope level.

3
  • I'm a little confused by the 'language-agnostic' tag here. The question is pretty specific to Python.
    – JimmyJames
    Feb 25, 2021 at 20:40
  • @JimmyJames Language of preference for easy of example.
    – pstatix
    Feb 26, 2021 at 3:27
  • The problem is that you used a language that has a specific meaning for the term 'global' that doesn't have the same meaning for languages in general.
    – JimmyJames
    Feb 26, 2021 at 16:08

3 Answers 3

3

You’re well on your way to making a global.

The principle of data hiding tells us to limit exposure of details. Constants are details. The wider the scope the wider the exposure. The correct scope for variables and constants is the narrowest scope that works. Having them visible to code that doesn’t need them harms readability.

If there is a need to define them elsewhere the answer isn’t a wider scope. It’s passing them to where they are needed. This used to just be called reference passing but the fancy new term for it is pure dependency injection. It lets you separate use from construction. It works for constants just as well.

But if it can be defined where it is used I don’t see any reason to make it visible outside of that. It’s far easier to read the code when you limit scope.

11
  • I disagree with the "pass everywhere", unless creating an object that has dependencies. If there are a list of functions that all need access to a particular variable (say a hex constant), and these functions are exposed as a public API, defining them internally to each function is wasteful and making it visible at the global scope makes sense. Again, that's only because they are used in multiple places.
    – pstatix
    Feb 25, 2021 at 18:33
  • 1
    I am not advocating creating copies everywhere. I’m denouncing using globals to solve this problem. You should not be reaching out to find things. When you do you create hidden dependencies that make your code brittle. Feb 25, 2021 at 18:37
  • So 4 functions all use the same variable internally; each has its own responsibility and exposed via an API. Why would declaring the variable, say ROOT_ID = 0x0115, be creating a hidden dependency? I would agree that create a global variable would be bad, but a constant should be perfectly fine.
    – pstatix
    Feb 25, 2021 at 18:47
  • I'm not sure if you know this or not but global is a keyword and has a specific meaning in Python that is different from the general term 'global'. I think you mean it in the general programming sense here but it's not clear. Everything declared at the module scope is global in Python. It's impossible to avoid using global declarations in Python.
    – JimmyJames
    Feb 25, 2021 at 19:14
  • 2
    @madeslurpy the shared mutable state of a global variable is indeed worse than a shared constant. However, a global/widely-scoped constant is still harmful to readability. A limited scope gives me confidence that I know what I’m impacting when I redefine the constant. Please limit what I have to think about when I code. Feb 25, 2021 at 20:21
1

Your second example is a little weird. It's not clear why you define this as a dict when you only show using the values. If you need the key and the value, I would generally prefer the for key, value in dict.items() idiom.

That aside, there's a third option here: don't declare a variable at all. You could simply do this:

def some_op():
    for op in [_call_foo, _call_bar]:
        op()

If you come from a 'curly-brace' background as I do, this might seem grotesquely wrong but as I have spent more time writing Python, I've actually started to question the idea that all 'constants' need to be given a name. I think it actually hurt readability in a lot of cases, especially if they are always declared at the top of the file. Why force the person reading your code to pause, scroll somewhere else and then come back to where they were. There's really no sense in that and I think it's just one of those things that's done because that's what you are 'supposed' to do.

Of course, this might be a trivial example and if you have a longer list, you would want to give it a name because of that. I think that really comes down to why you are giving something a name. Is it just to organize the code or does the name have some meaning? For example if you name a list of numbers, FIRST_TEN_PRIMES is far more interesting and informative than NUMBERS. In the latter case, I don't see much point in declaring it at all.

As far as the scope goes, this is a little more tricky. In general a tighter scope is preferred. If you only need it in the one method, I would generally declare it there. However, if you think this might be more generally useful or needed in other parts of the code, it might be worth putting it at the module scope. You can also suggest that it is 'private' with an underscore prefix. There has been at least one time where I had a bug because I didn't realize that my local declaration of a name was shadowing a module declaration. Something to think about. Again, I would stick to the tightest scoping possible in most cases.

6
  • Meaningless intermediate names are wrong in any language. Please don't blame the curly-brace languages for the stupid things people do with them. Feb 25, 2021 at 23:22
  • @candied_orange Please don't make (incorrect) assumptions about my meaning. I didn't blame the language at all. I blame the people who pushed ridiculous 'rules' like single-return, Hungarian warts, interfaces for every class, etc. I also blame the people who blindly followed those rules without question. It's about culture.
    – JimmyJames
    Feb 26, 2021 at 16:12
  • The correct response to an incorrect assumption is to edit your answer and remove the ambiguity that caused it. I agree with your assessment of the “ridiculous rules” with the exception of single return. That rule is language dependent. It’s valid in languages that don’t have finally, destructors, or some other method of unifying cleanup. This is important even when there is no cleanup to do now because it may need to be added later. Which is why newer languages added ways to do that cleanly. Anyway, names are very important but putting them where they're not needed is bad in any language. Feb 26, 2021 at 18:14
  • There is no ambiguity, your interpretation was presumptive.
    – JimmyJames
    Feb 26, 2021 at 19:32
  • @candied_orange Frankly, I don't get why you are telling me to 'remove the ambiguity' when your answer is literally using the term global in an ambiguous way (i.e. differently than the question uses it) and have not fixed that.
    – JimmyJames
    Feb 26, 2021 at 19:42
0

My "policy" is:

  • For small, simple things, inside some_op,
  • For more complex things, directly above some_op,
  • For things that I treat as ad-hoc configuration, even if they are used only in one function (e.g. switch for a library version), top of file.

Not the answer you're looking for? Browse other questions tagged or ask your own question.