diff --git a/doc/src/sgml/plpython.sgml b/doc/src/sgml/plpython.sgml
index 1921915..ac989a3 100644
--- a/doc/src/sgml/plpython.sgml
+++ b/doc/src/sgml/plpython.sgml
@@ -164,13 +164,6 @@
   </para>
 
   <para>
-   See also the
-   document <ulink url="https://docs.python.org/3/whatsnew/3.0.html">What's
-   New In Python 3.0</ulink> for more information about porting to
-   Python 3.
-  </para>
-
-  <para>
    It is not allowed to use PL/Python based on Python 2 and PL/Python
    based on Python 3 in the same session, because the symbols in the
    dynamic modules would clash, which could result in crashes of the
@@ -179,6 +172,90 @@
    a mismatch is detected.  It is possible, however, to use both
    PL/Python variants in the same database, from separate sessions.
   </para>
+
+  <sect2 id="plpython-python3-conversion">
+   <title>Converting from Python 2 to Python 3</title>
+
+   <para>
+    See the
+    document <ulink url="https://docs.python.org/3/whatsnew/3.0.html">What's
+    New In Python 3.0</ulink> for the Python community's information and
+    recommendations about porting to Python 3.
+   </para>
+
+   <para>
+    <productname>PostgreSQL</productname> provides some support for helping
+    you to convert existing Python 2 routines to Python 3.  In an
+    installation built with Python 3, there is an
+    extension <filename>convert_python3</filename> that changes functions
+    and procedures from the <literal>plpythonu</literal>
+    and <literal>plpython2u</literal> languages to
+    the <literal>plpython3u</literal> language.  While doing so, it applies
+    the <filename>2to3</filename> tool described in the above document to
+    the body of each such routine.
+   </para>
+
+   <para>
+    Using <filename>convert_python3</filename> can be as simple as:
+<programlisting>
+CREATE EXTENSION convert_python3;
+CALL convert_python3_all();
+</programlisting>
+    This must be done as database superuser.  If you wish, you can drop the
+    extension once you're done converting everything.
+   </para>
+
+   <para>
+    Since <filename>convert_python3</filename> is Python 3 code, be careful
+    not to install or run it in a session that has previously executed any
+    Python 2 code.  As explained above, that won't work.
+   </para>
+
+   <para>
+    <function>convert_python3_all</function> has two optional arguments: the
+    name of the conversion tool to use (by default <literal>2to3</literal>,
+    but you might for instance need to provide a full path name) and any
+    special command-line options to provide to it.  You might for example
+    want to adjust the set of <quote>fixer</quote> rules
+    that <literal>2to3</literal> applies:
+<programlisting>
+CALL convert_python3_all(options =&gt; '-f idioms -x apply');
+</programlisting>
+    See <literal>2to3</literal>'s
+    <ulink url="https://docs.python.org/3/library/2to3.html">documentation</ulink>
+    for more information.
+   </para>
+
+   <para>
+    The <filename>convert_python3</filename> extension also provides a
+    procedure that converts just one Python 2 function at a time:
+<programlisting>
+CALL convert_python3_one('myfunc(int)');
+</programlisting>
+    The argument is the target function's OID, which can be written as
+    a <type>regprocedure</type> constant (see
+    <xref linkend="datatype-oid"/>).  The main reason to use this would be
+    if you need to use different options for different functions.  It has
+    the same optional arguments as <function>convert_python3_all</function>:
+<programlisting>
+CALL convert_python3_one('otherfunc(text)', tool =&gt; '/usr/bin/2to3',
+                         options =&gt; '-f idioms');
+</programlisting>
+   </para>
+
+   <para>
+    If you have needs that go beyond this, consult the source code for
+    the <filename>convert_python3</filename> extension (it's just a
+    couple of <literal>plpython3u</literal> procedures) and adapt those
+    procedures as necessary.
+   </para>
+
+   <para>
+    Keep in mind that if you've constructed any <command>DO</command> blocks
+    that use Python 2 code, those will have to be fixed up manually,
+    wherever the source code for them exists.
+   </para>
+  </sect2>
  </sect1>
 
  <sect1 id="plpython-funcs">
diff --git a/src/pl/plpython/Makefile b/src/pl/plpython/Makefile
index 9e95285..03f858b 100644
--- a/src/pl/plpython/Makefile
+++ b/src/pl/plpython/Makefile
@@ -38,6 +38,9 @@ DATA = $(NAME)u.control $(NAME)u--1.0.sql
 ifeq ($(python_majorversion),2)
 DATA += plpythonu.control plpythonu--1.0.sql
 endif
+ifeq ($(python_majorversion),3)
+DATA += convert_python3.control convert_python3--1.0.sql
+endif
 
 # header files to install - it's not clear which of these might be needed
 # so install them all.
