diff --git a/contrib/hstore_plpython/expected/hstore_plpython.out b/contrib/hstore_plpython/expected/hstore_plpython.out
index df49cd5f373a284738d5de90b95babf40472bbc7..1ab5feea93d79fe426d0b5b19d79a4ef445c8da6 100644
--- a/contrib/hstore_plpython/expected/hstore_plpython.out
+++ b/contrib/hstore_plpython/expected/hstore_plpython.out
@@ -68,12 +68,30 @@ AS $$
 val = [{'a': 1, 'b': 'boo', 'c': None}, {'d': 2}]
 return val
 $$;
- SELECT test2arr();
+SELECT test2arr();
                            test2arr                           
 --------------------------------------------------------------
  {"\"a\"=>\"1\", \"b\"=>\"boo\", \"c\"=>NULL","\"d\"=>\"2\""}
 (1 row)
 
+-- test python -> domain over hstore
+CREATE DOMAIN hstore_foo AS hstore CHECK(VALUE ? 'foo');
+CREATE FUNCTION test2dom(fn text) RETURNS hstore_foo
+LANGUAGE plpythonu
+TRANSFORM FOR TYPE hstore
+AS $$
+return {'a': 1, fn: 'boo', 'c': None}
+$$;
+SELECT test2dom('foo');
+             test2dom              
+-----------------------------------
+ "a"=>"1", "c"=>NULL, "foo"=>"boo"
+(1 row)
+
+SELECT test2dom('bar');  -- fail
+ERROR:  value for domain hstore_foo violates check constraint "hstore_foo_check"
+CONTEXT:  while creating return value
+PL/Python function "test2dom"
 -- test as part of prepare/execute
 CREATE FUNCTION test3() RETURNS void
 LANGUAGE plpythonu
diff --git a/contrib/hstore_plpython/sql/hstore_plpython.sql b/contrib/hstore_plpython/sql/hstore_plpython.sql
index 911bbd67fede733702ecd2bd405549c69762bbdd..2c54ee6aaad264be8ceb28ff9bcd7e4e2d31c568 100644
--- a/contrib/hstore_plpython/sql/hstore_plpython.sql
+++ b/contrib/hstore_plpython/sql/hstore_plpython.sql
@@ -60,7 +60,21 @@ val = [{'a': 1, 'b': 'boo', 'c': None}, {'d': 2}]
 return val
 $$;
 
- SELECT test2arr();
+SELECT test2arr();
+
+
+-- test python -> domain over hstore
+CREATE DOMAIN hstore_foo AS hstore CHECK(VALUE ? 'foo');
+
+CREATE FUNCTION test2dom(fn text) RETURNS hstore_foo
+LANGUAGE plpythonu
+TRANSFORM FOR TYPE hstore
+AS $$
+return {'a': 1, fn: 'boo', 'c': None}
+$$;
+
+SELECT test2dom('foo');
+SELECT test2dom('bar');  -- fail
 
 
 -- test as part of prepare/execute
diff --git a/src/backend/utils/cache/typcache.c b/src/backend/utils/cache/typcache.c
index 7aadc5d6ef76abc35390f209bb1371e8d6a9bdbd..f6450c402c3bbcc436de565edb4c3bde2d94416b 100644
--- a/src/backend/utils/cache/typcache.c
+++ b/src/backend/utils/cache/typcache.c
@@ -377,6 +377,7 @@ lookup_type_cache(Oid type_id, int flags)
 		typentry->typstorage = typtup->typstorage;
 		typentry->typtype = typtup->typtype;
 		typentry->typrelid = typtup->typrelid;
+		typentry->typelem = typtup->typelem;
 
 		/* If it's a domain, immediately thread it into the domain cache list */
 		if (typentry->typtype == TYPTYPE_DOMAIN)
@@ -791,6 +792,12 @@ load_typcache_tupdesc(TypeCacheEntry *typentry)
 	Assert(typentry->tupDesc->tdrefcount > 0);
 	typentry->tupDesc->tdrefcount++;
 
+	/*
+	 * In future, we could take some pains to not increment the seqno if the
+	 * tupdesc didn't really change; but for now it's not worth it.
+	 */
+	typentry->tupDescSeqNo++;
+
 	relation_close(rel, AccessShareLock);
 }
 
diff --git a/src/include/utils/typcache.h b/src/include/utils/typcache.h
index ea799a8894f8fe06e1d5420151bb292c7129e7fe..c203dabbd0aea2a5fbd5647ac94507e7b985ab4b 100644
--- a/src/include/utils/typcache.h
+++ b/src/include/utils/typcache.h
@@ -40,6 +40,7 @@ typedef struct TypeCacheEntry
 	char		typstorage;
 	char		typtype;
 	Oid			typrelid;
+	Oid			typelem;
 
 	/*
 	 * Information obtained from opfamily entries
@@ -75,9 +76,11 @@ typedef struct TypeCacheEntry
 	/*
 	 * Tuple descriptor if it's a composite type (row type).  NULL if not
 	 * composite or information hasn't yet been requested.  (NOTE: this is a
-	 * reference-counted tupledesc.)
+	 * reference-counted tupledesc.)  To simplify caching dependent info,
+	 * tupDescSeqNo is incremented each time tupDesc is rebuilt in a session.
 	 */
 	TupleDesc	tupDesc;
+	int64		tupDescSeqNo;
 
 	/*
 	 * Fields computed when TYPECACHE_RANGE_INFO is requested.  Zeroes if not
diff --git a/src/pl/plpython/expected/plpython_types.out b/src/pl/plpython/expected/plpython_types.out
index 893de301ddaffeb513b3601ec6650c27a43703b6..eda965a9e0d7f8810a67ced583457ff799e65483 100644
--- a/src/pl/plpython/expected/plpython_types.out
+++ b/src/pl/plpython/expected/plpython_types.out
@@ -765,6 +765,76 @@ SELECT * FROM test_type_conversion_array_domain_check_violation();
 ERROR:  value for domain ordered_pair_domain violates check constraint "ordered_pair_domain_check"
 CONTEXT:  while creating return value
 PL/Python function "test_type_conversion_array_domain_check_violation"
+--
+-- Arrays of domains
+--
+CREATE FUNCTION test_read_uint2_array(x uint2[]) RETURNS uint2 AS $$
+plpy.info(x, type(x))
+return x[0]
+$$ LANGUAGE plpythonu;
+select test_read_uint2_array(array[1::uint2]);
+INFO:  ([1], <type 'list'>)
+ test_read_uint2_array 
+-----------------------
+                     1
+(1 row)
+
+CREATE FUNCTION test_build_uint2_array(x int2) RETURNS uint2[] AS $$
+return [x, x]
+$$ LANGUAGE plpythonu;
+select test_build_uint2_array(1::int2);
+ test_build_uint2_array 
+------------------------
+ {1,1}
+(1 row)
+
+select test_build_uint2_array(-1::int2);  -- fail
+ERROR:  value for domain uint2 violates check constraint "uint2_check"
+CONTEXT:  while creating return value
+PL/Python function "test_build_uint2_array"
+--
+-- ideally this would work, but for now it doesn't, because the return value
+-- is [[2,4], [2,4]] which our conversion code thinks should become a 2-D
+-- integer array, not an array of arrays.
+--
+CREATE FUNCTION test_type_conversion_domain_array(x integer[])
+  RETURNS ordered_pair_domain[] AS $$
+return [x, x]
+$$ LANGUAGE plpythonu;
+select test_type_conversion_domain_array(array[2,4]);
+ERROR:  return value of function with array return type is not a Python sequence
+CONTEXT:  while creating return value
+PL/Python function "test_type_conversion_domain_array"
+select test_type_conversion_domain_array(array[4,2]);  -- fail
+ERROR:  return value of function with array return type is not a Python sequence
+CONTEXT:  while creating return value
+PL/Python function "test_type_conversion_domain_array"
+CREATE FUNCTION test_type_conversion_domain_array2(x ordered_pair_domain)
+  RETURNS integer AS $$
+plpy.info(x, type(x))
+return x[1]
+$$ LANGUAGE plpythonu;
+select test_type_conversion_domain_array2(array[2,4]);
+INFO:  ([2, 4], <type 'list'>)
+ test_type_conversion_domain_array2 
+------------------------------------
+                                  4
+(1 row)
+
+select test_type_conversion_domain_array2(array[4,2]);  -- fail
+ERROR:  value for domain ordered_pair_domain violates check constraint "ordered_pair_domain_check"
+CREATE FUNCTION test_type_conversion_array_domain_array(x ordered_pair_domain[])
+  RETURNS ordered_pair_domain AS $$
+plpy.info(x, type(x))
+return x[0]
+$$ LANGUAGE plpythonu;
+select test_type_conversion_array_domain_array(array[array[2,4]::ordered_pair_domain]);
+INFO:  ([[2, 4]], <type 'list'>)
+ test_type_conversion_array_domain_array 
+-----------------------------------------
+ {2,4}
+(1 row)
+
 ---
 --- Composite types
 ---
@@ -820,6 +890,64 @@ SELECT test_composite_type_input(row(1, 2));
                          3
 (1 row)
 
+--
+-- Domains within composite
+--
+CREATE TYPE nnint_container AS (f1 int, f2 nnint);
+CREATE FUNCTION nnint_test(x int, y int) RETURNS nnint_container AS $$
+return {'f1': x, 'f2': y}
+$$ LANGUAGE plpythonu;
+SELECT nnint_test(null, 3);
+ nnint_test 
+------------
+ (,3)
+(1 row)
+
+SELECT nnint_test(3, null);  -- fail
+ERROR:  value for domain nnint violates check constraint "nnint_check"
+CONTEXT:  while creating return value
+PL/Python function "nnint_test"
+--
+-- Domains of composite
+--
+CREATE DOMAIN ordered_named_pair AS named_pair_2 CHECK((VALUE).i <= (VALUE).j);
+CREATE FUNCTION read_ordered_named_pair(p ordered_named_pair) RETURNS integer AS $$
+return p['i'] + p['j']
+$$ LANGUAGE plpythonu;
+SELECT read_ordered_named_pair(row(1, 2));
+ read_ordered_named_pair 
+-------------------------
+                       3
+(1 row)
+
+SELECT read_ordered_named_pair(row(2, 1));  -- fail
+ERROR:  value for domain ordered_named_pair violates check constraint "ordered_named_pair_check"
+CREATE FUNCTION build_ordered_named_pair(i int, j int) RETURNS ordered_named_pair AS $$
+return {'i': i, 'j': j}
+$$ LANGUAGE plpythonu;
+SELECT build_ordered_named_pair(1,2);
+ build_ordered_named_pair 
+--------------------------
+ (1,2)
+(1 row)
+
+SELECT build_ordered_named_pair(2,1);  -- fail
+ERROR:  value for domain ordered_named_pair violates check constraint "ordered_named_pair_check"
+CONTEXT:  while creating return value
+PL/Python function "build_ordered_named_pair"
+CREATE FUNCTION build_ordered_named_pairs(i int, j int) RETURNS ordered_named_pair[] AS $$
+return [{'i': i, 'j': j}, {'i': i, 'j': j+1}]
+$$ LANGUAGE plpythonu;
+SELECT build_ordered_named_pairs(1,2);
+ build_ordered_named_pairs 
+---------------------------
+ {"(1,2)","(1,3)"}
+(1 row)
+
+SELECT build_ordered_named_pairs(2,1);  -- fail
+ERROR:  value for domain ordered_named_pair violates check constraint "ordered_named_pair_check"
+CONTEXT:  while creating return value
+PL/Python function "build_ordered_named_pairs"
 --
 -- Prepared statements
 --
diff --git a/src/pl/plpython/expected/plpython_types_3.out b/src/pl/plpython/expected/plpython_types_3.out
index 2d853bd5731771f81d07c91e4340f6b36513c240..69f958cbf288539cf44ace6497a6fa6ee4ccfa8f 100644
--- a/src/pl/plpython/expected/plpython_types_3.out
+++ b/src/pl/plpython/expected/plpython_types_3.out
@@ -765,6 +765,76 @@ SELECT * FROM test_type_conversion_array_domain_check_violation();
 ERROR:  value for domain ordered_pair_domain violates check constraint "ordered_pair_domain_check"
 CONTEXT:  while creating return value
 PL/Python function "test_type_conversion_array_domain_check_violation"
+--
+-- Arrays of domains
+--
+CREATE FUNCTION test_read_uint2_array(x uint2[]) RETURNS uint2 AS $$
+plpy.info(x, type(x))
+return x[0]
+$$ LANGUAGE plpythonu;
+select test_read_uint2_array(array[1::uint2]);
+INFO:  ([1], <class 'list'>)
+ test_read_uint2_array 
+-----------------------
+                     1
+(1 row)
+
+CREATE FUNCTION test_build_uint2_array(x int2) RETURNS uint2[] AS $$
+return [x, x]
+$$ LANGUAGE plpythonu;
+select test_build_uint2_array(1::int2);
+ test_build_uint2_array 
+------------------------
+ {1,1}
+(1 row)
+
+select test_build_uint2_array(-1::int2);  -- fail
+ERROR:  value for domain uint2 violates check constraint "uint2_check"
+CONTEXT:  while creating return value
+PL/Python function "test_build_uint2_array"
+--
+-- ideally this would work, but for now it doesn't, because the return value
+-- is [[2,4], [2,4]] which our conversion code thinks should become a 2-D
+-- integer array, not an array of arrays.
+--
+CREATE FUNCTION test_type_conversion_domain_array(x integer[])
+  RETURNS ordered_pair_domain[] AS $$
+return [x, x]
+$$ LANGUAGE plpythonu;
+select test_type_conversion_domain_array(array[2,4]);
+ERROR:  return value of function with array return type is not a Python sequence
+CONTEXT:  while creating return value
+PL/Python function "test_type_conversion_domain_array"
+select test_type_conversion_domain_array(array[4,2]);  -- fail
+ERROR:  return value of function with array return type is not a Python sequence
+CONTEXT:  while creating return value
+PL/Python function "test_type_conversion_domain_array"
+CREATE FUNCTION test_type_conversion_domain_array2(x ordered_pair_domain)
+  RETURNS integer AS $$
+plpy.info(x, type(x))
+return x[1]
+$$ LANGUAGE plpythonu;
+select test_type_conversion_domain_array2(array[2,4]);
+INFO:  ([2, 4], <class 'list'>)
+ test_type_conversion_domain_array2 
+------------------------------------
+                                  4
+(1 row)
+
+select test_type_conversion_domain_array2(array[4,2]);  -- fail
+ERROR:  value for domain ordered_pair_domain violates check constraint "ordered_pair_domain_check"
+CREATE FUNCTION test_type_conversion_array_domain_array(x ordered_pair_domain[])
+  RETURNS ordered_pair_domain AS $$
+plpy.info(x, type(x))
+return x[0]
+$$ LANGUAGE plpythonu;
+select test_type_conversion_array_domain_array(array[array[2,4]::ordered_pair_domain]);
+INFO:  ([[2, 4]], <class 'list'>)
+ test_type_conversion_array_domain_array 
+-----------------------------------------
+ {2,4}
+(1 row)
+
 ---
 --- Composite types
 ---
@@ -820,6 +890,64 @@ SELECT test_composite_type_input(row(1, 2));
                          3
 (1 row)
 
+--
+-- Domains within composite
+--
+CREATE TYPE nnint_container AS (f1 int, f2 nnint);
+CREATE FUNCTION nnint_test(x int, y int) RETURNS nnint_container AS $$
+return {'f1': x, 'f2': y}
+$$ LANGUAGE plpythonu;
+SELECT nnint_test(null, 3);
+ nnint_test 
+------------
+ (,3)
+(1 row)
+
+SELECT nnint_test(3, null);  -- fail
+ERROR:  value for domain nnint violates check constraint "nnint_check"
+CONTEXT:  while creating return value
+PL/Python function "nnint_test"
+--
+-- Domains of composite
+--
+CREATE DOMAIN ordered_named_pair AS named_pair_2 CHECK((VALUE).i <= (VALUE).j);
+CREATE FUNCTION read_ordered_named_pair(p ordered_named_pair) RETURNS integer AS $$
+return p['i'] + p['j']
+$$ LANGUAGE plpythonu;
+SELECT read_ordered_named_pair(row(1, 2));
+ read_ordered_named_pair 
+-------------------------
+                       3
+(1 row)
+
+SELECT read_ordered_named_pair(row(2, 1));  -- fail
+ERROR:  value for domain ordered_named_pair violates check constraint "ordered_named_pair_check"
+CREATE FUNCTION build_ordered_named_pair(i int, j int) RETURNS ordered_named_pair AS $$
+return {'i': i, 'j': j}
+$$ LANGUAGE plpythonu;
+SELECT build_ordered_named_pair(1,2);
+ build_ordered_named_pair 
+--------------------------
+ (1,2)
+(1 row)
+
+SELECT build_ordered_named_pair(2,1);  -- fail
+ERROR:  value for domain ordered_named_pair violates check constraint "ordered_named_pair_check"
+CONTEXT:  while creating return value
+PL/Python function "build_ordered_named_pair"
+CREATE FUNCTION build_ordered_named_pairs(i int, j int) RETURNS ordered_named_pair[] AS $$
+return [{'i': i, 'j': j}, {'i': i, 'j': j+1}]
+$$ LANGUAGE plpythonu;
+SELECT build_ordered_named_pairs(1,2);
+ build_ordered_named_pairs 
+---------------------------
+ {"(1,2)","(1,3)"}
+(1 row)
+
+SELECT build_ordered_named_pairs(2,1);  -- fail
+ERROR:  value for domain ordered_named_pair violates check constraint "ordered_named_pair_check"
+CONTEXT:  while creating return value
+PL/Python function "build_ordered_named_pairs"
 --
 -- Prepared statements
 --
diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c
index 0108471bfe218ccfd46d42208ef07a95a546339d..10ca786fbc2426cad04e8e27f6a6248da67b15bb 100644
--- a/src/pl/plpython/plpy_cursorobject.c
+++ b/src/pl/plpython/plpy_cursorobject.c
@@ -9,6 +9,7 @@
 #include <limits.h>
 
 #include "access/xact.h"
+#include "catalog/pg_type.h"
 #include "mb/pg_wchar.h"
 #include "utils/memutils.h"
 
@@ -106,6 +107,7 @@ static PyObject *
 PLy_cursor_query(const char *query)
 {
 	PLyCursorObject *cursor;
+	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 	volatile MemoryContext oldcontext;
 	volatile ResourceOwner oldowner;
 
@@ -116,7 +118,11 @@ PLy_cursor_query(const char *query)
 	cursor->mcxt = AllocSetContextCreate(TopMemoryContext,
 										 "PL/Python cursor context",
 										 ALLOCSET_DEFAULT_SIZES);
-	PLy_typeinfo_init(&cursor->result, cursor->mcxt);
+
+	/* Initialize for converting result tuples to Python */
+	PLy_input_setup_func(&cursor->result, cursor->mcxt,
+						 RECORDOID, -1,
+						 exec_ctx->curr_proc);
 
 	oldcontext = CurrentMemoryContext;
 	oldowner = CurrentResourceOwner;
