Running code snippets in a background thread
Sorry, didn't have time to look at the issue of preventing the infinite loops, but here is a way to address the printing issue.

Enforce thread-local printing

As you've found, because each thread shares sys.stdout there isn't a good way to redirect printing from distinct threads to distinct streams.
As you discovered, and as is documented, contextlib.redirect_stdout does not work in threaded applications, and has no effect on the output of subprocesses.
However, because you're using Python 3 print is a function, and because Python is a dynamic language, we can (ab)use that.

Consider the following function signature:

def print(*objects, sep='', end='\n', file=sys.stdout, flush=False)


An astute reader may notice that this is Python 3's print function. Suppose we changed it just a little bit, to not use
sys.stdout as the default stream and instead use a thread-local stream. Now we may be tempted to use the builtin threading.local function,
however that will make it hard to recover the output after the fact. Let's start by making a mechanism for getting a thread-local output stream.

thread_local_data = {
    'stdout': {},
    'stderr': {}
}

def register_thread(thread_ident=None):
    """Register a thread with the monitor."""

    thread_ident = thread_ident or threading.get_ident()

    thread_local_data['stdout'][thread_ident] = io.StringIO()
    thread_local_data['stderr'][thread_ident] = io.StringIO()

def unregister_thread(thread_ident=None):
    """Unregister a thread with the monitor."""

    thread_ident = thread_ident or threading.get_ident()

    thread_local_data['stdout'][thread_ident].close()
    thread_local_data['stderr'][thread_ident].close()

def get_thread_stdout(thread_ident=None):
    """Get output stream for a thread."""

    thread_ident = thread_ident or threading.get_ident()
    return thread_local_data['stdout'][thread_ident]

def get_thread_stderr(thread_ident=None):
    """Get error stream for a thread."""

    thread_ident = thread_ident or threading.get_ident()
    return thread_local_data['stderr'][thread_ident]

original_print = print
def print(*objects, sep='', end='\n', file=None, flush=False):
    if file is None or file is sys.stdout:
        file = get_thread_stdout()
    if file is sys.stderr:
        file = get_thread_stderr()

    original_print(*objects, sep=sep, end=end, file=file, flush=flush)


You can test a simplified version of it right from the REPL - notice that we now have a print function in our globals() dict,
where there was not one previously. Then we can see 

&gt;&gt;&gt; old_print=globals()['__builtins__'].print
&gt;&gt;&gt; old_print("test")
test
&gt;&gt;&gt; def print(): old_print("I WIN")
...
&gt;&gt;&gt; import pprint
&gt;&gt;&gt; pprint.pprint(globals())
{'__annotations__': {},
 '__builtins__': &lt;module 'builtins' (built-in)&gt;,
 '__doc__': None,
 '__loader__': &lt;class '_frozen_importlib.BuiltinImporter'&gt;,
 '__name__': '__main__',
 '__package__': None,
 '__spec__': None,
 'old_print': &lt;built-in function print&gt;,
 'pprint': &lt;module 'pprint' from 'C:\\Users\\dobermil\\AppData\\Local\\Programs\\Python\\Python36-32\\lib\\pprint.py'&gt;,
 'print': &lt;function print at 0x02BD5198&gt;}
&gt;&gt;&gt; exec("print()", globals())
I WIN


Now that we have the mechanism to print as we want to, you just need to make sure you register the thread before starting it. That will setup the thread-local stdout.