diff --git a/src/pl/plpython/convert_python3--1.0.sql b/src/pl/plpython/convert_python3--1.0.sql
new file mode 100644
index 0000000..3444ac0
--- /dev/null
+++ b/src/pl/plpython/convert_python3--1.0.sql
@@ -0,0 +1,149 @@
+/* convert_python3--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION convert_python3" to load this file. \quit
+
+-- This module provides two procedures, one to convert all python2
+-- functions and one to do just one.  They're nearly identical, and
+-- in principle convert_python3_all() could be written as a loop
+-- around convert_python3_one().  It's not done that way since
+-- creating a temp directory for each function in a bulk conversion
+-- could get expensive.
+
+-- For some benighted reason, lib2to3 has exactly no documented API,
+-- so we must use the command-line API "2to3" instead.  User may pass
+-- in the name of that program (in case it's not in the server's PATH)
+-- as well as any desired options for it (perhaps some -f switches).
+
+create procedure convert_python3_all(tool text default '2to3',
+                                     options text default '')
+language plpython3u as $$
+import re, subprocess, tempfile
+
+# pattern to extract just the function header from pg_get_functiondef result
+aspat = re.compile("^(.*?\nAS )", re.DOTALL)
+# pattern for replacing LANGUAGE portion
+langpat = re.compile("\n LANGUAGE plpython2?u\n")
+
+# collect info about functions to update
+rv = plpy.execute("""
+select p.oid::pg_catalog.regprocedure as funcid,
+       pg_catalog.pg_get_functiondef(p.oid) as fd,
+       prosrc as body
+from pg_catalog.pg_proc p join pg_catalog.pg_language l on p.prolang = l.oid
+where lanname in ('plpythonu', 'plpython2u')
+order by proname
+""")
+
+# Make a temp directory to hold the file for 2to3 to work on.
+with tempfile.TemporaryDirectory() as tmpdirname:
+
+    # process each function
+    for r in rv:
+        # emit notices so user can tell which function failed, if one does
+        plpy.notice("converting function " + r["funcid"])
+
+        # extract everything but the body from pg_get_functiondef result
+        m = aspat.match(r["fd"])
+        if not m:
+            raise ValueError('unexpected match failure')
+        fheader = m.group(1)
+
+        # replace the language clause
+        fheader = langpat.sub("\n LANGUAGE plpython3u\n", fheader, 1)
+
+        # put body in a temp file so we can apply 2to3
+        f = open(tmpdirname + "/temp.py", mode = 'w')
+        f.write(r["body"])
+        f.close()
+
+        # apply 2to3 to body
+        subprocess.check_call(tool + " " + options + " --no-diffs -w " + tmpdirname + "/temp.py", shell=True)
+        f = open(tmpdirname + "/temp.py", mode = 'r')
+        fbody = f.read()
+        f.close()
+
+        # ensure check_function_bodies is enabled
+        plpy.execute("set local check_function_bodies = true")
+
+        # construct and execute SQL command to replace the function
+        newstmt = fheader + plpy.quote_literal(fbody)
+        # uncomment this for debugging purposes:
+        # plpy.info(newstmt)
+        plpy.execute(newstmt)
+
+        # commit after each successful replacement, in case a later one fails
+        plpy.commit()
+$$;
+
+-- The above procedure has to be superuser-only since it trivially allows
+-- executing random programs.  But you'd have to be superuser anyway
+-- to replace the definitions of plpython functions.
+
+revoke all on procedure convert_python3_all(text, text) from public;
+
+
+-- Here's the one-function version.
+
+create procedure convert_python3_one(funcid regprocedure,
+                                     tool text default '2to3',
+                                     options text default '')
+language plpython3u as $$
+import re, subprocess, tempfile
+
+# pattern to extract just the function header from pg_get_functiondef result
+aspat = re.compile("^(.*?\nAS )", re.DOTALL)
+# pattern for replacing LANGUAGE portion
+langpat = re.compile("\n LANGUAGE plpython2?u\n")
+
+# collect info about function to update, making sure it's the right language
+plan = plpy.prepare("""
+select p.oid::pg_catalog.regprocedure as funcid,
+       pg_catalog.pg_get_functiondef(p.oid) as fd,
+       prosrc as body
+from pg_catalog.pg_proc p join pg_catalog.pg_language l on p.prolang = l.oid
+where p.oid = $1 and lanname in ('plpythonu', 'plpython2u')
+""", ["pg_catalog.regprocedure"])
+
+rv = plpy.execute(plan, [funcid])
+
+# Make a temp directory to hold the file for 2to3 to work on.
+with tempfile.TemporaryDirectory() as tmpdirname:
+
+    # process each function (we only expect one, but it's easy to loop)
+    for r in rv:
+        # extract everything but the body from pg_get_functiondef result
+        m = aspat.match(r["fd"])
+        if not m:
+            raise ValueError('unexpected match failure')
+        fheader = m.group(1)
+
+        # replace the language clause
+        fheader = langpat.sub("\n LANGUAGE plpython3u\n", fheader, 1)
+
+        # put body in a temp file so we can apply 2to3
+        f = open(tmpdirname + "/temp.py", mode = 'w')
+        f.write(r["body"])
+        f.close()
+
+        # apply 2to3 to body
+        subprocess.check_call(tool + " " + options + " --no-diffs -w " + tmpdirname + "/temp.py", shell=True)
+        f = open(tmpdirname + "/temp.py", mode = 'r')
+        fbody = f.read()
+        f.close()
+
+        # ensure check_function_bodies is enabled
+        plpy.execute("set local check_function_bodies = true")
+
+        # construct and execute SQL command to replace the function
+        newstmt = fheader + plpy.quote_literal(fbody)
+        # uncomment this for debugging purposes:
+        # plpy.info(newstmt)
+        plpy.execute(newstmt)
+$$;
+
+-- The above procedure has to be superuser-only since it trivially allows
+-- executing random programs.  But you'd have to be superuser anyway
+-- to replace the definitions of plpython functions.
+
+revoke all on procedure convert_python3_one(regprocedure, text, text) from public;
diff --git a/src/pl/plpython/convert_python3.control b/src/pl/plpython/convert_python3.control
new file mode 100644
index 0000000..8debb3b
--- /dev/null
+++ b/src/pl/plpython/convert_python3.control
@@ -0,0 +1,5 @@
+# convert_python3 extension
+comment = 'convert plpython[2]u functions to plpython3u'
+default_version = '1.0'
+relocatable = true
+requires = 'plpython3u'