@@ -125,7 +131,6 @@ PLy_cursor_query(const char *query)
 
 	PG_TRY();
 	{
-		PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 		SPIPlanPtr	plan;
 		Portal		portal;
 
@@ -166,6 +171,7 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 	volatile int nargs;
 	int			i;
 	PLyPlanObject *plan;
+	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 	volatile MemoryContext oldcontext;
 	volatile ResourceOwner oldowner;
 
@@ -208,7 +214,11 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 	cursor->mcxt = AllocSetContextCreate(TopMemoryContext,
 										 "PL/Python cursor context",
 										 ALLOCSET_DEFAULT_SIZES);
-	PLy_typeinfo_init(&cursor->result, cursor->mcxt);
+
+	/* Initialize for converting result tuples to Python */
+	PLy_input_setup_func(&cursor->result, cursor->mcxt,
+						 RECORDOID, -1,
+						 exec_ctx->curr_proc);
 
 	oldcontext = CurrentMemoryContext;
 	oldowner = CurrentResourceOwner;
@@ -217,7 +227,6 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 
 	PG_TRY();
 	{
-		PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 		Portal		portal;
 		char	   *volatile nulls;
 		volatile int j;
@@ -229,39 +238,24 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 
 		for (j = 0; j < nargs; j++)
 		{
+			PLyObToDatum *arg = &plan->args[j];
 			PyObject   *elem;
 
 			elem = PySequence_GetItem(args, j);
-			if (elem != Py_None)
+			PG_TRY();
 			{
-				PG_TRY();
-				{
-					plan->values[j] =
-						plan->args[j].out.d.func(&(plan->args[j].out.d),
-												 -1,
-												 elem,
-												 false);
-				}
-				PG_CATCH();
-				{
-					Py_DECREF(elem);
-					PG_RE_THROW();
-				}
-				PG_END_TRY();
+				bool		isnull;
 
-				Py_DECREF(elem);
-				nulls[j] = ' ';
+				plan->values[j] = PLy_output_convert(arg, elem, &isnull);
+				nulls[j] = isnull ? 'n' : ' ';
 			}
-			else
+			PG_CATCH();
 			{
 				Py_DECREF(elem);
-				plan->values[j] =
-					InputFunctionCall(&(plan->args[j].out.d.typfunc),
-									  NULL,
-									  plan->args[j].out.d.typioparam,
-									  -1);
-				nulls[j] = 'n';
+				PG_RE_THROW();
 			}
+			PG_END_TRY();
+			Py_DECREF(elem);
 		}
 
 		portal = SPI_cursor_open(NULL, plan->plan, plan->values, nulls,
@@ -281,7 +275,7 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 		/* cleanup plan->values array */
 		for (k = 0; k < nargs; k++)
 		{
-			if (!plan->args[k].out.d.typbyval &&
+			if (!plan->args[k].typbyval &&
 				(plan->values[k] != PointerGetDatum(NULL)))
 			{
 				pfree(DatumGetPointer(plan->values[k]));
@@ -298,7 +292,7 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 
 	for (i = 0; i < nargs; i++)
 	{
-		if (!plan->args[i].out.d.typbyval &&
+		if (!plan->args[i].typbyval &&
 			(plan->values[i] != PointerGetDatum(NULL)))
 		{
 			pfree(DatumGetPointer(plan->values[i]));
@@ -339,6 +333,7 @@ PLy_cursor_iternext(PyObject *self)
 {
 	PLyCursorObject *cursor;
 	PyObject   *ret;
+	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 	volatile MemoryContext oldcontext;
 	volatile ResourceOwner oldowner;
 	Portal		portal;
@@ -374,11 +369,11 @@ PLy_cursor_iternext(PyObject *self)
 		}
 		else
 		{
-			if (cursor->result.is_rowtype != 1)
-				PLy_input_tuple_funcs(&cursor->result, SPI_tuptable->tupdesc);
+			PLy_input_setup_tuple(&cursor->result, SPI_tuptable->tupdesc,
+								  exec_ctx->curr_proc);
 
-			ret = PLyDict_FromTuple(&cursor->result, SPI_tuptable->vals[0],
-									SPI_tuptable->tupdesc);
+			ret = PLy_input_from_tuple(&cursor->result, SPI_tuptable->vals[0],
+									   SPI_tuptable->tupdesc);
 		}
 
 		SPI_freetuptable(SPI_tuptable);
@@ -401,6 +396,7 @@ PLy_cursor_fetch(PyObject *self, PyObject *args)
 	PLyCursorObject *cursor;
 	int			count;
 	PLyResultObject *ret;
+	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 	volatile MemoryContext oldcontext;
 	volatile ResourceOwner oldowner;
 	Portal		portal;
@@ -437,9 +433,6 @@ PLy_cursor_fetch(PyObject *self, PyObject *args)
 	{
 		SPI_cursor_fetch(portal, true, count);
 
-		if (cursor->result.is_rowtype != 1)
-			PLy_input_tuple_funcs(&cursor->result, SPI_tuptable->tupdesc);
-
 		Py_DECREF(ret->status);
 		ret->status = PyInt_FromLong(SPI_OK_FETCH);
 
@@ -465,11 +458,14 @@ PLy_cursor_fetch(PyObject *self, PyObject *args)
 			Py_DECREF(ret->rows);
 			ret->rows = PyList_New(SPI_processed);
 
+			PLy_input_setup_tuple(&cursor->result, SPI_tuptable->tupdesc,
+								  exec_ctx->curr_proc);
+
 			for (i = 0; i < SPI_processed; i++)
 			{
-				PyObject   *row = PLyDict_FromTuple(&cursor->result,
-													SPI_tuptable->vals[i],
-													SPI_tuptable->tupdesc);
+				PyObject   *row = PLy_input_from_tuple(&cursor->result,
+													   SPI_tuptable->vals[i],
+													   SPI_tuptable->tupdesc);
 
 				PyList_SetItem(ret->rows, i, row);
 			}
diff --git a/src/pl/plpython/plpy_cursorobject.h b/src/pl/plpython/plpy_cursorobject.h
index 018b169cbf2d90d3b578befa4b70486cf4d1c80d..e4d2c0ed2532a632f0c24665b73e7f7f21538008 100644
--- a/src/pl/plpython/plpy_cursorobject.h
+++ b/src/pl/plpython/plpy_cursorobject.h
@@ -12,7 +12,7 @@ typedef struct PLyCursorObject
 {
 	PyObject_HEAD
 	char	   *portalname;
-	PLyTypeInfo result;
+	PLyDatumToOb result;
 	bool		closed;
 	MemoryContext mcxt;
 } PLyCursorObject;
diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c
index 26f61dd0f37b613f64fdfbb369a5725d56eca8be..02d7d2ad5f8eda8e3872177375939dec1dd0f9cc 100644
--- a/src/pl/plpython/plpy_exec.c
+++ b/src/pl/plpython/plpy_exec.c
@@ -202,7 +202,7 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 		 * return value as a special "void datum" rather than NULL (as is the
 		 * case for non-void-returning functions).
 		 */
-		if (proc->result.out.d.typoid == VOIDOID)
+		if (proc->result.typoid == VOIDOID)
 		{
 			if (plrv != Py_None)
 				ereport(ERROR,
@@ -212,48 +212,22 @@ PLy_exec_function(FunctionCallInfo fcinfo, PLyProcedure *proc)
 			fcinfo->isnull = false;
 			rv = (Datum) 0;
 		}
-		else if (plrv == Py_None)
+		else if (plrv == Py_None &&
+				 srfstate && srfstate->iter == NULL)
 		{
-			fcinfo->isnull = true;
-
 			/*
 			 * In a SETOF function, the iteration-ending null isn't a real
 			 * value; don't pass it through the input function, which might
 			 * complain.
 			 */
-			if (srfstate && srfstate->iter == NULL)
-				rv = (Datum) 0;
-			else if (proc->result.is_rowtype < 1)
-				rv = InputFunctionCall(&proc->result.out.d.typfunc,
-									   NULL,
-									   proc->result.out.d.typioparam,
-									   -1);
-			else
-				/* Tuple as None */
-				rv = (Datum) NULL;
-		}
-		else if (proc->result.is_rowtype >= 1)
-		{
-			TupleDesc	desc;
-
-			/* make sure it's not an unnamed record */
-			Assert((proc->result.out.d.typoid == RECORDOID &&
-					proc->result.out.d.typmod != -1) ||
-				   (proc->result.out.d.typoid != RECORDOID &&
-					proc->result.out.d.typmod == -1));
-
-			desc = lookup_rowtype_tupdesc(proc->result.out.d.typoid,
-										  proc->result.out.d.typmod);
-
-			rv = PLyObject_ToCompositeDatum(&proc->result, desc, plrv, false);
-			fcinfo->isnull = (rv == (Datum) NULL);
-
-			ReleaseTupleDesc(desc);
+			fcinfo->isnull = true;
+			rv = (Datum) 0;
 		}
 		else
 		{
-			fcinfo->isnull = false;
-			rv = (proc->result.out.d.func) (&proc->result.out.d, -1, plrv, false);
+			/* Normal conversion of result */
+			rv = PLy_output_convert(&proc->result, plrv,
+									&fcinfo->isnull);
 		}
 	}
 	PG_CATCH();
@@ -328,20 +302,32 @@ PLy_exec_trigger(FunctionCallInfo fcinfo, PLyProcedure *proc)
 	PyObject   *volatile plargs = NULL;
 	PyObject   *volatile plrv = NULL;
 	TriggerData *tdata;
+	TupleDesc	rel_descr;
 
 	Assert(CALLED_AS_TRIGGER(fcinfo));
+	tdata = (TriggerData *) fcinfo->context;
 
 	/*
-	 * Input/output conversion for trigger tuples.  Use the result TypeInfo
-	 * variable to store the tuple conversion info.  We do this over again on
-	 * each call to cover the possibility that the relation's tupdesc changed
-	 * since the trigger was last called. PLy_input_tuple_funcs and
-	 * PLy_output_tuple_funcs are responsible for not doing repetitive work.
+	 * Input/output conversion for trigger tuples.  We use the result and
+	 * result_in fields to store the tuple conversion info.  We do this over
+	 * again on each call to cover the possibility that the relation's tupdesc
+	 * changed since the trigger was last called.  The PLy_xxx_setup_func
+	 * calls should only happen once, but PLy_input_setup_tuple and
+	 * PLy_output_setup_tuple are responsible for not doing repetitive work.
 	 */
-	tdata = (TriggerData *) fcinfo->context;
-
-	PLy_input_tuple_funcs(&(proc->result), tdata->tg_relation->rd_att);
-	PLy_output_tuple_funcs(&(proc->result), tdata->tg_relation->rd_att);
+	rel_descr = RelationGetDescr(tdata->tg_relation);
+	if (proc->result.typoid != rel_descr->tdtypeid)
+		PLy_output_setup_func(&proc->result, proc->mcxt,
+							  rel_descr->tdtypeid,
+							  rel_descr->tdtypmod,
+							  proc);
+	if (proc->result_in.typoid != rel_descr->tdtypeid)
+		PLy_input_setup_func(&proc->result_in, proc->mcxt,
+							 rel_descr->tdtypeid,
+							 rel_descr->tdtypmod,
+							 proc);
+	PLy_output_setup_tuple(&proc->result, rel_descr, proc);
+	PLy_input_setup_tuple(&proc->result_in, rel_descr, proc);
 
 	PG_TRY();
 	{
@@ -436,46 +422,12 @@ PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc)
 		args = PyList_New(proc->nargs);
 		for (i = 0; i < proc->nargs; i++)
 		{
-			if (proc->args[i].is_rowtype > 0)
-			{
-				if (fcinfo->argnull[i])
-					arg = NULL;
-				else
-				{
-					HeapTupleHeader td;
-					Oid			tupType;
-					int32		tupTypmod;
-					TupleDesc	tupdesc;
-					HeapTupleData tmptup;
-
-					td = DatumGetHeapTupleHeader(fcinfo->arg[i]);
-					/* Extract rowtype info and find a tupdesc */
-					tupType = HeapTupleHeaderGetTypeId(td);
-					tupTypmod = HeapTupleHeaderGetTypMod(td);
-					tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
-
-					/* Set up I/O funcs if not done yet */
-					if (proc->args[i].is_rowtype != 1)
-						PLy_input_tuple_funcs(&(proc->args[i]), tupdesc);
-
-					/* Build a temporary HeapTuple control structure */
-					tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
-					tmptup.t_data = td;
-
-					arg = PLyDict_FromTuple(&(proc->args[i]), &tmptup, tupdesc);
-					ReleaseTupleDesc(tupdesc);
-				}
-			}
+			PLyDatumToOb *arginfo = &proc->args[i];
+
+			if (fcinfo->argnull[i])
+				arg = NULL;
 			else
-			{
-				if (fcinfo->argnull[i])
-					arg = NULL;
-				else
-				{
-					arg = (proc->args[i].in.d.func) (&(proc->args[i].in.d),
-													 fcinfo->arg[i]);
-				}
-			}
+				arg = PLy_input_convert(arginfo, fcinfo->arg[i]);
 
 			if (arg == NULL)
 			{
@@ -493,7 +445,7 @@ PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc)
 		}
 
 		/* Set up output conversion for functions returning RECORD */
-		if (proc->result.out.d.typoid == RECORDOID)
+		if (proc->result.typoid == RECORDOID)
 		{
 			TupleDesc	desc;
 
@@ -504,7 +456,7 @@ PLy_function_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc)
 								"that cannot accept type record")));
 
 			/* cache the output conversion functions */
-			PLy_output_record_funcs(&(proc->result), desc);
+			PLy_output_setup_record(&proc->result, desc, proc);
 		}
 	}
 	PG_CATCH();
@@ -723,6 +675,7 @@ static PyObject *
 PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *rv)
 {
 	TriggerData *tdata = (TriggerData *) fcinfo->context;
+	TupleDesc	rel_descr = RelationGetDescr(tdata->tg_relation);
 	PyObject   *pltname,
 			   *pltevent,
 			   *pltwhen,
@@ -790,8 +743,9 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
 				pltevent = PyString_FromString("INSERT");
 
 				PyDict_SetItemString(pltdata, "old", Py_None);
-				pytnew = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple,
-										   tdata->tg_relation->rd_att);
+				pytnew = PLy_input_from_tuple(&proc->result_in,
+											  tdata->tg_trigtuple,
+											  rel_descr);
 				PyDict_SetItemString(pltdata, "new", pytnew);
 				Py_DECREF(pytnew);
 				*rv = tdata->tg_trigtuple;
@@ -801,8 +755,9 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
 				pltevent = PyString_FromString("DELETE");
 
 				PyDict_SetItemString(pltdata, "new", Py_None);
-				pytold = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple,
-										   tdata->tg_relation->rd_att);
+				pytold = PLy_input_from_tuple(&proc->result_in,
+											  tdata->tg_trigtuple,
+											  rel_descr);
 				PyDict_SetItemString(pltdata, "old", pytold);
 				Py_DECREF(pytold);
 				*rv = tdata->tg_trigtuple;
@@ -811,12 +766,14 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
 			{
 				pltevent = PyString_FromString("UPDATE");
 
-				pytnew = PLyDict_FromTuple(&(proc->result), tdata->tg_newtuple,
-										   tdata->tg_relation->rd_att);
+				pytnew = PLy_input_from_tuple(&proc->result_in,
+											  tdata->tg_newtuple,
+											  rel_descr);
 				PyDict_SetItemString(pltdata, "new", pytnew);
 				Py_DECREF(pytnew);
-				pytold = PLyDict_FromTuple(&(proc->result), tdata->tg_trigtuple,
-										   tdata->tg_relation->rd_att);
+				pytold = PLy_input_from_tuple(&proc->result_in,
+											  tdata->tg_trigtuple,
+											  rel_descr);
 				PyDict_SetItemString(pltdata, "old", pytold);
 				Py_DECREF(pytold);
 				*rv = tdata->tg_newtuple;
@@ -897,6 +854,9 @@ PLy_trigger_build_args(FunctionCallInfo fcinfo, PLyProcedure *proc, HeapTuple *r
 	return pltdata;
 }
 
+/*
+ * Apply changes requested by a MODIFY return from a trigger function.
+ */
 static HeapTuple
 PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata,
 				 HeapTuple otup)
@@ -938,7 +898,7 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata,
 		plkeys = PyDict_Keys(plntup);
 		nkeys = PyList_Size(plkeys);
 
-		tupdesc = tdata->tg_relation->rd_att;
+		tupdesc = RelationGetDescr(tdata->tg_relation);
 
 		modvalues = (Datum *) palloc0(tupdesc->natts * sizeof(Datum));
 		modnulls = (bool *) palloc0(tupdesc->natts * sizeof(bool));
@@ -950,7 +910,6 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata,
 			char	   *plattstr;
 			int			attn;
 			PLyObToDatum *att;
-			Form_pg_attribute attr;
 
 			platt = PyList_GetItem(plkeys, i);
 			if (PyString_Check(platt))
@@ -975,7 +934,6 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata,
 						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						 errmsg("cannot set system attribute \"%s\"",
 								plattstr)));
-			att = &proc->result.out.r.atts[attn - 1];
 
 			plval = PyDict_GetItem(plntup, platt);
 			if (plval == NULL)
@@ -983,25 +941,12 @@ PLy_modify_tuple(PLyProcedure *proc, PyObject *pltd, TriggerData *tdata,
 
 			Py_INCREF(plval);
 
-			attr = TupleDescAttr(tupdesc, attn - 1);
-			if (plval != Py_None)
-			{
-				modvalues[attn - 1] =
-					(att->func) (att,
-								 attr->atttypmod,
-								 plval,
-								 false);
-				modnulls[attn - 1] = false;
-			}
-			else
-			{
-				modvalues[attn - 1] =
-					InputFunctionCall(&att->typfunc,
-									  NULL,
-									  att->typioparam,
-									  attr->atttypmod);
-				modnulls[attn - 1] = true;
-			}
+			/* We assume proc->result is set up to convert tuples properly */
+			att = &proc->result.u.tuple.atts[attn - 1];
+
+			modvalues[attn - 1] = PLy_output_convert(att,
+													 plval,
+													 &modnulls[attn - 1]);
 			modrepls[attn - 1] = true;
 
 			Py_DECREF(plval);
diff --git a/src/pl/plpython/plpy_main.c b/src/pl/plpython/plpy_main.c
index 7df50c09c82bd5dc57640a6f24e00f5e3a5be18f..29db90e4489f3edb3cc2d4feb276671749f55a89 100644
--- a/src/pl/plpython/plpy_main.c
+++ b/src/pl/plpython/plpy_main.c
@@ -318,7 +318,12 @@ plpython_inline_handler(PG_FUNCTION_ARGS)
 									  ALLOCSET_DEFAULT_SIZES);
 	proc.pyname = MemoryContextStrdup(proc.mcxt, "__plpython_inline_block");
 	proc.langid = codeblock->langOid;
-	proc.result.out.d.typoid = VOIDOID;
+
+	/*
+	 * This is currently sufficient to get PLy_exec_function to work, but
+	 * someday we might need to be honest and use PLy_output_setup_func.
+	 */
+	proc.result.typoid = VOIDOID;
 
 	/*
 	 * Push execution context onto stack.  It is important that this get
diff --git a/src/pl/plpython/plpy_planobject.h b/src/pl/plpython/plpy_planobject.h
index 5adc957053f0b56a0b181f799fedd7c56086e022..729effb1631daafd2d9cb9caf738f64054ae0c41 100644
--- a/src/pl/plpython/plpy_planobject.h
+++ b/src/pl/plpython/plpy_planobject.h
@@ -16,7 +16,7 @@ typedef struct PLyPlanObject
 	int			nargs;
 	Oid		   *types;
 	Datum	   *values;
-	PLyTypeInfo *args;
+	PLyObToDatum *args;
 	MemoryContext mcxt;
 } PLyPlanObject;
 
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index 26acc88b2703332077ff388915dc2014f151cfd2..58d6988202557b20366fe0053f97ea5c14292067 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -15,6 +15,7 @@
 #include "utils/builtins.h"
 #include "utils/hsearch.h"
 #include "utils/inval.h"
+#include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/syscache.h"
 
@@ -29,7 +30,6 @@
 static HTAB *PLy_procedure_cache = NULL;
 
 static PLyProcedure *PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger);
-static bool PLy_procedure_argument_valid(PLyTypeInfo *arg);
 static bool PLy_procedure_valid(PLyProcedure *proc, HeapTuple procTup);
 static char *PLy_procedure_munge_source(const char *name, const char *src);
 
@@ -165,6 +165,7 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 			*ptr = '_';
 	}
 
+	/* Create long-lived context that all procedure info will live in */
 	cxt = AllocSetContextCreate(TopMemoryContext,
 								procName,
 								ALLOCSET_DEFAULT_SIZES);
@@ -188,11 +189,9 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 		proc->fn_tid = procTup->t_self;
 		proc->fn_readonly = (procStruct->provolatile != PROVOLATILE_VOLATILE);
 		proc->is_setof = procStruct->proretset;
-		PLy_typeinfo_init(&proc->result, proc->mcxt);
 		proc->src = NULL;
 		proc->argnames = NULL;
-		for (i = 0; i < FUNC_MAX_ARGS; i++)
-			PLy_typeinfo_init(&proc->args[i], proc->mcxt);
+		proc->args = NULL;
 		proc->nargs = 0;
 		proc->langid = procStruct->prolang;
 		protrftypes_datum = SysCacheGetAttr(PROCOID, procTup,
@@ -211,50 +210,48 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 		 */
 		if (!is_trigger)
 		{
+			Oid			rettype = procStruct->prorettype;
 			HeapTuple	rvTypeTup;
 			Form_pg_type rvTypeStruct;
 
-			rvTypeTup = SearchSysCache1(TYPEOID,
-										ObjectIdGetDatum(procStruct->prorettype));
+			rvTypeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(rettype));
 			if (!HeapTupleIsValid(rvTypeTup))
-				elog(ERROR, "cache lookup failed for type %u",
-					 procStruct->prorettype);
+				elog(ERROR, "cache lookup failed for type %u", rettype);
 			rvTypeStruct = (Form_pg_type) GETSTRUCT(rvTypeTup);
 
 			/* Disallow pseudotype result, except for void or record */
 			if (rvTypeStruct->typtype == TYPTYPE_PSEUDO)
 			{
-				if (procStruct->prorettype == TRIGGEROID)
+				if (rettype == VOIDOID ||
+					rettype == RECORDOID)
+					 /* okay */ ;
+				else if (rettype == TRIGGEROID || rettype == EVTTRIGGEROID)
 					ereport(ERROR,
 							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 							 errmsg("trigger functions can only be called as triggers")));
-				else if (procStruct->prorettype != VOIDOID &&
-						 procStruct->prorettype != RECORDOID)
+				else
 					ereport(ERROR,
 							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 							 errmsg("PL/Python functions cannot return type %s",
-									format_type_be(procStruct->prorettype))));
+									format_type_be(rettype))));
 			}
 
