43

I have two scripts, the main is in Python 3, and the second one is written in Python 2 (it also uses a Python 2 library).

There is one method in the Python 2 script I want to call from the Python 3 script, but I don't know how to cross this bridge.

Gary Ye
  • 727
  • 1
  • 6
  • 9
  • Does that particular method rely on Python 2-specific functionality? Could it be converted to Python 3? – jonrsharpe Jan 09 '15 at 15:27
  • It's using a Python 2 only library (Obspy, currently not supported in 3.x) . – Gary Ye Jan 09 '15 at 15:28
  • 2
    If it's not supported in 3x then simply you can't run it. –  Jan 09 '15 at 15:28
  • Then you will have to convert the Python 3 script back to Python 2 if you want to use them together. – jonrsharpe Jan 09 '15 at 15:29
  • Converting back is not an option for me. – Gary Ye Jan 09 '15 at 15:30
  • @GaryYe There is no method except converting them, since that module is not supported in 3x, you have to convert 3x to 2x. Otherwise there is no solution for your problem. –  Jan 09 '15 at 15:31
  • Not possible... My idea is now as follows: I build a main Python 2 script, which does its task since it has only one method I want to call! Then I give the Python 2 script the corresponding program (method) arguments. Is there a neat way? – Gary Ye Jan 09 '15 at 15:34
  • 4
    Another way might be to call the python 2 script via a system call and store the results in a file... but only if all else fails. – tobias_k Jan 09 '15 at 15:34
  • 2
    @GaryYe One idea if the output of what you need can be converted to a text file or something similar, you can create a python2 script outputting what you need (to a file or console). Then you can run the script via subprocess from python3 (assummnig you have both Python 2.x and Python 3.x installed) and get the results via subprocess or opening the file created by the Python 2 script. – avenet Jan 09 '15 at 15:35
  • 1
    Yep, just bridge it the same way you would bridge it for two completely different languages.... just need to come up with some kind of interface that will work for whatever data you're trying to pass around – user3012759 Jan 09 '15 at 15:46
  • Question is not clear (IMHO). Running python 2 from python 3 means calling a python 2 interpreter also. Is that what you are asking? – lmblanes Feb 13 '18 at 11:48

8 Answers8

24

Calling different python versions from each other can be done very elegantly using execnet. The following function does the charm:

import execnet

def call_python_version(Version, Module, Function, ArgumentList):
    gw      = execnet.makegateway("popen//python=python%s" % Version)
    channel = gw.remote_exec("""
        from %s import %s as the_function
        channel.send(the_function(*channel.receive()))
    """ % (Module, Function))
    channel.send(ArgumentList)
    return channel.receive()

Example: A my_module.py written in Python 2.7:

def my_function(X, Y): 
    return "Hello %s %s!" % (X, Y)

Then the following function calls

result = call_python_version("2.7", "my_module", "my_function",  
                             ["Mr", "Bear"]) 
print(result) 
result = call_python_version("2.7", "my_module", "my_function",  
                             ["Mrs", "Wolf"]) 
print(result)

result in

Hello Mr Bear!
Hello Mrs Wolf!

What happened is that a 'gateway' was instantiated waiting for an argument list with channel.receive(). Once it came in, it as been translated and passed to my_function. my_function returned the string it generated and channel.send(...) sent the string back. On other side of the gateway channel.receive() catches that result and returns it to the caller. The caller finally prints the string as produced by my_function in the python 3 module.

