Is a string formatter that pulls variables from its calling scope bad practice?

I have some code that does an awful lot of string formatting, Often, I end up with code along the lines of:

"...".format(x=x, y=y, z=z, foo=foo, ...)

Where I'm trying to interpolate a large number of variables into a large string.

Is there a good reason not to write a function like this that uses the inspect module to find variables to interpolate?

import inspect

def interpolate(s):
    return s.format(**inspect.currentframe().f_back.f_locals)

def generateTheString(x):
    y = foo(x)
    z = x + y
    # more calculations go here
    return interpolate("{x}, {y}, {z}")

Update: Python 3.6 has this feature (a more powerful variant) builtin:

x, y, z = range(3)
print(f"{x} {y + z}")
# -> 0 3

See PEP 0498 -- Literal String Interpolation


It[manual solution] leads to somewhat surprising behaviour with nested functions:

from callerscope import format

def outer():
    def inner():
        nonlocal a
        try:
            print(format("{a} {b}"))
        except KeyError as e:
            assert e.args[0] == 'b'
        else:
            assert 0

    def inner_read_b():
        nonlocal a
        print(b) # read `b` from outer()
        try:
            print(format("{a} {b}"))
        except KeyError as e:
            assert 0
    a, b = "ab"
    inner()
    inner_read_b()

Note: the same call succeeds or fails depending on whether a variable is mentioned somewhere above or below it.

Where callerscope is:

import inspect
from collections import ChainMap
from string import Formatter

def format(format_string, *args, _format=Formatter().vformat, **kwargs):
    caller_locals = inspect.currentframe().f_back.f_locals
    return _format(format_string, args, ChainMap(kwargs, caller_locals))

A simpler and safer approach would be the code below. inspect.currentframe isn't available on all implementation of python so your code would break when it isn't. Under jython, ironpython, or pypy it might not be available because it seems to be a cpython thing. This makes your code less portable.

print "{x}, {y}".format(**vars())

this technique is actually described in the Python tutorial's Input and Output chapter

This could also be done by passing the table as keyword arguments with the ‘**’ notation. This is particularly useful in combination with the new built-in vars() function, which returns a dictionary containing all local variables.

also in the python docs for inspect.currentframe

CPython implementation detail: This function relies on Python stack frame support in the interpreter, which isn’t guaranteed to exist in all implementations of Python. If running in an implementation without Python stack frame support this function returns None.


The good old mailman has a function _ that does exactly this thing:

def _(s):
    if s == '':
        return s
    assert s
    # Do translation of the given string into the current language, and do
    # Ping-string interpolation into the resulting string.
    #
    # This lets you write something like:
    #
    #     now = time.ctime(time.time())
    #     print _('The current time is: %(now)s')
    #
    # and have it Just Work.  Note that the lookup order for keys in the
    # original string is 1) locals dictionary, 2) globals dictionary.
    #
    # First, get the frame of the caller
    frame = sys._getframe(1)
    # A `safe' dictionary is used so we won't get an exception if there's a
    # missing key in the dictionary.
    dict = SafeDict(frame.f_globals.copy())
    dict.update(frame.f_locals)
    # Translate the string, then interpolate into it.
    return _translation.gettext(s) % dict

So if Barry Warsaw can do that, why can't we?


In the inspect module, currentframe is defined like this:

if hasattr(sys, '_getframe'):
    currentframe = sys._getframe
else:
    currentframe = lambda _=None: None

So unless sys has a _getframe attribute, the interpolate function will not work.

The docs for sys._getframe say:

CPython implementation detail: This function should be used for internal and specialized purposes only. It is not guaranteed to exist in all implementations of Python.


Writing

"{x}, {y}, {z}".format(**vars())

in the function body is not that much longer than

interpolate("{x}, {y}, {z}")

and your code will be more portable.