-			if (rvTypeStruct->typtype == TYPTYPE_COMPOSITE ||
-				procStruct->prorettype == RECORDOID)
-			{
-				/*
-				 * Tuple: set up later, during first call to
-				 * PLy_function_handler
-				 */
-				proc->result.out.d.typoid = procStruct->prorettype;
-				proc->result.out.d.typmod = -1;
-				proc->result.is_rowtype = 2;
-			}
-			else
-			{
-				/* do the real work */
-				PLy_output_datum_func(&proc->result, rvTypeTup, proc->langid, proc->trftypes);
-			}
+			/* set up output function for procedure result */
+			PLy_output_setup_func(&proc->result, proc->mcxt,
+								  rettype, -1, proc);
 
 			ReleaseSysCache(rvTypeTup);
 		}
+		else
+		{
+			/*
+			 * In a trigger function, we use proc->result and proc->result_in
+			 * for converting tuples, but we don't yet have enough info to set
+			 * them up.  PLy_exec_trigger will deal with it.
+			 */
+			proc->result.typoid = InvalidOid;
+			proc->result_in.typoid = InvalidOid;
+		}
 
 		/*
 		 * Now get information required for input conversion of the
@@ -287,7 +284,10 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 				}
 			}
 
+			/* Allocate arrays for per-input-argument data */
 			proc->argnames = (char **) palloc0(sizeof(char *) * proc->nargs);
+			proc->args = (PLyDatumToOb *) palloc0(sizeof(PLyDatumToOb) * proc->nargs);
+
 			for (i = pos = 0; i < total; i++)
 			{
 				HeapTuple	argTypeTup;
@@ -306,28 +306,17 @@ PLy_procedure_create(HeapTuple procTup, Oid fn_oid, bool is_trigger)
 					elog(ERROR, "cache lookup failed for type %u", types[i]);
 				argTypeStruct = (Form_pg_type) GETSTRUCT(argTypeTup);
 
-				/* check argument type is OK, set up I/O function info */
-				switch (argTypeStruct->typtype)
-				{
-					case TYPTYPE_PSEUDO:
-						/* Disallow pseudotype argument */
-						ereport(ERROR,
-								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-								 errmsg("PL/Python functions cannot accept type %s",
-										format_type_be(types[i]))));
-						break;
-					case TYPTYPE_COMPOSITE:
-						/* we'll set IO funcs at first call */
-						proc->args[pos].is_rowtype = 2;
-						break;
-					default:
-						PLy_input_datum_func(&(proc->args[pos]),
-											 types[i],
-											 argTypeTup,
-											 proc->langid,
-											 proc->trftypes);
-						break;
-				}
+				/* disallow pseudotype arguments */
+				if (argTypeStruct->typtype == TYPTYPE_PSEUDO)
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("PL/Python functions cannot accept type %s",
+									format_type_be(types[i]))));
+
+				/* set up I/O function info */
+				PLy_input_setup_func(&proc->args[pos], proc->mcxt,
+									 types[i], -1,	/* typmod not known */
+									 proc);
 
 				/* get argument name */
 				proc->argnames[pos] = names ? pstrdup(names[i]) : NULL;
@@ -424,54 +413,12 @@ PLy_procedure_delete(PLyProcedure *proc)
 	MemoryContextDelete(proc->mcxt);
 }
 
-/*
- * Check if our cached information about a datatype is still valid
- */
-static bool
-PLy_procedure_argument_valid(PLyTypeInfo *arg)
-{
-	HeapTuple	relTup;
-	bool		valid;
-
-	/* Nothing to cache unless type is composite */
-	if (arg->is_rowtype != 1)
-		return true;
-
-	/*
-	 * Zero typ_relid means that we got called on an output argument of a
-	 * function returning an unnamed record type; the info for it can't
-	 * change.
-	 */
-	if (!OidIsValid(arg->typ_relid))
-		return true;
-
-	/* Else we should have some cached data */
-	Assert(TransactionIdIsValid(arg->typrel_xmin));
-	Assert(ItemPointerIsValid(&arg->typrel_tid));
-
-	/* Get the pg_class tuple for the data type */
-	relTup = SearchSysCache1(RELOID, ObjectIdGetDatum(arg->typ_relid));
-	if (!HeapTupleIsValid(relTup))
-		elog(ERROR, "cache lookup failed for relation %u", arg->typ_relid);
-
-	/* If it has changed, the cached data is not valid */
-	valid = (arg->typrel_xmin == HeapTupleHeaderGetRawXmin(relTup->t_data) &&
-			 ItemPointerEquals(&arg->typrel_tid, &relTup->t_self));
-
-	ReleaseSysCache(relTup);
-
-	return valid;
-}
-
 /*
  * Decide whether a cached PLyProcedure struct is still valid
  */
 static bool
 PLy_procedure_valid(PLyProcedure *proc, HeapTuple procTup)
 {
-	int			i;
-	bool		valid;
-
 	if (proc == NULL)
 		return false;
 
@@ -480,22 +427,7 @@ PLy_procedure_valid(PLyProcedure *proc, HeapTuple procTup)
 		  ItemPointerEquals(&proc->fn_tid, &procTup->t_self)))
 		return false;
 
-	/* Else check the input argument datatypes */
-	valid = true;
-	for (i = 0; i < proc->nargs; i++)
-	{
-		valid = PLy_procedure_argument_valid(&proc->args[i]);
-
-		/* Short-circuit on first changed argument */
-		if (!valid)
-			break;
-	}
-
-	/* if the output type is composite, it might have changed */
-	if (valid)
-		valid = PLy_procedure_argument_valid(&proc->result);
-
-	return valid;
+	return true;
 }
 
 static char *
diff --git a/src/pl/plpython/plpy_procedure.h b/src/pl/plpython/plpy_procedure.h
index d05944fc39851e9a605c7d430b531eb2fe318309..cd1b87fdc3cadfef3d654f6ae22301ddf70f7b6d 100644
--- a/src/pl/plpython/plpy_procedure.h
+++ b/src/pl/plpython/plpy_procedure.h
@@ -31,12 +31,12 @@ typedef struct PLyProcedure
 	ItemPointerData fn_tid;
 	bool		fn_readonly;
 	bool		is_setof;		/* true, if procedure returns result set */
-	PLyTypeInfo result;			/* also used to store info for trigger tuple
-								 * type */
+	PLyObToDatum result;		/* Function result output conversion info */
+	PLyDatumToOb result_in;		/* For converting input tuples in a trigger */
 	char	   *src;			/* textual procedure code, after mangling */
 	char	  **argnames;		/* Argument names */
-	PLyTypeInfo args[FUNC_MAX_ARGS];
-	int			nargs;
+	PLyDatumToOb *args;			/* Argument input conversion info */
+	int			nargs;			/* Number of elements in above arrays */
 	Oid			langid;			/* OID of plpython pg_language entry */
 	List	   *trftypes;		/* OID list of transform types */
 	PyObject   *code;			/* compiled procedure code */