Frank-Rene Schäfer
  • 2,586
  • 17
  • 38
  • 1
    Can you send and receive objects other than text ? – jadsq Jul 07 '17 at 14:46
  • 2
    Yes, you can! That's the nice thing about 'execnet'. It serializes objects in a compatible manner. – Frank-Rene Schäfer Jul 07 '17 at 15:49
  • @Frank-ReneSchäfer I tried running your function to call a python2 function that returns a tuple of floats and strings. Got an error saying `DumpError: can't serialize `. Any idea how I can possibly fix this? – IM94 Jul 25 '17 at 18:01
  • That's an numpy question. Nevertheless, you might use 'value.item()' to get the python type that can be serialized, or use 'numpy.asscalar(value)'. In any case, you might have to serialize on your own. You might not be able to find a solution without manual serialization, anyway. – Frank-Rene Schäfer Jul 26 '17 at 14:18
  • 1
    Can anyone comment on the performance of this method relative to pure Python 2? I assume there must be some penalty from the need to start up another Python interpreter and from the serialization and deserialization. – erobertc Feb 14 '19 at 19:45
  • 4
    [On the page](https://pypi.org/project/execnet/): "`execnet` currently is in maintenance-only mode, mostly because it is still the backend of the `pytest-xdist` plugin. Do not use in new projects." hmm. now what? – ijoseph Aug 27 '19 at 22:00
  • @ijoseph: I do not know what this is supposed to mean. May be, just use an older version or contact the authors. – Frank-Rene Schäfer Aug 29 '19 at 08:32
  • Is there a way to import the module once (expensive import) and then call the function multiple times without reimporting? – tslater May 20 '21 at 23:53
17

You could run python2 from bash using subprocess (python module) doing the following:

From python 3:

#!/usr/bin/env python3
import subprocess

python3_command = "py2file.py arg1 arg2"  # launch your python2 script using bash

process = subprocess.Popen(python3_command.split(), stdout=subprocess.PIPE)
output, error = process.communicate()  # receive output from the python2 script

Where output stores whatever python 2 returned

mikelsr
  • 419
  • 6
  • 13
  • 2
    If you want to wait for the subprocess to finish before executing the next Python line, consider using `subprocess.call` https://stackoverflow.com/a/89243/3822261 – Matt Kleinsmith Nov 01 '17 at 09:57
  • 11
    I don't understand how this is an answer. This use the python3 interpreter. – Matthew Lueder Nov 13 '17 at 22:10
  • 1
    for me it throws `OSError: [Errno 2] No such file or directory` (I'm trying to launch Py 3 from Py 2; the sample target script just has 1 print statement). Also I'm very confused on if this worked like frakman1 says or not like @MatthewLueder implies? – Hack-R May 01 '18 at 14:16
  • What does not work? Are you using the right path to call the command? – mikelsr Nov 06 '18 at 09:16
12

Maybe to late, but there is one more simple option for call python2.7 scripts:

script = ["python2.7", "script.py", "arg1"]    
process = subprocess.Popen(" ".join(script),
                                        shell=True,  
                                        env={"PYTHONPATH": "."})
Martin Koubek
  • 415
  • 3
  • 8
  • Even later ... By "python2.7" I assumed you mean the Python 2.7 executable, eg: '"C:\Program Files\Python27\python.exe"'. Note the nested quotes, to allow for the space in the Program Files directory, (which I still think is one of Microsoft's worst ever brailf**ts). – JonnyCab May 04 '21 at 13:52
3

I am running my python code with python 3, but I need a tool (ocropus) that is written with python 2.7. I spent a long time trying all these options with subprocess, and kept having errors, and the script would not complete. From the command line, it runs just fine. So I finally tried something simple that worked, but that I had not found in my searches online. I put the ocropus command inside a bash script:

#!/bin/bash

/usr/local/bin/ocropus-gpageseg $1

I call the bash script with subprocess.

command = [ocropus_gpageseg_path,  current_path]
process = subprocess.Popen(command,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
output, error = process.communicate()
print('output',output,'error',error)

This really gives the ocropus script its own little world, which it seems to need. I am posting this in the hope that it will save someone else some time.

excyberlabber
  • 557
  • 1
  • 6
  • 15
2

It works for me if I call the python 2 executable directly from a python 3 environment.

python2_command = 'C:\Python27\python.exe python2_script.py arg1'
process = subprocess.Popen(python2_command.split(), stdout=subprocess.PIPE)
output, error = process.communicate()

python3_command = 'python python3_script.py arg1'
process = subprocess.Popen(python3_command.split(), stdout=subprocess.PIPE)
output, error = process.communicate()
sparrow
  • 7,668
  • 9
  • 42
  • 65
1

I ended up creating a new function in the python3 script, which wraps the python2.7 code. It correctly formats error messages created by the python2.7 code and is extending mikelsr's answer and using run() as recommended by subprocess docs.

in bar.py (python2.7 code):

def foo27(input):
    return input * 2

in your python3 file:

import ast
import subprocess

def foo3(parameter):
    try:
        return ast.literal_eval(subprocess.run(
            [
                "C:/path/to/python2.7/python.exe", "-c", # run python2.7 in command mode
                "from bar import foo27;"+
                "print(foo27({}))".format(parameter) # print the output 
            ],
            capture_output=True,
            check=True
        ).stdout.decode("utf-8")) # evaluate the printed output
    except subprocess.CalledProcessError as e:
        print(e.stdout)
        raise Exception("foo27 errored with message below:\n\n{}"
                                .format(e.stderr.decode("utf-8")))
print(foo3(21))
# 42

This works when passing in simple python objects, like dicts, as the parameter but does not work for objects created by classes, eg. numpy arrays. These have to be serialized and re-instantiated on the other side of the barrier.

Zikoat
  • 113
  • 1
  • 7
0

Note: This was happening when running my python 2.x s/w in the liclipse IDE. When I ran it from a bash script on the command line it didn't have the problem. Here is a problem & solution I had when mixing python 2.x & 3.x scripts.

I am running a python 2.6 process & needed to call/execute a python 3.6 script. The environment variable PYTHONPATH was set to point to 2.6 python s/w, so it was choking on the followng:

File "/usr/lib64/python2.6/encodings/__init__.py", line 123
raise CodecRegistryError,\

This caused the 3.6 python script to fail. So instead of calling the 3.6 program directly I created a bash script which nuked the PYTHONPATH environment variable.

#!/bin/bash
export PYTHONPATH=
## Now call the 3.6 python scrtipt
./36psrc/rpiapi/RPiAPI.py $1
Eric Aya
  • 68,765
  • 33
  • 165
  • 232
-2

I recommend to convert the Python2 files to Python3:

https://pythonconverter.com/

Freeman
  • 5,246
  • 2
  • 41
  • 46
  • This sometimes doesn't work. Some Python2 modules don't work on Python 3. The script in the question uses a Python2 library, so this won't work – Andy Zhang Mar 23 '21 at 03:40