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_OPSop

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.

JimmyJames supports Canada
  • 30,578
  • 3
  • 59
  • 108
pstatix
  • 1,047

3 Answers3

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.

candied_orange
  • 119,268
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.

JimmyJames supports Canada
  • 30,578
  • 3
  • 59
  • 108
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.
Pedja
  • 1