diff --git a/src/pl/plpython/plpy_spi.c b/src/pl/plpython/plpy_spi.c
index 955769c5e327fa7856588154f1fb9530d27680b6..69eb6b39f67ec66f1f21501a816cb5f34330fbd1 100644
--- a/src/pl/plpython/plpy_spi.c
+++ b/src/pl/plpython/plpy_spi.c
@@ -46,6 +46,7 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
 	PyObject   *list = NULL;
 	PyObject   *volatile optr = NULL;
 	char	   *query;
+	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 	volatile MemoryContext oldcontext;
 	volatile ResourceOwner oldowner;
 	volatile int nargs;
@@ -71,9 +72,9 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
 	nargs = list ? PySequence_Length(list) : 0;
 
 	plan->nargs = nargs;
-	plan->types = nargs ? palloc(sizeof(Oid) * nargs) : NULL;
-	plan->values = nargs ? palloc(sizeof(Datum) * nargs) : NULL;
-	plan->args = nargs ? palloc(sizeof(PLyTypeInfo) * nargs) : NULL;
+	plan->types = nargs ? palloc0(sizeof(Oid) * nargs) : NULL;
+	plan->values = nargs ? palloc0(sizeof(Datum) * nargs) : NULL;
+	plan->args = nargs ? palloc0(sizeof(PLyObToDatum) * nargs) : NULL;
 
 	MemoryContextSwitchTo(oldcontext);
 
@@ -85,22 +86,10 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
 	PG_TRY();
 	{
 		int			i;
-		PLyExecutionContext *exec_ctx = PLy_current_execution_context();
-
-		/*
-		 * the other loop might throw an exception, if PLyTypeInfo member
-		 * isn't properly initialized the Py_DECREF(plan) will go boom
-		 */
-		for (i = 0; i < nargs; i++)
-		{
-			PLy_typeinfo_init(&plan->args[i], plan->mcxt);
-			plan->values[i] = PointerGetDatum(NULL);
-		}
 
 		for (i = 0; i < nargs; i++)
 		{
 			char	   *sptr;
-			HeapTuple	typeTup;
 			Oid			typeId;
 			int32		typmod;
 
@@ -124,11 +113,6 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
 
 			parseTypeString(sptr, &typeId, &typmod, false);
 
-			typeTup = SearchSysCache1(TYPEOID,
-									  ObjectIdGetDatum(typeId));
-			if (!HeapTupleIsValid(typeTup))
-				elog(ERROR, "cache lookup failed for type %u", typeId);
-
 			Py_DECREF(optr);
 
 			/*
@@ -138,8 +122,9 @@ PLy_spi_prepare(PyObject *self, PyObject *args)
 			optr = NULL;
 
 			plan->types[i] = typeId;
-			PLy_output_datum_func(&plan->args[i], typeTup, exec_ctx->curr_proc->langid, exec_ctx->curr_proc->trftypes);
-			ReleaseSysCache(typeTup);
+			PLy_output_setup_func(&plan->args[i], plan->mcxt,
+								  typeId, typmod,
+								  exec_ctx->curr_proc);
 		}
 
 		pg_verifymbstr(query, strlen(query), false);
@@ -253,39 +238,24 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
 
 		for (j = 0; j < nargs; j++)
 		{
+			PLyObToDatum *arg = &plan->args[j];
 			PyObject   *elem;
 
 			elem = PySequence_GetItem(list, j);
-			if (elem != Py_None)
+			PG_TRY();
 			{
-				PG_TRY();
-				{
-					plan->values[j] =
-						plan->args[j].out.d.func(&(plan->args[j].out.d),
-												 -1,
-												 elem,
-												 false);
-				}
-				PG_CATCH();
-				{
-					Py_DECREF(elem);
-					PG_RE_THROW();
-				}
-				PG_END_TRY();
+				bool		isnull;
 
-				Py_DECREF(elem);
-				nulls[j] = ' ';
+				plan->values[j] = PLy_output_convert(arg, elem, &isnull);
+				nulls[j] = isnull ? 'n' : ' ';
 			}
-			else
+			PG_CATCH();
 			{
 				Py_DECREF(elem);
-				plan->values[j] =
-					InputFunctionCall(&(plan->args[j].out.d.typfunc),
-									  NULL,
-									  plan->args[j].out.d.typioparam,
-									  -1);
-				nulls[j] = 'n';
+				PG_RE_THROW();
 			}
+			PG_END_TRY();
+			Py_DECREF(elem);
 		}
 
 		rv = SPI_execute_plan(plan->plan, plan->values, nulls,
@@ -306,7 +276,7 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
 		 */
 		for (k = 0; k < nargs; k++)
 		{
-			if (!plan->args[k].out.d.typbyval &&
+			if (!plan->args[k].typbyval &&
 				(plan->values[k] != PointerGetDatum(NULL)))
 			{
 				pfree(DatumGetPointer(plan->values[k]));
@@ -321,7 +291,7 @@ PLy_spi_execute_plan(PyObject *ob, PyObject *list, long limit)
 
 	for (i = 0; i < nargs; i++)
 	{
-		if (!plan->args[i].out.d.typbyval &&
+		if (!plan->args[i].typbyval &&
 			(plan->values[i] != PointerGetDatum(NULL)))
 		{
 			pfree(DatumGetPointer(plan->values[i]));
@@ -386,6 +356,7 @@ static PyObject *
 PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status)
 {
 	PLyResultObject *result;
+	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
 	volatile MemoryContext oldcontext;
 
 	result = (PLyResultObject *) PLy_result_new();
@@ -401,7 +372,7 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status)
 	}
 	else if (status > 0 && tuptable != NULL)
 	{
-		PLyTypeInfo args;
+		PLyDatumToOb ininfo;
 		MemoryContext cxt;
 
 		Py_DECREF(result->nrows);
@@ -412,7 +383,10 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status)
 		cxt = AllocSetContextCreate(CurrentMemoryContext,
 									"PL/Python temp context",
 									ALLOCSET_DEFAULT_SIZES);
-		PLy_typeinfo_init(&args, cxt);
+
+		/* Initialize for converting result tuples to Python */
+		PLy_input_setup_func(&ininfo, cxt, RECORDOID, -1,
+							 exec_ctx->curr_proc);
 
 		oldcontext = CurrentMemoryContext;
 		PG_TRY();
@@ -436,12 +410,14 @@ PLy_spi_execute_fetch_result(SPITupleTable *tuptable, uint64 rows, int status)
 				Py_DECREF(result->rows);
 				result->rows = PyList_New(rows);
 
-				PLy_input_tuple_funcs(&args, tuptable->tupdesc);
+				PLy_input_setup_tuple(&ininfo, tuptable->tupdesc,
+									  exec_ctx->curr_proc);
+
 				for (i = 0; i < rows; i++)
 				{
-					PyObject   *row = PLyDict_FromTuple(&args,
-														tuptable->vals[i],
-														tuptable->tupdesc);
+					PyObject   *row = PLy_input_from_tuple(&ininfo,
+														   tuptable->vals[i],
+														   tuptable->tupdesc);
 
 					PyList_SetItem(result->rows, i, row);
 				}
diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c
index e4af8cc9ef0d7d91ca34bc2d71f301884ea1b70c..ce1527072e4d008c0c8b49c1ecf6f57aa6680bfa 100644
--- a/src/pl/plpython/plpy_typeio.c
+++ b/src/pl/plpython/plpy_typeio.c
@@ -7,19 +7,15 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
-#include "access/transam.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "mb/pg_wchar.h"
-#include "parser/parse_type.h"
+#include "miscadmin.h"
 #include "utils/array.h"
 #include "utils/builtins.h"
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
-#include "utils/numeric.h"
-#include "utils/syscache.h"
-#include "utils/typcache.h"
 
 #include "plpython.h"
 
@@ -29,10 +25,6 @@
 #include "plpy_main.h"
 
 
-/* I/O function caching */
-static void PLy_input_datum_func2(PLyDatumToOb *arg, MemoryContext arg_mcxt, Oid typeOid, HeapTuple typeTup, Oid langid, List *trftypes);
-static void PLy_output_datum_func2(PLyObToDatum *arg, MemoryContext arg_mcxt, HeapTuple typeTup, Oid langid, List *trftypes);
-
 /* conversion from Datums to Python objects */
 static PyObject *PLyBool_FromBool(PLyDatumToOb *arg, Datum d);
 static PyObject *PLyFloat_FromFloat4(PLyDatumToOb *arg, Datum d);
@@ -43,361 +35,365 @@ static PyObject *PLyInt_FromInt32(PLyDatumToOb *arg, Datum d);
 static PyObject *PLyLong_FromInt64(PLyDatumToOb *arg, Datum d);
 static PyObject *PLyLong_FromOid(PLyDatumToOb *arg, Datum d);
 static PyObject *PLyBytes_FromBytea(PLyDatumToOb *arg, Datum d);
-static PyObject *PLyString_FromDatum(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyString_FromScalar(PLyDatumToOb *arg, Datum d);
 static PyObject *PLyObject_FromTransform(PLyDatumToOb *arg, Datum d);
 static PyObject *PLyList_FromArray(PLyDatumToOb *arg, Datum d);
 static PyObject *PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 						  char **dataptr_p, bits8 **bitmap_p, int *bitmask_p);
+static PyObject *PLyDict_FromComposite(PLyDatumToOb *arg, Datum d);
+static PyObject *PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc);
 
 /* conversion from Python objects to Datums */
-static Datum PLyObject_ToBool(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
-static Datum PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
-static Datum PLyObject_ToComposite(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
-static Datum PLyObject_ToDatum(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
-static Datum PLyObject_ToTransform(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
-static Datum PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray);
+static Datum PLyObject_ToBool(PLyObToDatum *arg, PyObject *plrv,
+				 bool *isnull, bool inarray);
+static Datum PLyObject_ToBytea(PLyObToDatum *arg, PyObject *plrv,
+				  bool *isnull, bool inarray);
+static Datum PLyObject_ToComposite(PLyObToDatum *arg, PyObject *plrv,
+					  bool *isnull, bool inarray);
+static Datum PLyObject_ToScalar(PLyObToDatum *arg, PyObject *plrv,
+				   bool *isnull, bool inarray);
+static Datum PLyObject_ToDomain(PLyObToDatum *arg, PyObject *plrv,
+				   bool *isnull, bool inarray);
+static Datum PLyObject_ToTransform(PLyObToDatum *arg, PyObject *plrv,
+					  bool *isnull, bool inarray);
+static Datum PLySequence_ToArray(PLyObToDatum *arg, PyObject *plrv,
+					bool *isnull, bool inarray);
 static void PLySequence_ToArray_recurse(PLyObToDatum *elm, PyObject *list,
 							int *dims, int ndim, int dim,
 							Datum *elems, bool *nulls, int *currelem);
 
-/* conversion from Python objects to composite Datums (used by triggers and SRFs) */
-static Datum PLyString_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *string, bool inarray);
-static Datum PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping);
-static Datum PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence);
-static Datum PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object, bool inarray);
+/* conversion from Python objects to composite Datums */
+static Datum PLyString_ToComposite(PLyObToDatum *arg, PyObject *string, bool inarray);
+static Datum PLyMapping_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *mapping);
+static Datum PLySequence_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *sequence);
+static Datum PLyGenericObject_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *object, bool inarray);
 
-void
-PLy_typeinfo_init(PLyTypeInfo *arg, MemoryContext mcxt)
-{
-	arg->is_rowtype = -1;
-	arg->in.r.natts = arg->out.r.natts = 0;
-	arg->in.r.atts = NULL;
-	arg->out.r.atts = NULL;
-	arg->typ_relid = InvalidOid;
-	arg->typrel_xmin = InvalidTransactionId;
-	ItemPointerSetInvalid(&arg->typrel_tid);
-	arg->mcxt = mcxt;
-}
 
 /*
  * Conversion functions.  Remember output from Python is input to
  * PostgreSQL, and vice versa.
  */
-void
-PLy_input_datum_func(PLyTypeInfo *arg, Oid typeOid, HeapTuple typeTup, Oid langid, List *trftypes)
+
+/*
+ * Perform input conversion, given correctly-set-up state information.
+ *
+ * This is the outer-level entry point for any input conversion.  Internally,
+ * the conversion functions recurse directly to each other.
+ */
+PyObject *
+PLy_input_convert(PLyDatumToOb *arg, Datum val)
 {
-	if (arg->is_rowtype > 0)
-		elog(ERROR, "PLyTypeInfo struct is initialized for Tuple");
-	arg->is_rowtype = 0;
-	PLy_input_datum_func2(&(arg->in.d), arg->mcxt, typeOid, typeTup, langid, trftypes);
+	PyObject   *result;
+	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
+	MemoryContext scratch_context = PLy_get_scratch_context(exec_ctx);
+	MemoryContext oldcontext;
+
+	/*
+	 * Do the work in the scratch context to avoid leaking memory from the
+	 * datatype output function calls.  (The individual PLyDatumToObFunc
+	 * functions can't reset the scratch context, because they recurse and an
+	 * inner one might clobber data an outer one still needs.  So we do it
+	 * once at the outermost recursion level.)
+	 *
+	 * We reset the scratch context before, not after, each conversion cycle.
+	 * This way we aren't on the hook to release a Python refcount on the
+	 * result object in case MemoryContextReset throws an error.
+	 */
+	MemoryContextReset(scratch_context);
+
+	oldcontext = MemoryContextSwitchTo(scratch_context);
+
+	result = arg->func(arg, val);
+
+	MemoryContextSwitchTo(oldcontext);
+
+	return result;
 }
 
-void
-PLy_output_datum_func(PLyTypeInfo *arg, HeapTuple typeTup, Oid langid, List *trftypes)
+/*
+ * Perform output conversion, given correctly-set-up state information.
+ *
+ * This is the outer-level entry point for any output conversion.  Internally,
+ * the conversion functions recurse directly to each other.
+ *
+ * The result, as well as any cruft generated along the way, are in the
+ * current memory context.  Caller is responsible for cleanup.
+ */
+Datum
+PLy_output_convert(PLyObToDatum *arg, PyObject *val, bool *isnull)
 {
-	if (arg->is_rowtype > 0)
-		elog(ERROR, "PLyTypeInfo struct is initialized for a Tuple");
-	arg->is_rowtype = 0;
-	PLy_output_datum_func2(&(arg->out.d), arg->mcxt, typeTup, langid, trftypes);
+	/* at outer level, we are not considering an array element */
+	return arg->func(arg, val, isnull, false);
 }
 
-void
-PLy_input_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc)
+/*
+ * Transform a tuple into a Python dict object.
+ *
+ * Note: the tupdesc must match the one used to set up *arg.  We could
+ * insist that this function lookup the tupdesc from what is in *arg,
+ * but in practice all callers have the right tupdesc available.
+ */
+PyObject *
+PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
 {
-	int			i;
+	PyObject   *dict;
 	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
-	MemoryContext oldcxt;
+	MemoryContext scratch_context = PLy_get_scratch_context(exec_ctx);
+	MemoryContext oldcontext;
 
-	oldcxt = MemoryContextSwitchTo(arg->mcxt);
+	/*
+	 * As in PLy_input_convert, do the work in the scratch context.
+	 */
+	MemoryContextReset(scratch_context);
 
-	if (arg->is_rowtype == 0)
-		elog(ERROR, "PLyTypeInfo struct is initialized for a Datum");
-	arg->is_rowtype = 1;
+	oldcontext = MemoryContextSwitchTo(scratch_context);
 
-	if (arg->in.r.natts != desc->natts)
-	{
-		if (arg->in.r.atts)
-			pfree(arg->in.r.atts);
-		arg->in.r.natts = desc->natts;
-		arg->in.r.atts = palloc0(desc->natts * sizeof(PLyDatumToOb));
-	}
+	dict = PLyDict_FromTuple(arg, tuple, desc);
 
-	/* Can this be an unnamed tuple? If not, then an Assert would be enough */
-	if (desc->tdtypmod != -1)
-		elog(ERROR, "received unnamed record type as input");
+	MemoryContextSwitchTo(oldcontext);
 
-	Assert(OidIsValid(desc->tdtypeid));
+	return dict;
+}
 
