Brainfuck to Python compiler
The way you define your various functions is inconsistent:


you define add, sub, putch, yet use data[index] = getch()[0] directly in the generated code;
you group together multiple +, -, &lt;, and &gt; but neither . nor , (not that this last one would be of great use but still, for the sake of consistency);
you access global data into those functions;
putch is defined using a lambda but add and sub uses def…


Since [ and ] are special anyway as they do more than producing a string, I'd rather choose to either:


inline everything in the generated code;
or only use functions that take data, index and count as parameters.




I would also generate code that respect some standard practices: defining a main function and wrapping its call into an if __name__ == '__main__': guard. This would mean starting level at 1 as you would generate code that is inside a def main():.

You should also take a look at textwrap.dedent to simplify your header (and thus footer too) generation.

And since you are using formated strings, you could make directly use of the variable width alignment such as f'{"": &gt;{level*4}}rest of the code'.



Storing generated instructions into a list that is returned at the end feels like your function would benefit from being turned into a generator. Especially if you plan on writing the generated string to a file, this would help interleaving computation and waiting for I/O.



Lastly your assertion feels off as they can be turned off at runtime. Instead I would raise a real exception such as ValueError with a proper error message.



Proposed improvements with code inlined:

import itertools
from textwrap import dedent
from contextlib import suppress


BYTECODES = {
        '+': '{0: &gt;{1}}data[index] = (data[index] + {2}) % 256',
        '-': '{0: &gt;{1}}data[index] = (data[index] - {2}) % 256',
        '&lt;': '{0: &gt;{1}}index -= {2}',
        '&gt;': '{0: &gt;{1}}index += {2}',
        '.': '{0: &gt;{1}}for _ in range({2}):\n{0: &gt;{1}}    print(chr(data[index]), end="")',
        ',': '{0: &gt;{1}}for _ in range({2}):\n{0: &gt;{1}}    data[index] = getch()[0]',
}


def pairwise(iterable):
    a, b = itertools.tee(iterable)
    next(b, None)
    yield from itertools.zip_longest(a, b)


def compile(sourcecode):
    yield dedent("""\
            from collections import defaultdict
            from msvcrt import getch


            def main():
                index = 0
                data = defaultdict(int)""")

    level = count = 1
    for operation, next_operation in pairwise(sourcecode):
        if operation == '[':
            yield f'{"": &gt;{level*4}}while data[index]:'
            level += 1
        elif operation == ']':
            level -= 1
            if level &lt; 1:
                raise ValueError('Unbalanced brackets, found ] before opening [')
        elif operation == next_operation:
            count += 1
        else:
            with suppress(KeyError):
                yield BYTECODES[operation].format('', level * 4, count)
            count = 1

    yield dedent("""\


            if __name__ == '__main__':
                main()""")


def translate(sourcecode):
    return '\n'.join(compile(sourcecode))


if __name__ == '__main__':
    code = '++++++++++[&gt;+++++++&gt;++++++++++&gt;+++&gt;+&lt;&lt;&lt;&lt;-]&gt;++.&gt;+.+++++++..+++.&gt;++.&lt;&lt;+++++++++++++++.&gt;.+++.------.--------.&gt;+.&gt;.'
    for line in compile(code):
        print(line)

    print('*------*')
    print(translate(code))


Proposed improvements with functions defined:

import itertools
from textwrap import dedent
from contextlib import suppress


BYTECODES = {
        '+': '{0: &gt;{1}}add(data, index, {2})',
        '-': '{0: &gt;{1}}sub(data, index, {2})',
        '&lt;': '{0: &gt;{1}}index -= {2}',
        '&gt;': '{0: &gt;{1}}index += {2}',
        '.': '{0: &gt;{1}}putchar(data, index, {2})',
        ',': '{0: &gt;{1}}getchar(data, index, {2})',
}


def pairwise(iterable):
    a, b = itertools.tee(iterable)
    next(b, None)
    yield from itertools.zip_longest(a, b)


def compile(sourcecode):
    yield dedent("""\
            from collections import defaultdict
            from msvcrt import getch


            def add(data, index, x):
                data[index] = (data[index] + x) % 256


            def sub(data, index, x):
                data[index] = (data[index] - x) % 256


            def putchar(data, index, x):
                for _ in range(x):
                    print(chr(data[index]), end='')


            def getchar(data, index, x):
                for _ in range(x):
                    data[index] = getch()[0]


            def main():
                index = 0
                data = defaultdict(int)""")

    level = count = 1
    for operation, next_operation in pairwise(sourcecode):
        if operation == '[':
            yield f'{"": &gt;{level*4}}while data[index]:'
            level += 1
        elif operation == ']':
            level -= 1
            if level &lt; 1:
                raise ValueError('Unbalanced brackets, found ] before opening [')
        elif operation == next_operation:
            count += 1
        else:
            with suppress(KeyError):
                yield BYTECODES[operation].format('', level * 4, count)
            count = 1

    yield dedent("""\


            if __name__ == '__main__':
                main()""")


def translate(sourcecode):
    return '\n'.join(compile(sourcecode))


if __name__ == '__main__':
    code = '++++++++++[&gt;+++++++&gt;++++++++++&gt;+++&gt;+&lt;&lt;&lt;&lt;-]&gt;++.&gt;+.+++++++..+++.&gt;++.&lt;&lt;+++++++++++++++.&gt;.+++.------.--------.&gt;+.&gt;.'
    for line in compile(code):
        print(line)

    print('*------*')
    print(translate(code))


As you can see, differences are minimal but generated code feels neater for the second version. I also kept the translate function for ease of use but the generator is simple enough to use on its own.