I personally found 3 possible solutions to this problem that work well in different situations:
Option 1 - Set ansible_python_interpreter: /usr/bin/python3 for hosts that have python3 installed by default
I think this is the superior method for solving the problem if you have a way to group your hosts by whether or not they have python3 installed by default. As far as I'm aware, python3 is available on all Ubuntu releases 16.04 and higher.
- If all your hosts definitely have python3, you could add the variable to yourgroup_vars/all.yml(or equivalent):
# group_vars/all.yml
ansible_python_interpreter: /usr/bin/python3
- If some of your hosts don't have python3and you have a way to tag them when using dynamic inventory (e.g. AWS tagging forec2.py), you could apply the variable to certain hosts like this:
# group_vars/tag_OS_ubuntu1804.yml
ansible_python_interpreter: /usr/bin/python3
- If you use static inventory and are able to group hosts based on whether they have python3, you could do something like this:
# inventory/hosts
[python2_hosts]
centos7_server
[python3_hosts]
u1804_server
[python3_hosts:vars]
ansible_python_interpreter=/usr/bin/python3
I like this option the most because it requires no changes on the remote host and only minor changes to variables, as opposed to options 2 and 3, which require additions to every playbook.
Option 2 - Install Python 2 using raw
This option requires putting a play at the top of every playbook with gather_facts: false that uses raw to install python:
- name: install python2 on all instances
  hosts: "*"
  gather_facts: false
  tasks:
    - name: run apt-get update and install python
      raw: "{{ item }}"
      loop:
        - sudo apt-get update
        - sudo apt-get -y install python
      become: true
      ignore_errors: true
ignore_errors: true is required if you plan to run the play on hosts that don't have apt-get installed (e.g. anything RHEL-based), otherwise they will error out in the first play.
This solution works, but is the lowest on my list for a few reasons:
- Needs to go at the top of every playbook (as opposed to option 1)
- Assumes aptis on the system and ignores errors (as opposed to option 3)
- apt-getcommands are slow (as opposed to option 3)
Option 3 - Symlink /usr/bin/python -> /usr/bin/python3 using raw
I haven't seen this solution proposed by anyone else. It's not ideal, but I think it's superior to option 2 in a lot of ways. My suggestion is to use raw to run a shell command to symlink /usr/bin/python -> /usr/bin/python3 if python3 is on the system and python is not:
- name: symlink /usr/bin/python -> /usr/bin/python3
  hosts: "*"
  gather_facts: false
  tasks:
    - name: symlink /usr/bin/python -> /usr/bin/python3
      raw: |
        if [ -f /usr/bin/python3 ] && [ ! -f /usr/bin/python ]; then
          ln --symbolic /usr/bin/python3 /usr/bin/python; 
        fi
      become: true
This solution is similar to option 2 in that we need to put it at the top of every playbook, but I think it's superior in a few ways:
- Only creates the symlink in the specific case that python3is present andpythonis not -- it won't override Python 2 if it's already installed
- Does not assume aptis installed
- Can run against all hosts without any special error handling
- Is super fast compared to anything with apt-get
Obviously if you need Python 2 installed at /usr/bin/python, this solution is a no go and option 2 is better.
Conclusion
- I suggest using option 1 in all cases if you can. 
- I suggest using option 3 if your inventory is really large/complex and you have no way to easily group hosts with python3, making option 1 much more difficult and error-prone.
- I only suggest option 2 over option 3 if you need Python 2 installed at /usr/bin/python.
Sources