-	/*
-	 * RECORDOID means we got called to create input functions for a tuple
-	 * fetched by plpy.execute or for an anonymous record type
-	 */
-	if (desc->tdtypeid != RECORDOID)
-	{
-		HeapTuple	relTup;
+/*
+ * Initialize, or re-initialize, per-column input info for a composite type.
+ *
+ * This is separate from PLy_input_setup_func() because in cases involving
+ * anonymous record types, we need to be passed the tupdesc explicitly.
+ * It's caller's responsibility that the tupdesc has adequate lifespan
+ * in such cases.  If the tupdesc is for a named composite or registered
+ * record type, it does not need to be long-lived.
+ */
+void
+PLy_input_setup_tuple(PLyDatumToOb *arg, TupleDesc desc, PLyProcedure *proc)
+{
+	int			i;
 
-		/* Get the pg_class tuple corresponding to the type of the input */
-		arg->typ_relid = typeidTypeRelid(desc->tdtypeid);
-		relTup = SearchSysCache1(RELOID, ObjectIdGetDatum(arg->typ_relid));
-		if (!HeapTupleIsValid(relTup))
-			elog(ERROR, "cache lookup failed for relation %u", arg->typ_relid);
+	/* We should be working on a previously-set-up struct */
+	Assert(arg->func == PLyDict_FromComposite);
 
-		/* Remember XMIN and TID for later validation if cache is still OK */
-		arg->typrel_xmin = HeapTupleHeaderGetRawXmin(relTup->t_data);
-		arg->typrel_tid = relTup->t_self;
+	/* Save pointer to tupdesc, but only if this is an anonymous record type */
+	if (arg->typoid == RECORDOID && arg->typmod < 0)
+		arg->u.tuple.recdesc = desc;
 
-		ReleaseSysCache(relTup);
+	/* (Re)allocate atts array as needed */
+	if (arg->u.tuple.natts != desc->natts)
+	{
+		if (arg->u.tuple.atts)
+			pfree(arg->u.tuple.atts);
+		arg->u.tuple.natts = desc->natts;
+		arg->u.tuple.atts = (PLyDatumToOb *)
+			MemoryContextAllocZero(arg->mcxt,
+								   desc->natts * sizeof(PLyDatumToOb));
 	}
 
+	/* Fill the atts entries, except for dropped columns */
 	for (i = 0; i < desc->natts; i++)
 	{
-		HeapTuple	typeTup;
 		Form_pg_attribute attr = TupleDescAttr(desc, i);
+		PLyDatumToOb *att = &arg->u.tuple.atts[i];
 
 		if (attr->attisdropped)
 			continue;
 
-		if (arg->in.r.atts[i].typoid == attr->atttypid)
+		if (att->typoid == attr->atttypid && att->typmod == attr->atttypmod)
 			continue;			/* already set up this entry */
 
-		typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(attr->atttypid));
-		if (!HeapTupleIsValid(typeTup))
-			elog(ERROR, "cache lookup failed for type %u",
-				 attr->atttypid);
-
-		PLy_input_datum_func2(&(arg->in.r.atts[i]), arg->mcxt,
-							  attr->atttypid,
-							  typeTup,
-							  exec_ctx->curr_proc->langid,
-							  exec_ctx->curr_proc->trftypes);
-
-		ReleaseSysCache(typeTup);
+		PLy_input_setup_func(att, arg->mcxt,
+							 attr->atttypid, attr->atttypmod,
+							 proc);
 	}
-
-	MemoryContextSwitchTo(oldcxt);
 }
 
+/*
+ * Initialize, or re-initialize, per-column output info for a composite type.
+ *
+ * This is separate from PLy_output_setup_func() because in cases involving
+ * anonymous record types, we need to be passed the tupdesc explicitly.
+ * It's caller's responsibility that the tupdesc has adequate lifespan
+ * in such cases.  If the tupdesc is for a named composite or registered
+ * record type, it does not need to be long-lived.
+ */
 void
-PLy_output_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc)
+PLy_output_setup_tuple(PLyObToDatum *arg, TupleDesc desc, PLyProcedure *proc)
 {
 	int			i;
-	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
-	MemoryContext oldcxt;
-
-	oldcxt = MemoryContextSwitchTo(arg->mcxt);
-
-	if (arg->is_rowtype == 0)
-		elog(ERROR, "PLyTypeInfo struct is initialized for a Datum");
-	arg->is_rowtype = 1;
 
-	if (arg->out.r.natts != desc->natts)
-	{
-		if (arg->out.r.atts)
-			pfree(arg->out.r.atts);
-		arg->out.r.natts = desc->natts;
-		arg->out.r.atts = palloc0(desc->natts * sizeof(PLyObToDatum));
-	}
+	/* We should be working on a previously-set-up struct */
+	Assert(arg->func == PLyObject_ToComposite);
 
-	Assert(OidIsValid(desc->tdtypeid));
+	/* Save pointer to tupdesc, but only if this is an anonymous record type */
+	if (arg->typoid == RECORDOID && arg->typmod < 0)
+		arg->u.tuple.recdesc = desc;
 
-	/*
-	 * RECORDOID means we got called to create output functions for an
-	 * anonymous record type
-	 */
-	if (desc->tdtypeid != RECORDOID)
+	/* (Re)allocate atts array as needed */
+	if (arg->u.tuple.natts != desc->natts)
 	{
-		HeapTuple	relTup;
-
-		/* Get the pg_class tuple corresponding to the type of the output */
-		arg->typ_relid = typeidTypeRelid(desc->tdtypeid);
-		relTup = SearchSysCache1(RELOID, ObjectIdGetDatum(arg->typ_relid));
-		if (!HeapTupleIsValid(relTup))
-			elog(ERROR, "cache lookup failed for relation %u", arg->typ_relid);
-
-		/* Remember XMIN and TID for later validation if cache is still OK */
-		arg->typrel_xmin = HeapTupleHeaderGetRawXmin(relTup->t_data);
-		arg->typrel_tid = relTup->t_self;
-
-		ReleaseSysCache(relTup);
+		if (arg->u.tuple.atts)
+			pfree(arg->u.tuple.atts);
+		arg->u.tuple.natts = desc->natts;
+		arg->u.tuple.atts = (PLyObToDatum *)
+			MemoryContextAllocZero(arg->mcxt,
+								   desc->natts * sizeof(PLyObToDatum));
 	}
 
+	/* Fill the atts entries, except for dropped columns */
 	for (i = 0; i < desc->natts; i++)
 	{
-		HeapTuple	typeTup;
 		Form_pg_attribute attr = TupleDescAttr(desc, i);
+		PLyObToDatum *att = &arg->u.tuple.atts[i];
 
 		if (attr->attisdropped)
 			continue;
 
-		if (arg->out.r.atts[i].typoid == attr->atttypid)
+		if (att->typoid == attr->atttypid && att->typmod == attr->atttypmod)
 			continue;			/* already set up this entry */
 
-		typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(attr->atttypid));
-		if (!HeapTupleIsValid(typeTup))
-			elog(ERROR, "cache lookup failed for type %u",
-				 attr->atttypid);
-
-		PLy_output_datum_func2(&(arg->out.r.atts[i]), arg->mcxt, typeTup,
-							   exec_ctx->curr_proc->langid,
-							   exec_ctx->curr_proc->trftypes);
-
-		ReleaseSysCache(typeTup);
+		PLy_output_setup_func(att, arg->mcxt,
+							  attr->atttypid, attr->atttypmod,
+							  proc);
 	}
-
-	MemoryContextSwitchTo(oldcxt);
 }
 
+/*
+ * Set up output info for a PL/Python function returning record.
+ *
+ * Note: the given tupdesc is not necessarily long-lived.
+ */
 void
-PLy_output_record_funcs(PLyTypeInfo *arg, TupleDesc desc)
+PLy_output_setup_record(PLyObToDatum *arg, TupleDesc desc, PLyProcedure *proc)
 {
+	/* Makes no sense unless RECORD */
+	Assert(arg->typoid == RECORDOID);
+	Assert(desc->tdtypeid == RECORDOID);
+
 	/*
-	 * If the output record functions are already set, we just have to check
-	 * if the record descriptor has not changed
+	 * Bless the record type if not already done.  We'd have to do this anyway
+	 * to return a tuple, so we might as well force the issue so we can use
+	 * the known-record-type code path.
 	 */
-	if ((arg->is_rowtype == 1) &&
-		(arg->out.d.typmod != -1) &&
-		(arg->out.d.typmod == desc->tdtypmod))
-		return;
-
-	/* bless the record to make it known to the typcache lookup code */
 	BlessTupleDesc(desc);
-	/* save the freshly generated typmod */
-	arg->out.d.typmod = desc->tdtypmod;
-	/* proceed with normal I/O function caching */
-	PLy_output_tuple_funcs(arg, desc);
 
 	/*
-	 * it should change is_rowtype to 1, so we won't go through this again
-	 * unless the output record description changes
+	 * Update arg->typmod, and clear the recdesc link if it's changed. The
+	 * next call of PLyObject_ToComposite will look up a long-lived tupdesc
+	 * for the record type.
 	 */
-	Assert(arg->is_rowtype == 1);
+	arg->typmod = desc->tdtypmod;
+	if (arg->u.tuple.recdesc &&
+		arg->u.tuple.recdesc->tdtypmod != arg->typmod)
+		arg->u.tuple.recdesc = NULL;
+
+	/* Update derived data if necessary */
+	PLy_output_setup_tuple(arg, desc, proc);
 }
 
 /*
- * Transform a tuple into a Python dict object.
+ * Recursively initialize the PLyObToDatum structure(s) needed to construct
+ * a SQL value of the specified typeOid/typmod from a Python value.
+ * (But note that at this point we may have RECORDOID/-1, ie, an indeterminate
+ * record type.)
+ * proc is used to look up transform functions.
  */
-PyObject *
-PLyDict_FromTuple(PLyTypeInfo *info, HeapTuple tuple, TupleDesc desc)
+void
+PLy_output_setup_func(PLyObToDatum *arg, MemoryContext arg_mcxt,
+					  Oid typeOid, int32 typmod,
+					  PLyProcedure *proc)
 {
-	PyObject   *volatile dict;
-	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
-	MemoryContext scratch_context = PLy_get_scratch_context(exec_ctx);
-	MemoryContext oldcontext = CurrentMemoryContext;
+	TypeCacheEntry *typentry;
+	char		typtype;
+	Oid			trfuncid;
+	Oid			typinput;
 
-	if (info->is_rowtype != 1)
-		elog(ERROR, "PLyTypeInfo structure describes a datum");
+	/* Since this is recursive, it could theoretically be driven to overflow */
+	check_stack_depth();
 
-	dict = PyDict_New();
-	if (dict == NULL)
-		PLy_elog(ERROR, "could not create new dictionary");
+	arg->typoid = typeOid;
+	arg->typmod = typmod;
+	arg->mcxt = arg_mcxt;
 
-	PG_TRY();
+	/*
+	 * Fetch typcache entry for the target type, asking for whatever info
+	 * we'll need later.  RECORD is a special case: just treat it as composite
+	 * without bothering with the typcache entry.
+	 */
+	if (typeOid != RECORDOID)
 	{
-		int			i;
-
-		/*
-		 * Do the work in the scratch context to avoid leaking memory from the
-		 * datatype output function calls.
-		 */
-		MemoryContextSwitchTo(scratch_context);
-		for (i = 0; i < info->in.r.natts; i++)
-		{
-			char	   *key;
-			Datum		vattr;
-			bool		is_null;
-			PyObject   *value;
-			Form_pg_attribute attr = TupleDescAttr(desc, i);
-
-			if (attr->attisdropped)
-				continue;
-
-			key = NameStr(attr->attname);
-			vattr = heap_getattr(tuple, (i + 1), desc, &is_null);
-
-			if (is_null || info->in.r.atts[i].func == NULL)
-				PyDict_SetItemString(dict, key, Py_None);
-			else
-			{
-				value = (info->in.r.atts[i].func) (&info->in.r.atts[i], vattr);
-				PyDict_SetItemString(dict, key, value);
-				Py_DECREF(value);
-			}
-		}
-		MemoryContextSwitchTo(oldcontext);
-		MemoryContextReset(scratch_context);
+		typentry = lookup_type_cache(typeOid, TYPECACHE_DOMAIN_BASE_INFO);
+		typtype = typentry->typtype;
+		arg->typbyval = typentry->typbyval;
+		arg->typlen = typentry->typlen;
+		arg->typalign = typentry->typalign;
 	}
-	PG_CATCH();
+	else
 	{
-		MemoryContextSwitchTo(oldcontext);
-		Py_DECREF(dict);
-		PG_RE_THROW();
+		typentry = NULL;
+		typtype = TYPTYPE_COMPOSITE;
+		/* hard-wired knowledge about type RECORD: */
+		arg->typbyval = false;
+		arg->typlen = -1;
+		arg->typalign = 'd';
 	}
-	PG_END_TRY();
-
-	return dict;
-}
-
-/*
- *	Convert a Python object to a composite Datum, using all supported
- *	conversion methods: composite as a string, as a sequence, as a mapping or
- *	as an object that has __getattr__ support.
- */
-Datum
-PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv, bool inarray)
-{
-	Datum		datum;
-
-	if (PyString_Check(plrv) || PyUnicode_Check(plrv))
-		datum = PLyString_ToComposite(info, desc, plrv, inarray);
-	else if (PySequence_Check(plrv))
-		/* composite type as sequence (tuple, list etc) */
-		datum = PLySequence_ToComposite(info, desc, plrv);
-	else if (PyMapping_Check(plrv))
-		/* composite type as mapping (currently only dict) */
-		datum = PLyMapping_ToComposite(info, desc, plrv);
-	else
-		/* returned as smth, must provide method __getattr__(name) */
-		datum = PLyGenericObject_ToComposite(info, desc, plrv, inarray);
-
-	return datum;
-}
-
-static void
-PLy_output_datum_func2(PLyObToDatum *arg, MemoryContext arg_mcxt, HeapTuple typeTup, Oid langid, List *trftypes)
-{
-	Form_pg_type typeStruct = (Form_pg_type) GETSTRUCT(typeTup);
-	Oid			element_type;
-	Oid			base_type;
-	Oid			funcid;
-	MemoryContext oldcxt;
-
-	oldcxt = MemoryContextSwitchTo(arg_mcxt);
-
-	fmgr_info_cxt(typeStruct->typinput, &arg->typfunc, arg_mcxt);
-	arg->typoid = HeapTupleGetOid(typeTup);
-	arg->typmod = -1;
-	arg->typioparam = getTypeIOParam(typeTup);
-	arg->typbyval = typeStruct->typbyval;
-
-	element_type = get_base_element_type(arg->typoid);
-	base_type = getBaseType(element_type ? element_type : arg->typoid);
 
 	/*
-	 * Select a conversion function to convert Python objects to PostgreSQL
-	 * datums.
+	 * Choose conversion method.  Note that transform functions are checked
+	 * for composite and scalar types, but not for arrays or domains.  This is
+	 * somewhat historical, but we'd have a problem allowing them on domains,
+	 * since we drill down through all levels of a domain nest without looking
+	 * at the intermediate levels at all.
 	 */
-
-	if ((funcid = get_transform_tosql(base_type, langid, trftypes)))
+	if (typtype == TYPTYPE_DOMAIN)
+	{
+		/* Domain */
+		arg->func = PLyObject_ToDomain;
+		arg->u.domain.domain_info = NULL;
+		/* Recursively set up conversion info for the element type */
+		arg->u.domain.base = (PLyObToDatum *)
+			MemoryContextAllocZero(arg_mcxt, sizeof(PLyObToDatum));
+		PLy_output_setup_func(arg->u.domain.base, arg_mcxt,
+							  typentry->domainBaseType,
+							  typentry->domainBaseTypmod,
+							  proc);
+	}
+	else if (typentry &&
+			 OidIsValid(typentry->typelem) && typentry->typlen == -1)
+	{
+		/* Standard varlena array (cf. get_element_type) */
+		arg->func = PLySequence_ToArray;
+		/* Get base type OID to insert into constructed array */
+		/* (note this might not be the same as the immediate child type) */
+		arg->u.array.elmbasetype = getBaseType(typentry->typelem);
+		/* Recursively set up conversion info for the element type */
+		arg->u.array.elm = (PLyObToDatum *)
+			MemoryContextAllocZero(arg_mcxt, sizeof(PLyObToDatum));
+		PLy_output_setup_func(arg->u.array.elm, arg_mcxt,
+							  typentry->typelem, typmod,
+							  proc);
+	}
+	else if ((trfuncid = get_transform_tosql(typeOid,
+											 proc->langid,
+											 proc->trftypes)))
 	{
 		arg->func = PLyObject_ToTransform;
-		fmgr_info_cxt(funcid, &arg->typtransform, arg_mcxt);
+		fmgr_info_cxt(trfuncid, &arg->u.transform.typtransform, arg_mcxt);
 	}
-	else if (typeStruct->typtype == TYPTYPE_COMPOSITE)
+	else if (typtype == TYPTYPE_COMPOSITE)
 	{
+		/* Named composite type, or RECORD */
 		arg->func = PLyObject_ToComposite;
+		/* We'll set up the per-field data later */
+		arg->u.tuple.recdesc = NULL;
+		arg->u.tuple.typentry = typentry;
+		arg->u.tuple.tupdescseq = typentry ? typentry->tupDescSeqNo - 1 : 0;
+		arg->u.tuple.atts = NULL;
+		arg->u.tuple.natts = 0;
+		/* Mark this invalid till needed, too */
+		arg->u.tuple.recinfunc.fn_oid = InvalidOid;
 	}
 	else
-		switch (base_type)
+	{
+		/* Scalar type, but we have a couple of special cases */
+		switch (typeOid)
 		{
 			case BOOLOID:
 				arg->func = PLyObject_ToBool;
@@ -406,66 +402,111 @@ PLy_output_datum_func2(PLyObToDatum *arg, MemoryContext arg_mcxt, HeapTuple type
 				arg->func = PLyObject_ToBytea;
 				break;
 			default:
-				arg->func = PLyObject_ToDatum;
+				arg->func = PLyObject_ToScalar;
+				getTypeInputInfo(typeOid, &typinput, &arg->u.scalar.typioparam);
+				fmgr_info_cxt(typinput, &arg->u.scalar.typfunc, arg_mcxt);
 				break;
 		}
-
-	if (element_type)
-	{
-		char		dummy_delim;
-		Oid			funcid;
-
-		if (type_is_rowtype(element_type))
-			arg->func = PLyObject_ToComposite;
-
-		arg->elm = palloc0(sizeof(*arg->elm));
-		arg->elm->func = arg->func;
-		arg->elm->typtransform = arg->typtransform;
-		arg->func = PLySequence_ToArray;
-
-		arg->elm->typoid = element_type;
-		arg->elm->typmod = -1;
-		get_type_io_data(element_type, IOFunc_input,
-						 &arg->elm->typlen, &arg->elm->typbyval, &arg->elm->typalign, &dummy_delim,
-						 &arg->elm->typioparam, &funcid);
-		fmgr_info_cxt(funcid, &arg->elm->typfunc, arg_mcxt);
 	}
-
-	MemoryContextSwitchTo(oldcxt);
 }
 
-static void
-PLy_input_datum_func2(PLyDatumToOb *arg, MemoryContext arg_mcxt, Oid typeOid, HeapTuple typeTup, Oid langid, List *trftypes)
+/*
+ * Recursively initialize the PLyDatumToOb structure(s) needed to construct
+ * a Python value from a SQL value of the specified typeOid/typmod.
+ * (But note that at this point we may have RECORDOID/-1, ie, an indeterminate
+ * record type.)
+ * proc is used to look up transform functions.
+ */
+void
+PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt,
+					 Oid typeOid, int32 typmod,
+					 PLyProcedure *proc)
 {
-	Form_pg_type typeStruct = (Form_pg_type) GETSTRUCT(typeTup);
-	Oid			element_type;
-	Oid			base_type;
-	Oid			funcid;
-	MemoryContext oldcxt;
-
-	oldcxt = MemoryContextSwitchTo(arg_mcxt);
+	TypeCacheEntry *typentry;
+	char		typtype;
+	Oid			trfuncid;
+	Oid			typoutput;
+	bool		typisvarlena;
 
-	/* Get the type's conversion information */
-	fmgr_info_cxt(typeStruct->typoutput, &arg->typfunc, arg_mcxt);
-	arg->typoid = HeapTupleGetOid(typeTup);
-	arg->typmod = -1;
-	arg->typioparam = getTypeIOParam(typeTup);
-	arg->typbyval = typeStruct->typbyval;
-	arg->typlen = typeStruct->typlen;
-	arg->typalign = typeStruct->typalign;
+	/* Since this is recursive, it could theoretically be driven to overflow */
+	check_stack_depth();
 
-	/* Determine which kind of Python object we will convert to */
+	arg->typoid = typeOid;
+	arg->typmod = typmod;
+	arg->mcxt = arg_mcxt;
 
-	element_type = get_base_element_type(typeOid);
-	base_type = getBaseType(element_type ? element_type : typeOid);
+	/*
+	 * Fetch typcache entry for the target type, asking for whatever info
+	 * we'll need later.  RECORD is a special case: just treat it as composite
+	 * without bothering with the typcache entry.
+	 */
+	if (typeOid != RECORDOID)
+	{
+		typentry = lookup_type_cache(typeOid, TYPECACHE_DOMAIN_BASE_INFO);
+		typtype = typentry->typtype;
+		arg->typbyval = typentry->typbyval;
+		arg->typlen = typentry->typlen;
+		arg->typalign = typentry->typalign;
+	}
+	else
+	{
+		typentry = NULL;
+		typtype = TYPTYPE_COMPOSITE;
+		/* hard-wired knowledge about type RECORD: */
+		arg->typbyval = false;
+		arg->typlen = -1;
+		arg->typalign = 'd';
+	}
 
-	if ((funcid = get_transform_fromsql(base_type, langid, trftypes)))
+	/*
+	 * Choose conversion method.  Note that transform functions are checked
+	 * for composite and scalar types, but not for arrays or domains.  This is
+	 * somewhat historical, but we'd have a problem allowing them on domains,
+	 * since we drill down through all levels of a domain nest without looking
+	 * at the intermediate levels at all.
+	 */
+	if (typtype == TYPTYPE_DOMAIN)
+	{
+		/* Domain --- we don't care, just recurse down to the base type */
+		PLy_input_setup_func(arg, arg_mcxt,
+							 typentry->domainBaseType,
+							 typentry->domainBaseTypmod,
+							 proc);
+	}
+	else if (typentry &&
+			 OidIsValid(typentry->typelem) && typentry->typlen == -1)
+	{
+		/* Standard varlena array (cf. get_element_type) */
+		arg->func = PLyList_FromArray;
+		/* Recursively set up conversion info for the element type */
+		arg->u.array.elm = (PLyDatumToOb *)
+			MemoryContextAllocZero(arg_mcxt, sizeof(PLyDatumToOb));
+		PLy_input_setup_func(arg->u.array.elm, arg_mcxt,
+							 typentry->typelem, typmod,
+							 proc);
+	}
+	else if ((trfuncid = get_transform_fromsql(typeOid,
+											   proc->langid,
+											   proc->trftypes)))
 	{
 		arg->func = PLyObject_FromTransform;
-		fmgr_info_cxt(funcid, &arg->typtransform, arg_mcxt);
+		fmgr_info_cxt(trfuncid, &arg->u.transform.typtransform, arg_mcxt);
+	}
+	else if (typtype == TYPTYPE_COMPOSITE)
+	{
+		/* Named composite type, or RECORD */
+		arg->func = PLyDict_FromComposite;
+		/* We'll set up the per-field data later */
+		arg->u.tuple.recdesc = NULL;
+		arg->u.tuple.typentry = typentry;
+		arg->u.tuple.tupdescseq = typentry ? typentry->tupDescSeqNo - 1 : 0;
+		arg->u.tuple.atts = NULL;
+		arg->u.tuple.natts = 0;
 	}
 	else
-		switch (base_type)
+	{
+		/* Scalar type, but we have a couple of special cases */
+		switch (typeOid)
 		{
 			case BOOLOID:
 				arg->func = PLyBool_FromBool;
@@ -495,30 +536,19 @@ PLy_input_datum_func2(PLyDatumToOb *arg, MemoryContext arg_mcxt, Oid typeOid, He
 				arg->func = PLyBytes_FromBytea;
 				break;
 			default:
-				arg->func = PLyString_FromDatum;
+				arg->func = PLyString_FromScalar;
+				getTypeOutputInfo(typeOid, &typoutput, &typisvarlena);
+				fmgr_info_cxt(typoutput, &arg->u.scalar.typfunc, arg_mcxt);
 				break;
 		}
-
-	if (element_type)
-	{
-		char		dummy_delim;
-		Oid			funcid;
-
-		arg->elm = palloc0(sizeof(*arg->elm));
-		arg->elm->func = arg->func;
-		arg->elm->typtransform = arg->typtransform;
-		arg->func = PLyList_FromArray;
-		arg->elm->typoid = element_type;
-		arg->elm->typmod = -1;
-		get_type_io_data(element_type, IOFunc_output,
-						 &arg->elm->typlen, &arg->elm->typbyval, &arg->elm->typalign, &dummy_delim,
-						 &arg->elm->typioparam, &funcid);
-		fmgr_info_cxt(funcid, &arg->elm->typfunc, arg_mcxt);
 	}
-
-	MemoryContextSwitchTo(oldcxt);
 }
 
+
+/*
+ * Special-purpose input converters.
+ */
+
 static PyObject *
 PLyBool_FromBool(PLyDatumToOb *arg, Datum d)
 {
@@ -611,27 +641,40 @@ PLyBytes_FromBytea(PLyDatumToOb *arg, Datum d)
 	return PyBytes_FromStringAndSize(str, size);
 }
 
+
+/*
+ * Generic input conversion using a SQL type's output function.
+ */
 static PyObject *
-PLyString_FromDatum(PLyDatumToOb *arg, Datum d)
+PLyString_FromScalar(PLyDatumToOb *arg, Datum d)
 {
-	char	   *x = OutputFunctionCall(&arg->typfunc, d);
+	char	   *x = OutputFunctionCall(&arg->u.scalar.typfunc, d);
 	PyObject   *r = PyString_FromString(x);
 
 	pfree(x);
 	return r;
 }
 
+/*
+ * Convert using a from-SQL transform function.
+ */
 static PyObject *
 PLyObject_FromTransform(PLyDatumToOb *arg, Datum d)
 {
-	return (PyObject *) DatumGetPointer(FunctionCall1(&arg->typtransform, d));
+	Datum		t;
+
+	t = FunctionCall1(&arg->u.transform.typtransform, d);
+	return (PyObject *) DatumGetPointer(t);
 }
 
+/*
+ * Convert a SQL array to a Python list.
+ */
 static PyObject *
 PLyList_FromArray(PLyDatumToOb *arg, Datum d)
 {
 	ArrayType  *array = DatumGetArrayTypeP(d);
-	PLyDatumToOb *elm = arg->elm;
+	PLyDatumToOb *elm = arg->u.array.elm;
 	int			ndim;
 	int		   *dims;
 	char	   *dataptr;
@@ -736,6 +779,94 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 	return list;
 }
 
+/*
+ * Convert a composite SQL value to a Python dict.
+ */
+static PyObject *
+PLyDict_FromComposite(PLyDatumToOb *arg, Datum d)
+{
+	PyObject   *dict;
+	HeapTupleHeader td;
+	Oid			tupType;
+	int32		tupTypmod;
+	TupleDesc	tupdesc;
+	HeapTupleData tmptup;
+
+	td = DatumGetHeapTupleHeader(d);
+	/* Extract rowtype info and find a tupdesc */
+	tupType = HeapTupleHeaderGetTypeId(td);
+	tupTypmod = HeapTupleHeaderGetTypMod(td);
+	tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
+
+	/* Set up I/O funcs if not done yet */
+	PLy_input_setup_tuple(arg, tupdesc,
+						  PLy_current_execution_context()->curr_proc);
+
+	/* Build a temporary HeapTuple control structure */
+	tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
+	tmptup.t_data = td;
+
+	dict = PLyDict_FromTuple(arg, &tmptup, tupdesc);
+
+	ReleaseTupleDesc(tupdesc);
+
+	return dict;
+}
+
+/*
+ * Transform a tuple into a Python dict object.
+ */
+static PyObject *
+PLyDict_FromTuple(PLyDatumToOb *arg, HeapTuple tuple, TupleDesc desc)
+{
+	PyObject   *volatile dict;
+
+	/* Simple sanity check that desc matches */
+	Assert(desc->natts == arg->u.tuple.natts);
+
+	dict = PyDict_New();
+	if (dict == NULL)
+		PLy_elog(ERROR, "could not create new dictionary");
+
+	PG_TRY();
+	{
+		int			i;
+
+		for (i = 0; i < arg->u.tuple.natts; i++)
+		{
+			PLyDatumToOb *att = &arg->u.tuple.atts[i];
+			Form_pg_attribute attr = TupleDescAttr(desc, i);
+			char	   *key;
+			Datum		vattr;
+			bool		is_null;
+			PyObject   *value;
+
+			if (attr->attisdropped)
+				continue;
+
+			key = NameStr(attr->attname);
+			vattr = heap_getattr(tuple, (i + 1), desc, &is_null);
+
+			if (is_null)
+				PyDict_SetItemString(dict, key, Py_None);
+			else
+			{
+				value = att->func(att, vattr);
+				PyDict_SetItemString(dict, key, value);
+				Py_DECREF(value);
+			}
+		}
+	}
+	PG_CATCH();
+	{
+		Py_DECREF(dict);
+		PG_RE_THROW();
+	}
+	PG_END_TRY();
+
+	return dict;
+}
+
 /*
  * Convert a Python object to a PostgreSQL bool datum.  This can't go
  * through the generic conversion function, because Python attaches a
@@ -743,17 +874,16 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
  * type can parse.
  */
 static Datum
-PLyObject_ToBool(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
+PLyObject_ToBool(PLyObToDatum *arg, PyObject *plrv,
+				 bool *isnull, bool inarray)
 {
-	Datum		rv;
-
-	Assert(plrv != Py_None);
-	rv = BoolGetDatum(PyObject_IsTrue(plrv));
-
-	if (get_typtype(arg->typoid) == TYPTYPE_DOMAIN)
-		domain_check(rv, false, arg->typoid, &arg->typfunc.fn_extra, arg->typfunc.fn_mcxt);
-
-	return rv;
+	if (plrv == Py_None)
+	{
+		*isnull = true;
+		return (Datum) 0;
+	}
+	*isnull = false;
+	return BoolGetDatum(PyObject_IsTrue(plrv));
 }
 
 /*
@@ -762,12 +892,18 @@ PLyObject_ToBool(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
  * with embedded nulls.  And it's faster this way.
  */
 static Datum
-PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
+PLyObject_ToBytea(PLyObToDatum *arg, PyObject *plrv,
+				  bool *isnull, bool inarray)
 {
 	PyObject   *volatile plrv_so = NULL;
 	Datum		rv;
 
-	Assert(plrv != Py_None);
+	if (plrv == Py_None)
+	{
+		*isnull = true;
+		return (Datum) 0;
+	}
+	*isnull = false;
 
 	plrv_so = PyObject_Bytes(plrv);
 	if (!plrv_so)
@@ -793,9 +929,6 @@ PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
 
 	Py_XDECREF(plrv_so);
 
-	if (get_typtype(arg->typoid) == TYPTYPE_DOMAIN)
-		domain_check(rv, false, arg->typoid, &arg->typfunc.fn_extra, arg->typfunc.fn_mcxt);
-
 	return rv;
 }
 
@@ -806,45 +939,87 @@ PLyObject_ToBytea(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
  * for obtaining PostgreSQL tuples.
  */
 static Datum
-PLyObject_ToComposite(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
+PLyObject_ToComposite(PLyObToDatum *arg, PyObject *plrv,
+					  bool *isnull, bool inarray)
 {
 	Datum		rv;
-	PLyTypeInfo info;
 	TupleDesc	desc;
-	MemoryContext cxt;
 
-	if (typmod != -1)
-		elog(ERROR, "received unnamed record type as input");
+	if (plrv == Py_None)
+	{
+		*isnull = true;
+		return (Datum) 0;
+	}
+	*isnull = false;
+
+	/*
+	 * The string conversion case doesn't require a tupdesc, nor per-field
+	 * conversion data, so just go for it if that's the case to use.
+	 */
+	if (PyString_Check(plrv) || PyUnicode_Check(plrv))
+		return PLyString_ToComposite(arg, plrv, inarray);
 
-	/* Create a dummy PLyTypeInfo */
-	cxt = AllocSetContextCreate(CurrentMemoryContext,
-								"PL/Python temp context",
-								ALLOCSET_DEFAULT_SIZES);
-	MemSet(&info, 0, sizeof(PLyTypeInfo));
-	PLy_typeinfo_init(&info, cxt);
-	/* Mark it as needing output routines lookup */
-	info.is_rowtype = 2;
+	/*
+	 * If we're dealing with a named composite type, we must look up the
+	 * tupdesc every time, to protect against possible changes to the type.
+	 * RECORD types can't change between calls; but we must still be willing
+	 * to set up the info the first time, if nobody did yet.
+	 */
+	if (arg->typoid != RECORDOID)
+	{
+		desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod);
+		/* We should have the descriptor of the type's typcache entry */
+		Assert(desc == arg->u.tuple.typentry->tupDesc);
+		/* Detect change of descriptor, update cache if needed */
+		if (arg->u.tuple.tupdescseq != arg->u.tuple.typentry->tupDescSeqNo)
+		{
+			PLy_output_setup_tuple(arg, desc,
+								   PLy_current_execution_context()->curr_proc);
+			arg->u.tuple.tupdescseq = arg->u.tuple.typentry->tupDescSeqNo;
+		}
+	}
+	else
+	{
+		desc = arg->u.tuple.recdesc;
+		if (desc == NULL)
+		{
+			desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod);
+			arg->u.tuple.recdesc = desc;
+		}
+		else
+		{
+			/* Pin descriptor to match unpin below */
+			PinTupleDesc(desc);
+		}
+	}
 
-	desc = lookup_rowtype_tupdesc(arg->typoid, arg->typmod);
+	/* Simple sanity check on our caching */
+	Assert(desc->natts == arg->u.tuple.natts);
 
 	/*
-	 * This will set up the dummy PLyTypeInfo's output conversion routines,
-	 * since we left is_rowtype as 2. A future optimization could be caching
-	 * that info instead of looking it up every time a tuple is returned from
-	 * the function.
+	 * Convert, using the appropriate method depending on the type of the
+	 * supplied Python object.
 	 */
-	rv = PLyObject_ToCompositeDatum(&info, desc, plrv, inarray);
+	if (PySequence_Check(plrv))
+		/* composite type as sequence (tuple, list etc) */
+		rv = PLySequence_ToComposite(arg, desc, plrv);
+	else if (PyMapping_Check(plrv))
+		/* composite type as mapping (currently only dict) */
+		rv = PLyMapping_ToComposite(arg, desc, plrv);
+	else
+		/* returned as smth, must provide method __getattr__(name) */
+		rv = PLyGenericObject_ToComposite(arg, desc, plrv, inarray);
 
 	ReleaseTupleDesc(desc);
 
-	MemoryContextDelete(cxt);
-
 	return rv;
 }
 
 
 /*
  * Convert Python object to C string in server encoding.
+ *
+ * Note: this is exported for use by add-on transform modules.
  */
 char *
 PLyObject_AsString(PyObject *plrv)
@@ -901,74 +1076,71 @@ PLyObject_AsString(PyObject *plrv)
 
 
 /*
- * Generic conversion function: Convert PyObject to cstring and
+ * Generic output conversion function: convert PyObject to cstring and
  * cstring into PostgreSQL type.
  */
 static Datum
-PLyObject_ToDatum(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
+PLyObject_ToScalar(PLyObToDatum *arg, PyObject *plrv,
+				   bool *isnull, bool inarray)
 {
 	char	   *str;
 
-	Assert(plrv != Py_None);
+	if (plrv == Py_None)
+	{
+		*isnull = true;
+		return (Datum) 0;
+	}
+	*isnull = false;
 
 	str = PLyObject_AsString(plrv);
 
-	/*
-	 * If we are parsing a composite type within an array, and the string
-	 * isn't a valid record literal, there's a high chance that the function
-	 * did something like:
-	 *
-	 * CREATE FUNCTION .. RETURNS comptype[] AS $$ return [['foo', 'bar']] $$
-	 * LANGUAGE plpython;
-	 *
-	 * Before PostgreSQL 10, that was interpreted as a single-dimensional
-	 * array, containing record ('foo', 'bar'). PostgreSQL 10 added support
-	 * for multi-dimensional arrays, and it is now interpreted as a
-	 * two-dimensional array, containing two records, 'foo', and 'bar'.
-	 * record_in() will throw an error, because "foo" is not a valid record
-	 * literal.
-	 *
-	 * To make that less confusing to users who are upgrading from older
-	 * versions, try to give a hint in the typical instances of that. If we
-	 * are parsing an array of composite types, and we see a string literal
-	 * that is not a valid record literal, give a hint. We only want to give
-	 * the hint in the narrow case of a malformed string literal, not any
-	 * error from record_in(), so check for that case here specifically.
-	 *
-	 * This check better match the one in record_in(), so that we don't forbid
-	 * literals that are actually valid!
-	 */
-	if (inarray && arg->typfunc.fn_oid == F_RECORD_IN)
-	{
-		char	   *ptr = str;
+	return InputFunctionCall(&arg->u.scalar.typfunc,
+							 str,
+							 arg->u.scalar.typioparam,
+							 arg->typmod);
+}
 
-		/* Allow leading whitespace */
-		while (*ptr && isspace((unsigned char) *ptr))
-			ptr++;
-		if (*ptr++ != '(')
-			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
-					 errmsg("malformed record literal: \"%s\"", str),
-					 errdetail("Missing left parenthesis."),
-					 errhint("To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\".")));
-	}
 
-	return InputFunctionCall(&arg->typfunc,
-							 str,
-							 arg->typioparam,
-							 typmod);
+/*
+ * Convert to a domain type.
+ */
+static Datum
+PLyObject_ToDomain(PLyObToDatum *arg, PyObject *plrv,
+				   bool *isnull, bool inarray)
+{
+	Datum		result;
+	PLyObToDatum *base = arg->u.domain.base;
+
+	result = base->func(base, plrv, isnull, inarray);
+	domain_check(result, *isnull, arg->typoid,
+				 &arg->u.domain.domain_info, arg->mcxt);
+	return result;
 }
 
 
+/*
+ * Convert using a to-SQL transform function.
+ */
 static Datum
-PLyObject_ToTransform(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
+PLyObject_ToTransform(PLyObToDatum *arg, PyObject *plrv,
+					  bool *isnull, bool inarray)
 {
-	return FunctionCall1(&arg->typtransform, PointerGetDatum(plrv));
+	if (plrv == Py_None)
+	{
+		*isnull = true;
+		return (Datum) 0;
+	}
+	*isnull = false;
+	return FunctionCall1(&arg->u.transform.typtransform, PointerGetDatum(plrv));
 }
 
 
+/*
+ * Convert Python sequence to SQL array.
+ */
 static Datum
-PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarray)
+PLySequence_ToArray(PLyObToDatum *arg, PyObject *plrv,
+					bool *isnull, bool inarray)
 {
 	ArrayType  *array;
 	int			i;
@@ -979,11 +1151,15 @@ PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarra
 	int			dims[MAXDIM];
 	int			lbs[MAXDIM];
 	int			currelem;
-	Datum		rv;
 	PyObject   *pyptr = plrv;
 	PyObject   *next;
 
-	Assert(plrv != Py_None);
+	if (plrv == Py_None)
+	{
+		*isnull = true;
+		return (Datum) 0;
+	}
+	*isnull = false;
 
 	/*
 	 * Determine the number of dimensions, and their sizes.
@@ -1049,7 +1225,7 @@ PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarra
 	elems = palloc(sizeof(Datum) * len);
 	nulls = palloc(sizeof(bool) * len);
 	currelem = 0;
-	PLySequence_ToArray_recurse(arg->elm, plrv,
+	PLySequence_ToArray_recurse(arg->u.array.elm, plrv,
 								dims, ndim, 0,
 								elems, nulls, &currelem);
 
@@ -1061,19 +1237,12 @@ PLySequence_ToArray(PLyObToDatum *arg, int32 typmod, PyObject *plrv, bool inarra
 							   ndim,
 							   dims,
 							   lbs,
-							   get_base_element_type(arg->typoid),
-							   arg->elm->typlen,
-							   arg->elm->typbyval,
-							   arg->elm->typalign);
+							   arg->u.array.elmbasetype,
+							   arg->u.array.elm->typlen,
+							   arg->u.array.elm->typbyval,
+							   arg->u.array.elm->typalign);
 
-	/*
-	 * If the result type is a domain of array, the resulting array must be
-	 * checked.
-	 */
-	rv = PointerGetDatum(array);
-	if (get_typtype(arg->typoid) == TYPTYPE_DOMAIN)
-		domain_check(rv, false, arg->typoid, &arg->typfunc.fn_extra, arg->typfunc.fn_mcxt);
-	return rv;
+	return PointerGetDatum(array);
 }
 
 /*
@@ -1110,16 +1279,7 @@ PLySequence_ToArray_recurse(PLyObToDatum *elm, PyObject *list,
 		{
 			PyObject   *obj = PySequence_GetItem(list, i);
 
-			if (obj == Py_None)
-			{
-				nulls[*currelem] = true;
-				elems[*currelem] = (Datum) 0;
-			}
-			else
-			{
-				nulls[*currelem] = false;
-				elems[*currelem] = elm->func(elm, -1, obj, true);
-			}
+			elems[*currelem] = elm->func(elm, obj, &nulls[*currelem], true);
 			Py_XDECREF(obj);
 			(*currelem)++;
 		}
@@ -1127,42 +1287,72 @@ PLySequence_ToArray_recurse(PLyObToDatum *elm, PyObject *list,
 }
 
 
+/*
+ * Convert a Python string to composite, using record_in.
+ */
 static Datum
-PLyString_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *string, bool inarray)
+PLyString_ToComposite(PLyObToDatum *arg, PyObject *string, bool inarray)
 {
-	Datum		result;
-	HeapTuple	typeTup;
-	PLyTypeInfo locinfo;
-	PLyExecutionContext *exec_ctx = PLy_current_execution_context();
-	MemoryContext cxt;
-
-	/* Create a dummy PLyTypeInfo */
-	cxt = AllocSetContextCreate(CurrentMemoryContext,
-								"PL/Python temp context",
-								ALLOCSET_DEFAULT_SIZES);
-	MemSet(&locinfo, 0, sizeof(PLyTypeInfo));
-	PLy_typeinfo_init(&locinfo, cxt);
-
-	typeTup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(desc->tdtypeid));
-	if (!HeapTupleIsValid(typeTup))
-		elog(ERROR, "cache lookup failed for type %u", desc->tdtypeid);
+	char	   *str;
 
-	PLy_output_datum_func2(&locinfo.out.d, locinfo.mcxt, typeTup,
-						   exec_ctx->curr_proc->langid,
-						   exec_ctx->curr_proc->trftypes);
+	/*
+	 * Set up call data for record_in, if we didn't already.  (We can't just
+	 * use DirectFunctionCall, because record_in needs a fn_extra field.)
+	 */
+	if (!OidIsValid(arg->u.tuple.recinfunc.fn_oid))
+		fmgr_info_cxt(F_RECORD_IN, &arg->u.tuple.recinfunc, arg->mcxt);
 
-	ReleaseSysCache(typeTup);
+	str = PLyObject_AsString(string);
 
-	result = PLyObject_ToDatum(&locinfo.out.d, desc->tdtypmod, string, inarray);
+	/*
+	 * If we are parsing a composite type within an array, and the string
+	 * isn't a valid record literal, there's a high chance that the function
+	 * did something like:
+	 *
+	 * CREATE FUNCTION .. RETURNS comptype[] AS $$ return [['foo', 'bar']] $$
+	 * LANGUAGE plpython;
+	 *
+	 * Before PostgreSQL 10, that was interpreted as a single-dimensional
+	 * array, containing record ('foo', 'bar'). PostgreSQL 10 added support
+	 * for multi-dimensional arrays, and it is now interpreted as a
+	 * two-dimensional array, containing two records, 'foo', and 'bar'.
+	 * record_in() will throw an error, because "foo" is not a valid record
+	 * literal.
+	 *
+	 * To make that less confusing to users who are upgrading from older
+	 * versions, try to give a hint in the typical instances of that. If we
+	 * are parsing an array of composite types, and we see a string literal
+	 * that is not a valid record literal, give a hint. We only want to give
+	 * the hint in the narrow case of a malformed string literal, not any
+	 * error from record_in(), so check for that case here specifically.
+	 *
+	 * This check better match the one in record_in(), so that we don't forbid
+	 * literals that are actually valid!
+	 */
+	if (inarray)
+	{
+		char	   *ptr = str;
 
-	MemoryContextDelete(cxt);
+		/* Allow leading whitespace */
+		while (*ptr && isspace((unsigned char) *ptr))
+			ptr++;
+		if (*ptr++ != '(')
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+					 errmsg("malformed record literal: \"%s\"", str),
+					 errdetail("Missing left parenthesis."),
+					 errhint("To return a composite type in an array, return the composite type as a Python tuple, e.g., \"[('foo',)]\".")));
+	}
 
-	return result;
+	return InputFunctionCall(&arg->u.tuple.recinfunc,
+							 str,
+							 arg->typoid,
+							 arg->typmod);
 }
 
 
 static Datum
-PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping)
+PLyMapping_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *mapping)
 {
 	Datum		result;
 	HeapTuple	tuple;
@@ -1172,10 +1362,6 @@ PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping)
 
 	Assert(PyMapping_Check(mapping));
 
-	if (info->is_rowtype == 2)
-		PLy_output_tuple_funcs(info, desc);
-	Assert(info->is_rowtype == 1);
-
 	/* Build tuple */
 	values = palloc(sizeof(Datum) * desc->natts);
 	nulls = palloc(sizeof(bool) * desc->natts);
@@ -1195,27 +1381,19 @@ PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping)
 
 		key = NameStr(attr->attname);
 		value = NULL;
-		att = &info->out.r.atts[i];
+		att = &arg->u.tuple.atts[i];
 		PG_TRY();
 		{
 			value = PyMapping_GetItemString(mapping, key);
-			if (value == Py_None)
-			{
-				values[i] = (Datum) NULL;
-				nulls[i] = true;
-			}
-			else if (value)
-			{
-				values[i] = (att->func) (att, -1, value, false);
-				nulls[i] = false;
-			}
-			else
+			if (!value)
 				ereport(ERROR,
 						(errcode(ERRCODE_UNDEFINED_COLUMN),
 						 errmsg("key \"%s\" not found in mapping", key),
 						 errhint("To return null in a column, "
 								 "add the value None to the mapping with the key named after the column.")));
 
+			values[i] = att->func(att, value, &nulls[i], false);
+
 			Py_XDECREF(value);
 			value = NULL;
 		}
@@ -1239,7 +1417,7 @@ PLyMapping_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *mapping)
 
 
 static Datum
-PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence)
+PLySequence_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *sequence)
 {
 	Datum		result;
 	HeapTuple	tuple;
@@ -1266,10 +1444,6 @@ PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence)
 				(errcode(ERRCODE_DATATYPE_MISMATCH),
 				 errmsg("length of returned sequence did not match number of columns in row")));
 
-	if (info->is_rowtype == 2)
-		PLy_output_tuple_funcs(info, desc);
-	Assert(info->is_rowtype == 1);
-
 	/* Build tuple */
 	values = palloc(sizeof(Datum) * desc->natts);
 	nulls = palloc(sizeof(bool) * desc->natts);
@@ -1287,21 +1461,13 @@ PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence)
 		}
 
 		value = NULL;
-		att = &info->out.r.atts[i];
+		att = &arg->u.tuple.atts[i];
 		PG_TRY();
 		{
 			value = PySequence_GetItem(sequence, idx);
 			Assert(value);
-			if (value == Py_None)
-			{
-				values[i] = (Datum) NULL;
-				nulls[i] = true;
-			}
-			else if (value)
-			{
-				values[i] = (att->func) (att, -1, value, false);
-				nulls[i] = false;
-			}
+
+			values[i] = att->func(att, value, &nulls[i], false);
 
 			Py_XDECREF(value);
 			value = NULL;
@@ -1328,7 +1494,7 @@ PLySequence_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *sequence)
 
 
 static Datum
-PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object, bool inarray)
+PLyGenericObject_ToComposite(PLyObToDatum *arg, TupleDesc desc, PyObject *object, bool inarray)
 {
 	Datum		result;
 	HeapTuple	tuple;
@@ -1336,10 +1502,6 @@ PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object
 	bool	   *nulls;
 	volatile int i;
 
-	if (info->is_rowtype == 2)
-		PLy_output_tuple_funcs(info, desc);
-	Assert(info->is_rowtype == 1);
-
 	/* Build tuple */
 	values = palloc(sizeof(Datum) * desc->natts);
 	nulls = palloc(sizeof(bool) * desc->natts);
@@ -1359,21 +1521,11 @@ PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object
 
 		key = NameStr(attr->attname);
 		value = NULL;
-		att = &info->out.r.atts[i];
+		att = &arg->u.tuple.atts[i];
 		PG_TRY();
 		{
 			value = PyObject_GetAttrString(object, key);
-			if (value == Py_None)
-			{
-				values[i] = (Datum) NULL;
-				nulls[i] = true;
-			}
-			else if (value)
-			{
-				values[i] = (att->func) (att, -1, value, false);
-				nulls[i] = false;
-			}
-			else
+			if (!value)
 			{
 				/*
 				 * No attribute for this column in the object.
@@ -1384,7 +1536,7 @@ PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object
 				 * array, with a composite type (123, 'foo') in it. But now
 				 * it's interpreted as a two-dimensional array, and we try to
 				 * interpret "123" as the composite type. See also similar
-				 * heuristic in PLyObject_ToDatum().
+				 * heuristic in PLyObject_ToScalar().
 				 */
 				ereport(ERROR,
 						(errcode(ERRCODE_UNDEFINED_COLUMN),
@@ -1394,6 +1546,8 @@ PLyGenericObject_ToComposite(PLyTypeInfo *info, TupleDesc desc, PyObject *object
 						 errhint("To return null in a column, let the returned object have an attribute named after column with value None.")));
 			}
 
+			values[i] = att->func(att, value, &nulls[i], false);
+
 			Py_XDECREF(value);
 			value = NULL;
 		}
diff --git a/src/pl/plpython/plpy_typeio.h b/src/pl/plpython/plpy_typeio.h
index 95f84d8341818ad9ba20fa56384f1c680600a3e7..91870c91b02b04ca671491e562a328118f8885e5 100644
--- a/src/pl/plpython/plpy_typeio.h
+++ b/src/pl/plpython/plpy_typeio.h
@@ -6,117 +6,169 @@
 #define PLPY_TYPEIO_H
 
 #include "access/htup.h"
-#include "access/tupdesc.h"
 #include "fmgr.h"
-#include "storage/itemptr.h"
+#include "utils/typcache.h"
+
+struct PLyProcedure;			/* avoid requiring plpy_procedure.h here */
+
 
 /*
- * Conversion from PostgreSQL Datum to a Python object.
+ * "Input" conversion from PostgreSQL Datum to a Python object.
+ *
+ * arg is the previously-set-up conversion data, val is the value to convert.
+ * val mustn't be NULL.
+ *
+ * Note: the conversion data structs should be regarded as private to
+ * plpy_typeio.c.  We declare them here only so that other modules can
+ * define structs containing them.
  */
-struct PLyDatumToOb;
-typedef PyObject *(*PLyDatumToObFunc) (struct PLyDatumToOb *arg, Datum val);
+typedef struct PLyDatumToOb PLyDatumToOb;	/* forward reference */
 
-typedef struct PLyDatumToOb
+typedef PyObject *(*PLyDatumToObFunc) (PLyDatumToOb *arg, Datum val);
+
+typedef struct PLyScalarToOb
 {
-	PLyDatumToObFunc func;
-	FmgrInfo	typfunc;		/* The type's output function */
-	FmgrInfo	typtransform;	/* from-SQL transform */
-	Oid			typoid;			/* The OID of the type */
-	int32		typmod;			/* The typmod of the type */
-	Oid			typioparam;
-	bool		typbyval;
-	int16		typlen;
-	char		typalign;
-	struct PLyDatumToOb *elm;
-} PLyDatumToOb;
+	FmgrInfo	typfunc;		/* lookup info for type's output function */
+} PLyScalarToOb;
+
+typedef struct PLyArrayToOb
+{
+	PLyDatumToOb *elm;			/* conversion info for array's element type */
+} PLyArrayToOb;
 
 typedef struct PLyTupleToOb
 {
-	PLyDatumToOb *atts;
-	int			natts;
+	/* If we're dealing with a RECORD type, actual descriptor is here: */
+	TupleDesc	recdesc;
+	/* If we're dealing with a named composite type, these fields are set: */
+	TypeCacheEntry *typentry;	/* typcache entry for type */
+	int64		tupdescseq;		/* last tupdesc seqno seen in typcache */
+	/* These fields are NULL/0 if not yet set: */
+	PLyDatumToOb *atts;			/* array of per-column conversion info */
+	int			natts;			/* length of array */
 } PLyTupleToOb;
 
-typedef union PLyTypeInput
+typedef struct PLyTransformToOb
+{
+	FmgrInfo	typtransform;	/* lookup info for from-SQL transform func */
+} PLyTransformToOb;
+
+struct PLyDatumToOb
 {
-	PLyDatumToOb d;
-	PLyTupleToOb r;
-} PLyTypeInput;
+	PLyDatumToObFunc func;		/* conversion control function */
+	Oid			typoid;			/* OID of the source type */
+	int32		typmod;			/* typmod of the source type */
+	bool		typbyval;		/* its physical representation details */
+	int16		typlen;
+	char		typalign;
+	MemoryContext mcxt;			/* context this info is stored in */
+	union						/* conversion-type-specific data */
+	{
+		PLyScalarToOb scalar;
+		PLyArrayToOb array;
+		PLyTupleToOb tuple;
+		PLyTransformToOb transform;
+	}			u;
+};
 
 /*
- * Conversion from Python object to a PostgreSQL Datum.
+ * "Output" conversion from Python object to a PostgreSQL Datum.
+ *
+ * arg is the previously-set-up conversion data, val is the value to convert.
  *
- * The 'inarray' argument to the conversion function is true, if the
- * converted value was in an array (Python list). It is used to give a
- * better error message in some cases.
+ * *isnull is set to true if val is Py_None, false otherwise.
+ * (The conversion function *must* be called even for Py_None,
+ * so that domain constraints can be checked.)
+ *
+ * inarray is true if the converted value was in an array (Python list).
+ * It is used to give a better error message in some cases.
  */
-struct PLyObToDatum;
-typedef Datum (*PLyObToDatumFunc) (struct PLyObToDatum *arg, int32 typmod, PyObject *val, bool inarray);
+typedef struct PLyObToDatum PLyObToDatum;	/* forward reference */
+
+typedef Datum (*PLyObToDatumFunc) (PLyObToDatum *arg, PyObject *val,
+								   bool *isnull,
+								   bool inarray);
 
-typedef struct PLyObToDatum
+typedef struct PLyObToScalar
 {
-	PLyObToDatumFunc func;
-	FmgrInfo	typfunc;		/* The type's input function */
-	FmgrInfo	typtransform;	/* to-SQL transform */
-	Oid			typoid;			/* The OID of the type */
-	int32		typmod;			/* The typmod of the type */
-	Oid			typioparam;
-	bool		typbyval;
-	int16		typlen;
-	char		typalign;
-	struct PLyObToDatum *elm;
-} PLyObToDatum;
+	FmgrInfo	typfunc;		/* lookup info for type's input function */
+	Oid			typioparam;		/* argument to pass to it */
+} PLyObToScalar;
+
+typedef struct PLyObToArray
+{
+	PLyObToDatum *elm;			/* conversion info for array's element type */
+	Oid			elmbasetype;	/* element base type */
+} PLyObToArray;
 
 typedef struct PLyObToTuple
 {
-	PLyObToDatum *atts;
-	int			natts;
+	/* If we're dealing with a RECORD type, actual descriptor is here: */
+	TupleDesc	recdesc;
+	/* If we're dealing with a named composite type, these fields are set: */
+	TypeCacheEntry *typentry;	/* typcache entry for type */
+	int64		tupdescseq;		/* last tupdesc seqno seen in typcache */
+	/* These fields are NULL/0 if not yet set: */
+	PLyObToDatum *atts;			/* array of per-column conversion info */
+	int			natts;			/* length of array */
+	/* We might need to convert using record_in(); if so, cache info here */
+	FmgrInfo	recinfunc;		/* lookup info for record_in */
 } PLyObToTuple;
 
-typedef union PLyTypeOutput
+typedef struct PLyObToDomain
 {
-	PLyObToDatum d;
-	PLyObToTuple r;
-} PLyTypeOutput;
+	PLyObToDatum *base;			/* conversion info for domain's base type */
+	void	   *domain_info;	/* cache space for domain_check() */
+} PLyObToDomain;
 
-/* all we need to move PostgreSQL data to Python objects,
- * and vice versa
- */
-typedef struct PLyTypeInfo
+typedef struct PLyObToTransform
 {
-	PLyTypeInput in;
-	PLyTypeOutput out;
-
-	/*
-	 * is_rowtype can be: -1 = not known yet (initial state); 0 = scalar
-	 * datatype; 1 = rowtype; 2 = rowtype, but I/O functions not set up yet
-	 */
-	int			is_rowtype;
-	/* used to check if the type has been modified */
-	Oid			typ_relid;
-	TransactionId typrel_xmin;
-	ItemPointerData typrel_tid;
-
-	/* context for subsidiary data (doesn't belong to this struct though) */
-	MemoryContext mcxt;
-} PLyTypeInfo;
-
-extern void PLy_typeinfo_init(PLyTypeInfo *arg, MemoryContext mcxt);
+	FmgrInfo	typtransform;	/* lookup info for to-SQL transform function */
+} PLyObToTransform;
 
-extern void PLy_input_datum_func(PLyTypeInfo *arg, Oid typeOid, HeapTuple typeTup, Oid langid, List *trftypes);
-extern void PLy_output_datum_func(PLyTypeInfo *arg, HeapTuple typeTup, Oid langid, List *trftypes);
-
-extern void PLy_input_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc);
-extern void PLy_output_tuple_funcs(PLyTypeInfo *arg, TupleDesc desc);
-
-extern void PLy_output_record_funcs(PLyTypeInfo *arg, TupleDesc desc);
-
-/* conversion from Python objects to composite Datums */
-extern Datum PLyObject_ToCompositeDatum(PLyTypeInfo *info, TupleDesc desc, PyObject *plrv, bool isarray);
-
-/* conversion from heap tuples to Python dictionaries */
-extern PyObject *PLyDict_FromTuple(PLyTypeInfo *info, HeapTuple tuple, TupleDesc desc);
-
-/* conversion from Python objects to C strings */
+struct PLyObToDatum
+{
+	PLyObToDatumFunc func;		/* conversion control function */
+	Oid			typoid;			/* OID of the target type */
+	int32		typmod;			/* typmod of the target type */
+	bool		typbyval;		/* its physical representation details */
+	int16		typlen;
+	char		typalign;
+	MemoryContext mcxt;			/* context this info is stored in */
+	union						/* conversion-type-specific data */
+	{
+		PLyObToScalar scalar;
+		PLyObToArray array;
+		PLyObToTuple tuple;
+		PLyObToDomain domain;
+		PLyObToTransform transform;
+	}			u;
+};
+
+
+extern PyObject *PLy_input_convert(PLyDatumToOb *arg, Datum val);
+extern Datum PLy_output_convert(PLyObToDatum *arg, PyObject *val,
+				   bool *isnull);
+
+extern PyObject *PLy_input_from_tuple(PLyDatumToOb *arg, HeapTuple tuple,
+					 TupleDesc desc);
+
+extern void PLy_input_setup_func(PLyDatumToOb *arg, MemoryContext arg_mcxt,
+					 Oid typeOid, int32 typmod,
+					 struct PLyProcedure *proc);
+extern void PLy_output_setup_func(PLyObToDatum *arg, MemoryContext arg_mcxt,
+					  Oid typeOid, int32 typmod,
+					  struct PLyProcedure *proc);
+
+extern void PLy_input_setup_tuple(PLyDatumToOb *arg, TupleDesc desc,
+					  struct PLyProcedure *proc);
+extern void PLy_output_setup_tuple(PLyObToDatum *arg, TupleDesc desc,
+					   struct PLyProcedure *proc);
+
+extern void PLy_output_setup_record(PLyObToDatum *arg, TupleDesc desc,
+						struct PLyProcedure *proc);
+
+/* conversion from Python objects to C strings --- exported for transforms */
 extern char *PLyObject_AsString(PyObject *plrv);
 
 #endif							/* PLPY_TYPEIO_H */
diff --git a/src/pl/plpython/sql/plpython_types.sql b/src/pl/plpython/sql/plpython_types.sql
index 8c57297c24e871758100820f47c78544c03bd970..cc0524ee806bddcc94d031d6c39ac7a132728060 100644
--- a/src/pl/plpython/sql/plpython_types.sql
+++ b/src/pl/plpython/sql/plpython_types.sql
@@ -387,6 +387,55 @@ $$ LANGUAGE plpythonu;
 SELECT * FROM test_type_conversion_array_domain_check_violation();
 
 
+--
+-- Arrays of domains
+--
+
+CREATE FUNCTION test_read_uint2_array(x uint2[]) RETURNS uint2 AS $$
+plpy.info(x, type(x))
+return x[0]
+$$ LANGUAGE plpythonu;
+
+select test_read_uint2_array(array[1::uint2]);
+
+CREATE FUNCTION test_build_uint2_array(x int2) RETURNS uint2[] AS $$
+return [x, x]
+$$ LANGUAGE plpythonu;
+
+select test_build_uint2_array(1::int2);
+select test_build_uint2_array(-1::int2);  -- fail
+
+--
+-- ideally this would work, but for now it doesn't, because the return value
+-- is [[2,4], [2,4]] which our conversion code thinks should become a 2-D
+-- integer array, not an array of arrays.
+--
+CREATE FUNCTION test_type_conversion_domain_array(x integer[])
+  RETURNS ordered_pair_domain[] AS $$
+return [x, x]
+$$ LANGUAGE plpythonu;
+
+select test_type_conversion_domain_array(array[2,4]);
+select test_type_conversion_domain_array(array[4,2]);  -- fail
+
+CREATE FUNCTION test_type_conversion_domain_array2(x ordered_pair_domain)
+  RETURNS integer AS $$
+plpy.info(x, type(x))
+return x[1]
+$$ LANGUAGE plpythonu;
+
+select test_type_conversion_domain_array2(array[2,4]);
+select test_type_conversion_domain_array2(array[4,2]);  -- fail
+
+CREATE FUNCTION test_type_conversion_array_domain_array(x ordered_pair_domain[])
+  RETURNS ordered_pair_domain AS $$
+plpy.info(x, type(x))
+return x[0]
+$$ LANGUAGE plpythonu;
+
+select test_type_conversion_array_domain_array(array[array[2,4]::ordered_pair_domain]);
+
+
 ---
 --- Composite types
 ---
@@ -430,6 +479,48 @@ ALTER TYPE named_pair RENAME TO named_pair_2;
 SELECT test_composite_type_input(row(1, 2));
 
 
+--
+-- Domains within composite
+--
+
+CREATE TYPE nnint_container AS (f1 int, f2 nnint);
+
+CREATE FUNCTION nnint_test(x int, y int) RETURNS nnint_container AS $$
+return {'f1': x, 'f2': y}
+$$ LANGUAGE plpythonu;
+
+SELECT nnint_test(null, 3);
+SELECT nnint_test(3, null);  -- fail
+
+
+--
+-- Domains of composite
+--
+
+CREATE DOMAIN ordered_named_pair AS named_pair_2 CHECK((VALUE).i <= (VALUE).j);
+
+CREATE FUNCTION read_ordered_named_pair(p ordered_named_pair) RETURNS integer AS $$
+return p['i'] + p['j']
+$$ LANGUAGE plpythonu;
+
+SELECT read_ordered_named_pair(row(1, 2));
+SELECT read_ordered_named_pair(row(2, 1));  -- fail
+
+CREATE FUNCTION build_ordered_named_pair(i int, j int) RETURNS ordered_named_pair AS $$
+return {'i': i, 'j': j}
+$$ LANGUAGE plpythonu;
+
+SELECT build_ordered_named_pair(1,2);
+SELECT build_ordered_named_pair(2,1);  -- fail
+
+CREATE FUNCTION build_ordered_named_pairs(i int, j int) RETURNS ordered_named_pair[] AS $$
+return [{'i': i, 'j': j}, {'i': i, 'j': j+1}]
+$$ LANGUAGE plpythonu;
+
+SELECT build_ordered_named_pairs(1,2);
+SELECT build_ordered_named_pairs(2,1);  -- fail
+
+
 --
 -- Prepared statements
 --