patch 9.1.1239: if_python: no tuple data type support

Problem:  if_python: no tuple data type support (after v9.1.1232)
Solution: Add support for using Vim tuple in the python interface
          (Yegappan Lakshmanan)

closes: #16964

Signed-off-by: Yegappan Lakshmanan <yegappan@yahoo.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/eval.c b/src/eval.c
index bd8e7cf..c2856fe 100644
--- a/src/eval.c
+++ b/src/eval.c
@@ -2050,7 +2050,7 @@
 	var2.v_type = VAR_UNKNOWN;
     }
 
-    if (lp->ll_tuple != NULL)
+    if (lp->ll_tuple != NULL && (flags & GLV_READ_ONLY) == 0)
     {
 	if (!quiet)
 	    emsg(_(e_tuple_is_immutable));
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 21ed15e..125ba55 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -8632,6 +8632,9 @@
 	    else if (lv.ll_list != NULL)
 		// List item.
 		rettv->vval.v_number = tv_islocked(&lv.ll_li->li_tv);
+	    else if (lv.ll_tuple != NULL)
+		// Tuple item.
+		rettv->vval.v_number = tv_islocked(lv.ll_tv);
 	    else
 		// Dictionary item.
 		rettv->vval.v_number = tv_islocked(&lv.ll_di->di_tv);
diff --git a/src/ex_docmd.c b/src/ex_docmd.c
index f341fd4..03eaef4 100644
--- a/src/ex_docmd.c
+++ b/src/ex_docmd.c
@@ -289,7 +289,6 @@
 # define ex_endif		ex_ni
 # define ex_endtry		ex_ni
 # define ex_endwhile		ex_ni
-# define ex_enum		ex_ni
 # define ex_eval		ex_ni
 # define ex_execute		ex_ni
 # define ex_finally		ex_ni
diff --git a/src/gc.c b/src/gc.c
index b95b2ca..c8aa5fa 100644
--- a/src/gc.c
+++ b/src/gc.c
@@ -439,6 +439,21 @@
 }
 
 /*
+ * Mark a tuple and its items with "copyID".
+ * Returns TRUE if setting references failed somehow.
+ */
+    int
+set_ref_in_tuple(tuple_T *tuple, int copyID)
+{
+    if (tuple != NULL && tuple->tv_copyID != copyID)
+    {
+	tuple->tv_copyID = copyID;
+	return set_ref_in_tuple_items(tuple, copyID, NULL, NULL);
+    }
+    return FALSE;
+}
+
+/*
  * Mark all lists and dicts referenced through tuple "t" with "copyID".
  * "ht_stack" is used to add hashtabs to be marked.  Can be NULL.
  *
diff --git a/src/if_py_both.h b/src/if_py_both.h
index 9f2f582..641c159 100644
--- a/src/if_py_both.h
+++ b/src/if_py_both.h
@@ -152,14 +152,15 @@
 
 # define PyList_GET_ITEM(list, i) PyList_GetItem(list, i)
 # define PyList_GET_SIZE(o) PyList_Size(o)
-# define PyTuple_GET_ITEM(o, pos) PyTuple_GetItem(o, pos)
-# define PyTuple_GET_SIZE(o) PyTuple_Size(o)
 
 // PyList_SET_ITEM and PyList_SetItem have slightly different behaviors. The
 // former will leave the old item dangling, and the latter will decref on it.
 // Since we only use this on new lists, this difference doesn't matter.
 # define PyList_SET_ITEM(list, i, item) PyList_SetItem(list, i, item)
 
+# define PyTuple_GET_ITEM(o, pos) PyTuple_GetItem(o, pos)
+# define PyTuple_GET_SIZE(o) PyTuple_Size(o)
+
 # if Py_LIMITED_API < 0x03080000
 // PyIter_check only became part of stable ABI in 3.8, and there is no easy way
 // to check for it in the API. We simply return false as a compromise. This
@@ -1014,11 +1015,14 @@
     // Check if we run into a recursive loop.  The item must be in lookup_dict
     // then and we can use it again.
     if ((our_tv->v_type == VAR_LIST && our_tv->vval.v_list != NULL)
+	    || (our_tv->v_type == VAR_TUPLE && our_tv->vval.v_tuple != NULL)
 	    || (our_tv->v_type == VAR_DICT && our_tv->vval.v_dict != NULL))
     {
 	sprintf(ptrBuf, "%p",
 		our_tv->v_type == VAR_LIST ? (void *)our_tv->vval.v_list
-					   : (void *)our_tv->vval.v_dict);
+		    : our_tv->v_type == VAR_TUPLE ?
+			(void *)our_tv->vval.v_tuple
+			: (void *)our_tv->vval.v_dict);
 
 	if ((ret = PyDict_GetItemString(lookup_dict, ptrBuf)))
 	{
@@ -1079,6 +1083,39 @@
 	    Py_DECREF(newObj);
 	}
     }
+    else if (our_tv->v_type == VAR_TUPLE)
+    {
+	tuple_T	*tuple = our_tv->vval.v_tuple;
+	int	len;
+
+	if (tuple == NULL)
+	    return NULL;
+
+	len = TUPLE_LEN(tuple);
+
+	ret = PyTuple_New(len);
+	if (ret == NULL)
+	    return NULL;
+
+	if (PyDict_SetItemString(lookup_dict, ptrBuf, ret))
+	{
+	    Py_DECREF(ret);
+	    return NULL;
+	}
+
+	for (int idx = 0; idx < len; idx++)
+	{
+	    typval_T	*item_tv = TUPLE_ITEM(tuple, idx);
+
+	    newObj = VimToPython(item_tv, depth + 1, lookup_dict);
+	    if (!newObj)
+	    {
+		Py_DECREF(ret);
+		return NULL;
+	    }
+	    PyTuple_SET_ITEM(ret, idx, newObj);
+	}
+    }
     else if (our_tv->v_type == VAR_DICT)
     {
 
@@ -1800,6 +1837,7 @@
 
 static pylinkedlist_T *lastdict = NULL;
 static pylinkedlist_T *lastlist = NULL;
+static pylinkedlist_T *lasttuple = NULL;
 static pylinkedlist_T *lastfunc = NULL;
 
     static void
@@ -3216,6 +3254,355 @@
     { NULL,	NULL,				0,		NULL}
 };
 
+DEFINE_PY_TYPE_OBJECT(TupleType);
+
+typedef struct
+{
+    PyObject_HEAD
+    tuple_T	*tuple;
+    pylinkedlist_T	ref;
+} TupleObject;
+
+#define NEW_TUPLE(tuple) TupleNew(TupleTypePtr, tuple)
+
+    static PyObject *
+TupleNew(PyTypeObject *subtype, tuple_T *tuple)
+{
+    TupleObject	*self;
+
+    if (tuple == NULL)
+	return NULL;
+
+    self = (TupleObject *) Py_TYPE_GET_TP_ALLOC(subtype)(subtype, 0);
+    if (self == NULL)
+	return NULL;
+    self->tuple = tuple;
+    ++tuple->tv_refcount;
+
+    pyll_add((PyObject *)(self), &self->ref, &lasttuple);
+
+    return (PyObject *)(self);
+}
+
+    static tuple_T *
+py_tuple_alloc(void)
+{
+    tuple_T	*ret;
+
+    if (!(ret = tuple_alloc()))
+    {
+	PyErr_NoMemory();
+	return NULL;
+    }
+    ++ret->tv_refcount;
+
+    return ret;
+}
+
+    static int
+tuple_py_concat(tuple_T *t, PyObject *obj, PyObject *lookup_dict)
+{
+    PyObject	*iterator;
+    PyObject	*item;
+
+    if (!(iterator = PyObject_GetIter(obj)))
+	return -1;
+
+    while ((item = PyIter_Next(iterator)))
+    {
+	typval_T    new_tv;
+
+	if (_ConvertFromPyObject(item, &new_tv, lookup_dict) == -1)
+	{
+	    Py_DECREF(item);
+	    Py_DECREF(iterator);
+	    return -1;
+	}
+
+	Py_DECREF(item);
+
+	if (tuple_append_tv(t, &new_tv) == FAIL)
+	{
+	    Py_DECREF(iterator);
+	    return -1;
+	}
+    }
+
+    Py_DECREF(iterator);
+
+    // Iterator may have finished due to an exception
+    if (PyErr_Occurred())
+	return -1;
+
+    return 0;
+}
+
+    static PyObject *
+TupleConstructor(PyTypeObject *subtype, PyObject *args, PyObject *kwargs)
+{
+    tuple_T	*tuple;
+    PyObject	*obj = NULL;
+
+    if (kwargs)
+    {
+	PyErr_SET_STRING(PyExc_TypeError,
+		N_("tuple constructor does not accept keyword arguments"));
+	return NULL;
+    }
+
+    if (!PyArg_ParseTuple(args, "|O", &obj))
+	return NULL;
+
+    if (!(tuple = py_tuple_alloc()))
+	return NULL;
+
+    if (obj)
+    {
+	PyObject	*lookup_dict;
+
+	if (!(lookup_dict = PyDict_New()))
+	{
+	    tuple_unref(tuple);
+	    return NULL;
+	}
+
+	if (tuple_py_concat(tuple, obj, lookup_dict) == -1)
+	{
+	    Py_DECREF(lookup_dict);
+	    tuple_unref(tuple);
+	    return NULL;
+	}
+
+	Py_DECREF(lookup_dict);
+    }
+
+    return TupleNew(subtype, tuple);
+}
+
+    static void
+TupleDestructor(PyObject *self_obj)
+{
+    TupleObject *self = (TupleObject *)self_obj;
+    pyll_remove(&self->ref, &lasttuple);
+    tuple_unref(self->tuple);
+
+    DESTRUCTOR_FINISH(self);
+}
+
+    static PyInt
+TupleLength(TupleObject *self)
+{
+    return ((PyInt)(tuple_len(self->tuple)));
+}
+
+    static PyObject *
+TupleIndex(TupleObject *self, Py_ssize_t index)
+{
+    PyInt	len = TupleLength(self);
+
+    if (index < 0)
+	index = len + index;
+
+    if (index < 0 || index >= len)
+    {
+	PyErr_SET_STRING(PyExc_IndexError, N_("tuple index out of range"));
+	return NULL;
+    }
+    return ConvertToPyObject(TUPLE_ITEM(self->tuple, index));
+}
+
+/*
+ * Return a new tuple object for the tuple slice starting from the index
+ * "first" and of length "slicelen" skipping "step" items.
+ */
+    static PyObject *
+TupleSlice(
+    TupleObject		*self,
+    Py_ssize_t		first,
+    Py_ssize_t		step,
+    Py_ssize_t		slicelen)
+{
+    PyInt	i;
+    PyObject	*tuple;
+
+    tuple = PyTuple_New(slicelen);
+    if (tuple == NULL)
+	return NULL;
+
+    for (i = 0; i < slicelen; ++i)
+    {
+	PyObject	*item;
+
+	item = TupleIndex(self, first + i * step);
+	if (item == NULL)
+	{
+	    Py_DECREF(tuple);
+	    return NULL;
+	}
+
+	PyTuple_SET_ITEM(tuple, i, item);
+    }
+
+    return tuple;
+}
+
+    static PyObject *
+TupleItem(TupleObject *self, PyObject* idx)
+{
+#if PY_MAJOR_VERSION < 3
+    if (PyInt_Check(idx))
+    {
+	long _idx = PyInt_AsLong(idx);
+	return TupleIndex(self, _idx);
+    }
+    else
+#endif
+    if (PyLong_Check(idx))
+    {
+	long _idx = PyLong_AsLong(idx);
+	return TupleIndex(self, _idx);
+    }
+    else if (PySlice_Check(idx))
+    {
+	Py_ssize_t start, stop, step, slicelen;
+
+	if (PySlice_GetIndicesEx((PySliceObject_T *)idx, TupleLength(self),
+				 &start, &stop, &step, &slicelen) < 0)
+	    return NULL;
+	return TupleSlice(self, start, step, slicelen);
+    }
+    else
+    {
+	RAISE_INVALID_INDEX_TYPE(idx);
+	return NULL;
+    }
+}
+
+typedef struct
+{
+    tuple_T	*tuple;
+    int		index;
+} tupleiterinfo_T;
+
+    static void
+TupleIterDestruct(void *arg)
+{
+    tupleiterinfo_T *tii = (tupleiterinfo_T*)arg;
+    tuple_unref(tii->tuple);
+    PyMem_Free(tii);
+}
+
+    static PyObject *
+TupleIterNext(void **arg)
+{
+    PyObject	*ret;
+    tupleiterinfo_T **tii = (tupleiterinfo_T**)arg;
+
+    if ((*tii)->index >= TUPLE_LEN((*tii)->tuple))
+	return NULL;
+
+    if (!(ret = ConvertToPyObject(TUPLE_ITEM((*tii)->tuple, (*tii)->index))))
+	return NULL;
+
+    (*tii)->index++;
+
+    return ret;
+}
+
+    static PyObject *
+TupleIter(PyObject *self_obj)
+{
+    TupleObject	*self = (TupleObject*)self_obj;
+    tupleiterinfo_T	*tii;
+    tuple_T	*t = self->tuple;
+
+    if (!(tii = PyMem_New(tupleiterinfo_T, 1)))
+    {
+	PyErr_NoMemory();
+	return NULL;
+    }
+
+    tii->tuple = t;
+    tii->index = 0;
+    ++t->tv_refcount;
+
+    return IterNew(tii,
+	    TupleIterDestruct, TupleIterNext,
+	    NULL, NULL, (PyObject *)self);
+}
+
+static char *TupleAttrs[] = {
+    "locked",
+    NULL
+};
+
+    static PyObject *
+TupleDir(PyObject *self, PyObject *args UNUSED)
+{
+    return ObjectDir(self, TupleAttrs);
+}
+
+    static int
+TupleSetattr(PyObject *self_obj, char *name, PyObject *valObject)
+{
+    TupleObject *self = (TupleObject*)self_obj;
+    if (valObject == NULL)
+    {
+	PyErr_SET_STRING(PyExc_AttributeError,
+		N_("cannot delete vim.Tuple attributes"));
+	return -1;
+    }
+
+    if (strcmp(name, "locked") == 0)
+    {
+	if (self->tuple->tv_lock == VAR_FIXED)
+	{
+	    PyErr_SET_STRING(PyExc_TypeError, N_("cannot modify fixed tuple"));
+	    return -1;
+	}
+	else
+	{
+	    int		istrue = PyObject_IsTrue(valObject);
+	    if (istrue == -1)
+		return -1;
+	    else if (istrue)
+		self->tuple->tv_lock = VAR_LOCKED;
+	    else
+		self->tuple->tv_lock = 0;
+	}
+	return 0;
+    }
+    else
+    {
+	PyErr_FORMAT(PyExc_AttributeError, N_("cannot set attribute %s"), name);
+	return -1;
+    }
+}
+
+static PySequenceMethods TupleAsSeq = {
+    (lenfunc)		TupleLength,	 // sq_length,	  len(x)
+    (binaryfunc)	0,		 // RangeConcat, sq_concat,  x+y
+    0,					 // RangeRepeat, sq_repeat,  x*n
+    (PyIntArgFunc)	TupleIndex,	 // sq_item,	  x[i]
+    0,					 // was_sq_slice,     x[i:j]
+    (PyIntObjArgProc)	0,		 // sq_as_item,  x[i]=v
+    0,					 // was_sq_ass_slice, x[i:j]=v
+    0,					 // sq_contains
+    (binaryfunc)	0,		 // sq_inplace_concat
+    0,					 // sq_inplace_repeat
+};
+
+static PyMappingMethods TupleAsMapping = {
+    /* mp_length	*/ (lenfunc) TupleLength,
+    /* mp_subscript     */ (binaryfunc) TupleItem,
+    /* mp_ass_subscript */ (objobjargproc) 0,
+};
+
+static struct PyMethodDef TupleMethods[] = {
+    {"__dir__",	(PyCFunction)TupleDir,		METH_NOARGS,	""},
+    { NULL,	NULL,				0,		NULL}
+};
+
 typedef struct
 {
     PyObject_HEAD
@@ -6219,6 +6606,7 @@
 {
     pylinkedlist_T	*cur;
     list_T		*ll;
+    tuple_T		*tt;
     int			i;
     int			abort = FALSE;
     FunctionObject	*func;
@@ -6239,6 +6627,15 @@
 	}
     }
 
+    if (lasttuple != NULL)
+    {
+	for (cur = lasttuple ; !abort && cur != NULL ; cur = cur->pll_prev)
+	{
+	    tt = ((TupleObject *) (cur->pll_obj))->tuple;
+	    abort = set_ref_in_tuple(tt, copyID);
+	}
+    }
+
     if (lastfunc != NULL)
     {
 	for (cur = lastfunc ; !abort && cur != NULL ; cur = cur->pll_prev)
@@ -6461,6 +6858,27 @@
     return 0;
 }
 
+    static int
+pytuple_to_tv(PyObject *obj, typval_T *tv, PyObject *lookup_dict)
+{
+    tuple_T	*t;
+
+    if (!(t = py_tuple_alloc()))
+	return -1;
+
+    tv->v_type = VAR_TUPLE;
+    tv->vval.v_tuple = t;
+
+    if (tuple_py_concat(t, obj, lookup_dict) == -1)
+    {
+	tuple_unref(t);
+	return -1;
+    }
+
+    --t->tv_refcount;
+    return 0;
+}
+
 typedef int (*pytotvfunc)(PyObject *, typval_T *, PyObject *);
 
     static int
@@ -6504,6 +6922,8 @@
 	    ++tv->vval.v_dict->dv_refcount;
 	else if (tv->v_type == VAR_LIST)
 	    ++tv->vval.v_list->lv_refcount;
+	else if (tv->v_type == VAR_TUPLE)
+	    ++tv->vval.v_tuple->tv_refcount;
     }
     else
     {
@@ -6607,6 +7027,12 @@
 	tv->vval.v_list = (((ListObject *)(obj))->list);
 	++tv->vval.v_list->lv_refcount;
     }
+    else if (PyType_IsSubtype(obj->ob_type, TupleTypePtr))
+    {
+	tv->v_type = VAR_TUPLE;
+	tv->vval.v_tuple = (((TupleObject *)(obj))->tuple);
+	++tv->vval.v_tuple->tv_refcount;
+    }
     else if (PyType_IsSubtype(obj->ob_type, FunctionTypePtr))
     {
 	FunctionObject *func = (FunctionObject *) obj;
@@ -6680,6 +7106,8 @@
 	if (PyErr_Occurred())
 	    return -1;
     }
+    else if (PyTuple_Check(obj))
+	return convert_dl(obj, tv, pytuple_to_tv, lookup_dict);
     else if (PyDict_Check(obj))
 	return convert_dl(obj, tv, pydict_to_tv, lookup_dict);
     else if (PyFloat_Check(obj))
@@ -6742,6 +7170,8 @@
 	    return PyFloat_FromDouble((double) tv->vval.v_float);
 	case VAR_LIST:
 	    return NEW_LIST(tv->vval.v_list);
+	case VAR_TUPLE:
+	    return NEW_TUPLE(tv->vval.v_tuple);
 	case VAR_DICT:
 	    return NEW_DICTIONARY(tv->vval.v_dict);
 	case VAR_FUNC:
@@ -6777,7 +7207,6 @@
 	case VAR_CLASS:
 	case VAR_OBJECT:
 	case VAR_TYPEALIAS:
-	case VAR_TUPLE:		// FIXME: Need to add support for tuple
 	    Py_INCREF(Py_None);
 	    return Py_None;
 	case VAR_BOOL:
@@ -7002,6 +7431,27 @@
     ListType.tp_setattr = ListSetattr;
 #endif
 
+    // Tuple type
+    CLEAR_FIELD(TupleType);
+    TupleType.tp_name = "vim.tuple";
+    TupleType.tp_dealloc = TupleDestructor;
+    TupleType.tp_basicsize = sizeof(TupleObject);
+    TupleType.tp_as_sequence = &TupleAsSeq;
+    TupleType.tp_as_mapping = &TupleAsMapping;
+    TupleType.tp_flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_BASETYPE;
+    TupleType.tp_doc = "tuple pushing modifications to Vim structure";
+    TupleType.tp_methods = TupleMethods;
+    TupleType.tp_iter = TupleIter;
+    TupleType.tp_new = TupleConstructor;
+    TupleType.tp_alloc = PyType_GenericAlloc;
+#if PY_MAJOR_VERSION >= 3
+    TupleType.tp_getattro = TupleGetattro;
+    TupleType.tp_setattro = TupleSetattro;
+#else
+    TupleType.tp_getattr = TupleGetattr;
+    TupleType.tp_setattr = TupleSetattr;
+#endif
+
     CLEAR_FIELD(FunctionType);
     FunctionType.tp_name = "vim.function";
     FunctionType.tp_basicsize = sizeof(FunctionObject);
@@ -7064,6 +7514,7 @@
     PYTYPE_READY(CurrentType);
     PYTYPE_READY(DictionaryType);
     PYTYPE_READY(ListType);
+    PYTYPE_READY(TupleType);
     PYTYPE_READY(FunctionType);
     PYTYPE_READY(OptionsType);
     PYTYPE_READY(OutputType);
@@ -7101,6 +7552,7 @@
     PYTYPE_CLEANUP(CurrentType);
     PYTYPE_CLEANUP(DictionaryType);
     PYTYPE_CLEANUP(ListType);
+    PYTYPE_CLEANUP(TupleType);
     PYTYPE_CLEANUP(FunctionType);
     PYTYPE_CLEANUP(OptionsType);
     PYTYPE_CLEANUP(OutputType);
@@ -7239,6 +7691,7 @@
 	{"TabPage",    (PyObject *)TabPageTypePtr},
 	{"Dictionary", (PyObject *)DictionaryTypePtr},
 	{"List",       (PyObject *)ListTypePtr},
+	{"Tuple",      (PyObject *)TupleTypePtr},
 	{"Function",   (PyObject *)FunctionTypePtr},
 	{"Options",    (PyObject *)OptionsTypePtr},
 #if PY_VERSION_HEX < 0x030700f0
diff --git a/src/if_python.c b/src/if_python.c
index 577807c..594d7e5 100644
--- a/src/if_python.c
+++ b/src/if_python.c
@@ -201,6 +201,10 @@
 # define PyList_SetItem dll_PyList_SetItem
 # define PyList_Size dll_PyList_Size
 # define PyList_Type (*dll_PyList_Type)
+# define PyTuple_GetItem dll_PyTuple_GetItem
+# define PyTuple_New dll_PyTuple_New
+# define PyTuple_Size dll_PyTuple_Size
+# define PyTuple_Type (*dll_PyTuple_Type)
 # define PySequence_Check dll_PySequence_Check
 # define PySequence_Size dll_PySequence_Size
 # define PySequence_GetItem dll_PySequence_GetItem
@@ -352,13 +356,16 @@
 static int(*dll_PyList_SetItem)(PyObject *, PyInt, PyObject *);
 static PyInt(*dll_PyList_Size)(PyObject *);
 static PyTypeObject* dll_PyList_Type;
+static PyObject*(*dll_PyTuple_GetItem)(PyObject *, PyInt);
+static int(*dll_PyTuple_SetItem)(PyObject *, PyInt, PyObject *);
+static int(*dll_PyTuple_SET_ITEM)(PyObject *, PyInt, PyObject *);
+static PyObject*(*dll_PyTuple_New)(PyInt size);
+static PyInt(*dll_PyTuple_Size)(PyObject *);
+static PyTypeObject* dll_PyTuple_Type;
 static int (*dll_PySequence_Check)(PyObject *);
 static PyInt(*dll_PySequence_Size)(PyObject *);
 static PyObject*(*dll_PySequence_GetItem)(PyObject *, PyInt);
 static PyObject*(*dll_PySequence_Fast)(PyObject *, const char *);
-static PyInt(*dll_PyTuple_Size)(PyObject *);
-static PyObject*(*dll_PyTuple_GetItem)(PyObject *, PyInt);
-static PyTypeObject* dll_PyTuple_Type;
 static int (*dll_PySlice_GetIndicesEx)(PySliceObject *r, PyInt length,
 		     PyInt *start, PyInt *stop, PyInt *step,
 		     PyInt *slicelen);
@@ -540,6 +547,12 @@
     {"PyList_SetItem", (PYTHON_PROC*)&dll_PyList_SetItem},
     {"PyList_Size", (PYTHON_PROC*)&dll_PyList_Size},
     {"PyList_Type", (PYTHON_PROC*)&dll_PyList_Type},
+    {"PyTuple_GetItem", (PYTHON_PROC*)&dll_PyTuple_GetItem},
+    {"PyTuple_SetItem", (PYTHON_PROC*)&dll_PyTuple_SetItem},
+    {"PyTuple_SET_ITEM", (PYTHON_PROC*)&dll_PyTuple_SET_ITEM},
+    {"PyTuple_New", (PYTHON_PROC*)&dll_PyTuple_New},
+    {"PyTuple_Size", (PYTHON_PROC*)&dll_PyTuple_Size},
+    {"PyTuple_Type", (PYTHON_PROC*)&dll_PyTuple_Type},
     {"PySequence_Size", (PYTHON_PROC*)&dll_PySequence_Size},
     {"PySequence_Check", (PYTHON_PROC*)&dll_PySequence_Check},
     {"PySequence_GetItem", (PYTHON_PROC*)&dll_PySequence_GetItem},
@@ -786,6 +799,7 @@
 static PyObject *RangeGetattr(PyObject *, char *);
 static PyObject *DictionaryGetattr(PyObject *, char*);
 static PyObject *ListGetattr(PyObject *, char *);
+static PyObject *TupleGetattr(PyObject *, char *);
 static PyObject *FunctionGetattr(PyObject *, char *);
 
 #ifndef Py_VISIT
@@ -1511,6 +1525,17 @@
 }
 
     static PyObject *
+TupleGetattr(PyObject *self, char *name)
+{
+    if (strcmp(name, "locked") == 0)
+	return PyInt_FromLong(((TupleObject *)(self))->tuple->tv_lock);
+    else if (strcmp(name, "__members__") == 0)
+	return ObjectDir(NULL, TupleAttrs);
+
+    return Py_FindMethod(TupleMethods, self, name);
+}
+
+    static PyObject *
 FunctionGetattr(PyObject *self, char *name)
 {
     PyObject	*r;
diff --git a/src/if_python3.c b/src/if_python3.c
index aa934cb..b2eb1d4 100644
--- a/src/if_python3.c
+++ b/src/if_python3.c
@@ -187,12 +187,16 @@
 # define PyList_New py3_PyList_New
 # define PyList_SetItem py3_PyList_SetItem
 # define PyList_Size py3_PyList_Size
+# define PyTuple_New py3_PyTuple_New
+# define PyTuple_GetItem py3_PyTuple_GetItem
+# define PyTuple_SetItem py3_PyTuple_SetItem
+# undef PyTuple_SET_ITEM
+# define PyTuple_SET_ITEM py3_PyTuple_SET_ITEM
+# define PyTuple_Size py3_PyTuple_Size
 # define PySequence_Check py3_PySequence_Check
 # define PySequence_Size py3_PySequence_Size
 # define PySequence_GetItem py3_PySequence_GetItem
 # define PySequence_Fast py3_PySequence_Fast
-# define PyTuple_Size py3_PyTuple_Size
-# define PyTuple_GetItem py3_PyTuple_GetItem
 # if PY_VERSION_HEX >= 0x030601f0
 #  define PySlice_AdjustIndices py3_PySlice_AdjustIndices
 #  define PySlice_Unpack py3_PySlice_Unpack
@@ -371,11 +375,14 @@
 static int (*py3_PyList_Append)(PyObject *, PyObject *);
 static int (*py3_PyList_Insert)(PyObject *, int, PyObject *);
 static Py_ssize_t (*py3_PyList_Size)(PyObject *);
+static PyObject* (*py3_PyTuple_New)(Py_ssize_t size);
+static int (*py3_PyTuple_SetItem)(PyObject *, Py_ssize_t, PyObject *);
+static int (*py3_PyTuple_SET_ITEM)(PyObject *, Py_ssize_t, PyObject *);
+static Py_ssize_t (*py3_PyTuple_Size)(PyObject *);
 static int (*py3_PySequence_Check)(PyObject *);
 static Py_ssize_t (*py3_PySequence_Size)(PyObject *);
 static PyObject* (*py3_PySequence_GetItem)(PyObject *, Py_ssize_t);
 static PyObject* (*py3_PySequence_Fast)(PyObject *, const char *);
-static Py_ssize_t (*py3_PyTuple_Size)(PyObject *);
 static PyObject* (*py3_PyTuple_GetItem)(PyObject *, Py_ssize_t);
 static int (*py3_PyMapping_Check)(PyObject *);
 static PyObject* (*py3_PyMapping_Keys)(PyObject *);
@@ -585,12 +592,15 @@
     {"PyList_Append", (PYTHON_PROC*)&py3_PyList_Append},
     {"PyList_Insert", (PYTHON_PROC*)&py3_PyList_Insert},
     {"PyList_Size", (PYTHON_PROC*)&py3_PyList_Size},
+    {"PyTuple_New", (PYTHON_PROC*)&py3_PyTuple_New},
+    {"PyTuple_GetItem", (PYTHON_PROC*)&py3_PyTuple_GetItem},
+    {"PyTuple_SetItem", (PYTHON_PROC*)&py3_PyTuple_SetItem},
+    {"PyTuple_SET_ITEM", (PYTHON_PROC*)&py3_PyTuple_SET_ITEM},
+    {"PyTuple_Size", (PYTHON_PROC*)&py3_PyTuple_Size},
     {"PySequence_Check", (PYTHON_PROC*)&py3_PySequence_Check},
     {"PySequence_Size", (PYTHON_PROC*)&py3_PySequence_Size},
     {"PySequence_GetItem", (PYTHON_PROC*)&py3_PySequence_GetItem},
     {"PySequence_Fast", (PYTHON_PROC*)&py3_PySequence_Fast},
-    {"PyTuple_Size", (PYTHON_PROC*)&py3_PyTuple_Size},
-    {"PyTuple_GetItem", (PYTHON_PROC*)&py3_PyTuple_GetItem},
 # if PY_VERSION_HEX >= 0x030601f0
     {"PySlice_AdjustIndices", (PYTHON_PROC*)&py3_PySlice_AdjustIndices},
     {"PySlice_Unpack", (PYTHON_PROC*)&py3_PySlice_Unpack},
@@ -1113,6 +1123,8 @@
 static int DictionarySetattro(PyObject *, PyObject *, PyObject *);
 static PyObject *ListGetattro(PyObject *, PyObject *);
 static int ListSetattro(PyObject *, PyObject *, PyObject *);
+static PyObject *TupleGetattro(PyObject *, PyObject *);
+static int TupleSetattro(PyObject *, PyObject *, PyObject *);
 static PyObject *FunctionGetattro(PyObject *, PyObject *);
 
 static struct PyModuleDef vimmodule;
@@ -2028,6 +2040,26 @@
     return ListSetattr(self, name, val);
 }
 
+// Tuple object - Definitions
+
+    static PyObject *
+TupleGetattro(PyObject *self, PyObject *nameobj)
+{
+    GET_ATTR_STRING(name, nameobj);
+
+    if (strcmp(name, "locked") == 0)
+	return PyLong_FromLong(((TupleObject *) (self))->tuple->tv_lock);
+
+    return PyObject_GenericGetAttr(self, nameobj);
+}
+
+    static int
+TupleSetattro(PyObject *self, PyObject *nameobj, PyObject *val)
+{
+    GET_ATTR_STRING(name, nameobj);
+    return TupleSetattr(self, name, val);
+}
+
 // Function object - Definitions
 
     static PyObject *
diff --git a/src/proto/gc.pro b/src/proto/gc.pro
index 8b55030..8c8f73f 100644
--- a/src/proto/gc.pro
+++ b/src/proto/gc.pro
@@ -5,6 +5,7 @@
 int set_ref_in_dict(dict_T *d, int copyID);
 int set_ref_in_list(list_T *ll, int copyID);
 int set_ref_in_list_items(list_T *l, int copyID, ht_stack_T **ht_stack, tuple_stack_T **tuple_stack);
+int set_ref_in_tuple(tuple_T *tuple, int copyID);
 int set_ref_in_tuple_items(tuple_T *tuple, int copyID, ht_stack_T **ht_stack, list_stack_T **list_stack);
 int set_ref_in_callback(callback_T *cb, int copyID);
 int set_ref_in_item_class(class_T *cl, int copyID, ht_stack_T **ht_stack, list_stack_T **list_stack, tuple_stack_T **tuple_stack);
diff --git a/src/testdir/test_python2.vim b/src/testdir/test_python2.vim
index 4ba0f8e..cafba3d 100644
--- a/src/testdir/test_python2.vim
+++ b/src/testdir/test_python2.vim
@@ -406,6 +406,107 @@
         \ 'Vim(python):TypeError: index must be int or slice, not dict')
 endfunc
 
+" Test for the python Tuple object
+func Test_python_tuple()
+  " Try to convert a null tuple
+  call AssertException(["py l = vim.eval('test_null_tuple()')"],
+        \ 'Vim(python):SystemError: error return without exception set')
+
+  " Try to convert a Tuple with a null Tuple item
+  call AssertException(["py t = vim.eval('(test_null_tuple(),)')"],
+        \ 'Vim(python):SystemError: error return without exception set')
+
+  " Try to convert a List with a null Tuple item
+  call AssertException(["py t = vim.eval('[test_null_tuple()]')"],
+        \ 'Vim(python):SystemError: error return without exception set')
+
+  " Try to convert a Tuple with a null List item
+  call AssertException(["py t = vim.eval('(test_null_list(),)')"],
+        \ 'Vim(python):SystemError: error return without exception set')
+
+  " Try to bind a null Tuple variable (works because an empty tuple is used)
+  let cmds =<< trim END
+    let t = test_null_tuple()
+    py tt = vim.bindeval('t')
+  END
+  call AssertException(cmds, '')
+
+  " Creating a tuple using different iterators
+  py t1 = vim.Tuple(['abc', 20, 1.2, (4, 5)])
+  call assert_equal(('abc', 20, 1.2, (4, 5)), pyeval('t1'))
+  py t2 = vim.Tuple('abc')
+  call assert_equal(('a', 'b', 'c'), pyeval('t2'))
+  py t3 = vim.Tuple({'color': 'red', 'model': 'ford'})
+  call assert_equal(('color', 'model'), pyeval('t3'))
+  py t4 = vim.Tuple()
+  call assert_equal((), pyeval('t4'))
+  py t5 = vim.Tuple(x**2 for x in range(5))
+  call assert_equal((0, 1, 4, 9, 16), pyeval('t5'))
+  py t6 = vim.Tuple(('abc', 20, 1.2, (4, 5)))
+  call assert_equal(('abc', 20, 1.2, (4, 5)), pyeval('t6'))
+
+  " Convert between Vim tuple/list and python tuple/list
+  py t = vim.Tuple(vim.bindeval("('a', ('b',), ['c'], {'s': 'd'})"))
+  call assert_equal(('a', ('b',), ['c'], {'s': 'd'}), pyeval('t'))
+  call assert_equal(['a', ('b',), ['c'], {'s': 'd'}], pyeval('list(t)'))
+  call assert_equal(('a', ('b',), ['c'], {'s': 'd'}), pyeval('tuple(t)'))
+
+  py l = vim.List(vim.bindeval("['e', ('f',), ['g'], {'s': 'h'}]"))
+  call assert_equal(('e', ('f',), ['g'], {'s': 'h'}), pyeval('tuple(l)'))
+
+  " Tuple assignment
+  py tt = vim.bindeval('("a", "b")')
+  call AssertException(['py tt[0] = 10'],
+        \ "Vim(python):TypeError: 'vim.tuple' object does not support item assignment")
+  py tt = vim.bindeval('("a", "b")')
+  call AssertException(['py tt[0:1] = (10, 20)'],
+        \ "Vim(python):TypeError: 'vim.tuple' object does not support item assignment")
+
+  " iterating over tuple from Python
+  py print([x for x in vim.bindeval("('a', 'b')")])
+
+  " modifying a list item within a tuple
+  let t = ('a', ['b', 'c'], 'd')
+  py vim.bindeval('t')[1][1] = 'x'
+  call assert_equal(('a', ['b', 'x'], 'd'), t)
+
+  " length of a tuple
+  let t = ()
+  py p_t = vim.bindeval('t')
+  call assert_equal(0, pyeval('len(p_t)'))
+  let t = ('a', )
+  py p_t = vim.bindeval('t')
+  call assert_equal(1, pyeval('len(p_t)'))
+  let t = ('a', 'b', 'c')
+  py p_t = vim.bindeval('t')
+  call assert_equal(3, pyeval('len(p_t)'))
+
+  " membership test
+  let t = ('a', 'b', 'c')
+  py p_t = vim.bindeval('t')
+  call assert_true(pyeval("b'c' in p_t"))
+  call assert_true(pyeval("b'd' not in p_t"))
+
+  py x = vim.eval('("a", (2), [3], {})')
+  call assert_equal(('a', '2', ['3'], {}), pyeval('x'))
+
+  " Using a keyword argument for a tuple
+  call AssertException(['py x = vim.Tuple(a=1)'],
+        \ 'Vim(python):TypeError: tuple constructor does not accept keyword arguments')
+
+  " Using dict as an index
+  call AssertException(['py x = tt[{}]'],
+        \ 'Vim(python):TypeError: index must be int or slice, not dict')
+  call AssertException(['py x = tt["abc"]'],
+        \ 'Vim(python):TypeError: index must be int or slice, not str')
+
+  call AssertException(['py del tt.locked'],
+        \ 'Vim(python):AttributeError: cannot delete vim.Tuple attributes')
+
+  call AssertException(['py tt.foobar = 1'],
+        \ 'Vim(python):AttributeError: cannot set attribute foobar')
+endfunc
+
 " Test for the python Dict object
 func Test_python_dict()
   let d = {}
@@ -787,11 +888,24 @@
         \ 'Vim(python):TypeError: cannot modify fixed list')
 endfunc
 
+" Test for locking/unlocking a tuple
+func Test_tuple_lock()
+  let t = (1, 2, 3)
+  py t = vim.bindeval('t')
+  py t.locked = True
+  call assert_equal(1, islocked('t'))
+  py t.locked = False
+  call assert_equal(0, islocked('t'))
+endfunc
+
 " Test for pyeval()
 func Test_python_pyeval()
   let l = pyeval('range(3)')
   call assert_equal([0, 1, 2], l)
 
+  let t = pyeval('("a", "b", "c")')
+  call assert_equal(("a", "b", "c"), t)
+
   let d = pyeval('{"a": "b", "c": 1, "d": ["e"]}')
   call assert_equal([['a', 'b'], ['c', 1], ['d', ['e']]], sort(items(d)))
 
@@ -812,18 +926,20 @@
   call AssertException(['let v = pyeval("vim")'], 'E859:')
 endfunc
 
-" Test for py3eval with locals
+" Test for pyeval with locals
 func Test_python_pyeval_locals()
   let str = 'a string'
   let num = 0xbadb33f
   let d = {'a': 1, 'b': 2, 'c': str}
   let l = [ str, num, d ]
+  let t = ( str, num, d )
 
   let locals = #{
         \ s: str,
         \ n: num,
         \ d: d,
         \ l: l,
+        \ t: t,
         \ }
 
   " check basics
@@ -831,6 +947,8 @@
   call assert_equal(0xbadb33f, pyeval('n', locals))
   call assert_equal(d, pyeval('d', locals))
   call assert_equal(l, pyeval('l', locals))
+  call assert_equal(t, pyeval('t', locals))
+  call assert_equal('a-b-c', 'b"-".join(t)'->pyeval({'t': ('a', 'b', 'c')}))
 
   py << trim EOF
   def __UpdateDict(d, upd):
@@ -997,6 +1115,92 @@
         \ 'Vim(python):SystemError: error return without exception set')
 endfunc
 
+" Slice
+func Test_python_tuple_slice()
+  py tt = vim.bindeval('(0, 1, 2, 3, 4, 5)')
+  py t = tt[:4]
+  call assert_equal((0, 1, 2, 3), pyeval('t'))
+  py t = tt[2:]
+  call assert_equal((2, 3, 4, 5), pyeval('t'))
+  py t = tt[:-4]
+  call assert_equal((0, 1), pyeval('t'))
+  py t = tt[-2:]
+  call assert_equal((4, 5), pyeval('t'))
+  py t = tt[2:4]
+  call assert_equal((2, 3), pyeval('t'))
+  py t = tt[4:2]
+  call assert_equal((), pyeval('t'))
+  py t = tt[-4:-2]
+  call assert_equal((2, 3), pyeval('t'))
+  py t = tt[-2:-4]
+  call assert_equal((), pyeval('t'))
+  py t = tt[:]
+  call assert_equal((0, 1, 2, 3, 4, 5), pyeval('t'))
+  py t = tt[0:6]
+  call assert_equal((0, 1, 2, 3, 4, 5), pyeval('t'))
+  py t = tt[-10:10]
+  call assert_equal((0, 1, 2, 3, 4, 5), pyeval('t'))
+  py t = tt[4:2:-1]
+  call assert_equal((4, 3), pyeval('t'))
+  py t = tt[::2]
+  call assert_equal((0, 2, 4), pyeval('t'))
+  py t = tt[4:2:1]
+  call assert_equal((), pyeval('t'))
+
+  " Error case: Use an invalid index
+  call AssertException(['py x = tt[-10]'], 'Vim(python):IndexError: tuple index out of range')
+
+  " Use a step value of 0
+  call AssertException(['py x = tt[0:3:0]'],
+        \ 'Vim(python):ValueError: slice step cannot be zero')
+
+  " Error case: Invalid slice type
+  call AssertException(["py x = tt['abc']"],
+        \ "Vim(python):TypeError: index must be int or slice, not str")
+
+  " Error case: List with a null tuple item
+  let t = (test_null_tuple(),)
+  py tt = vim.bindeval('t')
+  call AssertException(["py x = tt[:]"], 'Vim(python):SystemError: error return without exception set')
+endfunc
+
+func Test_python_pytuple_to_vimtuple()
+  let t = pyeval("('a', 'b')")
+  call assert_equal(('a', 'b'), t)
+  let t = pyeval("()")
+  call assert_equal((), t)
+  let t = pyeval("('x',)")
+  call assert_equal(('x',), t)
+  let t = pyeval("((1, 2), (), (3, 4))")
+  call assert_equal(((1, 2), (), (3, 4)), t)
+  let t = pyeval("((1, 2), {'a': 10}, [5, 6])")
+  call assert_equal(((1, 2), {'a': 10}, [5, 6]), t)
+
+  " Invalid python tuple
+  py << trim END
+    class FailingIter(object):
+      def __iter__(self):
+        raise NotImplementedError('iter')
+  END
+  call assert_fails('call pyeval("(1, FailingIter, 2)")',
+        \ 'E859: Failed to convert returned python object to a Vim value')
+
+  py del FailingIter
+endfunc
+
+" Test for tuple garbage collection
+func Test_python_tuple_garbage_collect()
+  let t = (1, (2, 3), [4, 5], {'a': 6})
+  py py_t = vim.bindeval('t')
+  let save_testing = v:testing
+  let v:testing = 1
+  call test_garbagecollect_now()
+  let v:testing = save_testing
+
+  let new_t = pyeval('py_t')
+  call assert_equal((1, (2, 3), [4, 5], {'a': 6}), new_t)
+endfunc
+
 " Vars
 func Test_python_vars()
   let g:foo = 'bac'
@@ -1976,6 +2180,7 @@
             ('range',      vim.current.range),
             ('dictionary', vim.bindeval('{}')),
             ('list',       vim.bindeval('[]')),
+            ('tuple',       vim.bindeval('()')),
             ('function',   vim.bindeval('function("tr")')),
             ('output',     sys.stdout),
         ):
@@ -1991,6 +2196,7 @@
     range:__dir__,__members__,append,end,start
     dictionary:__dir__,__members__,get,has_key,items,keys,locked,pop,popitem,scope,update,values
     list:__dir__,__members__,extend,locked
+    tuple:__dir__,__members__,locked
     function:__dir__,__members__,args,auto_rebind,self,softspace
     output:__dir__,__members__,close,closed,flush,isatty,readable,seekable,softspace,writable,write,writelines
   END
@@ -2003,7 +2209,9 @@
   call assert_equal({'a': 1}, pyeval('vim.Dictionary(a=1)'))
   call assert_equal({'a': 1}, pyeval('vim.Dictionary(((''a'', 1),))'))
   call assert_equal([], pyeval('vim.List()'))
+  call assert_equal((), pyeval('vim.Tuple()'))
   call assert_equal(['a', 'b', 'c', '7'], pyeval('vim.List(iter(''abc7''))'))
+  call assert_equal(('a', 'b', 'c', '7'), pyeval('vim.Tuple(iter(''abc7''))'))
   call assert_equal(function('tr'), pyeval('vim.Function(''tr'')'))
   call assert_equal(function('tr', [123, 3, 4]),
         \ pyeval('vim.Function(''tr'', args=[123, 3, 4])'))
diff --git a/src/testdir/test_python3.vim b/src/testdir/test_python3.vim
index c044954..e59ddf9 100644
--- a/src/testdir/test_python3.vim
+++ b/src/testdir/test_python3.vim
@@ -8,6 +8,10 @@
   return [1]
 endfunction
 
+func Create_vim_tuple()
+  return ('a', 'b')
+endfunction
+
 func Create_vim_dict()
   return {'a': 1}
 endfunction
@@ -627,6 +631,107 @@
         \ 'Vim(py3):TypeError: index must be int or slice, not dict')
 endfunc
 
+" Test for the python Tuple object
+func Test_python3_tuple()
+  " Try to convert a null tuple
+  call AssertException(["py3 l = vim.eval('test_null_tuple()')"],
+        \ s:system_error_pat)
+
+  " Try to convert a Tuple with a null Tuple item
+  call AssertException(["py3 t = vim.eval('(test_null_tuple(),)')"],
+        \ s:system_error_pat)
+
+  " Try to convert a List with a null Tuple item
+  call AssertException(["py3 t = vim.eval('[test_null_tuple()]')"],
+        \ s:system_error_pat)
+
+  " Try to convert a Tuple with a null List item
+  call AssertException(["py3 t = vim.eval('(test_null_list(),)')"],
+        \ s:system_error_pat)
+
+  " Try to bind a null Tuple variable (works because an empty tuple is used)
+  let cmds =<< trim END
+    let t = test_null_tuple()
+    py3 tt = vim.bindeval('t')
+  END
+  call AssertException(cmds, '')
+
+  " Creating a tuple using different iterators
+  py3 t1 = vim.Tuple(['abc', 20, 1.2, (4, 5)])
+  call assert_equal(('abc', 20, 1.2, (4, 5)), py3eval('t1'))
+  py3 t2 = vim.Tuple('abc')
+  call assert_equal(('a', 'b', 'c'), py3eval('t2'))
+  py3 t3 = vim.Tuple({'color': 'red', 'model': 'ford'})
+  call assert_equal(('color', 'model'), py3eval('t3'))
+  py3 t4 = vim.Tuple()
+  call assert_equal((), py3eval('t4'))
+  py3 t5 = vim.Tuple(x**2 for x in range(5))
+  call assert_equal((0, 1, 4, 9, 16), py3eval('t5'))
+  py3 t6 = vim.Tuple(('abc', 20, 1.2, (4, 5)))
+  call assert_equal(('abc', 20, 1.2, (4, 5)), py3eval('t6'))
+
+  " Convert between Vim tuple/list and python tuple/list
+  py3 t = vim.Tuple(vim.bindeval("('a', ('b',), ['c'], {'s': 'd'})"))
+  call assert_equal(('a', ('b',), ['c'], {'s': 'd'}), py3eval('t'))
+  call assert_equal(['a', ('b',), ['c'], {'s': 'd'}], py3eval('list(t)'))
+  call assert_equal(('a', ('b',), ['c'], {'s': 'd'}), py3eval('tuple(t)'))
+
+  py3 l = vim.List(vim.bindeval("['e', ('f',), ['g'], {'s': 'h'}]"))
+  call assert_equal(('e', ('f',), ['g'], {'s': 'h'}), py3eval('tuple(l)'))
+
+  " Tuple assignment
+  py3 tt = vim.bindeval('("a", "b")')
+  call AssertException(['py3 tt[0] = 10'],
+        \ "Vim(py3):TypeError: 'vim.tuple' object does not support item assignment")
+  py3 tt = vim.bindeval('("a", "b")')
+  call AssertException(['py3 tt[0:1] = (10, 20)'],
+        \ "Vim(py3):TypeError: 'vim.tuple' object does not support item assignment")
+
+  " iterating over tuple from Python
+  py3 print([x for x in vim.bindeval("('a', 'b')")])
+
+  " modifying a list item within a tuple
+  let t = ('a', ['b', 'c'], 'd')
+  py3 vim.bindeval('t')[1][1] = 'x'
+  call assert_equal(('a', ['b', 'x'], 'd'), t)
+
+  " length of a tuple
+  let t = ()
+  py3 p_t = vim.bindeval('t')
+  call assert_equal(0, py3eval('len(p_t)'))
+  let t = ('a', )
+  py3 p_t = vim.bindeval('t')
+  call assert_equal(1, py3eval('len(p_t)'))
+  let t = ('a', 'b', 'c')
+  py3 p_t = vim.bindeval('t')
+  call assert_equal(3, py3eval('len(p_t)'))
+
+  " membership test
+  let t = ('a', 'b', 'c')
+  py3 p_t = vim.bindeval('t')
+  call assert_true(py3eval("b'c' in p_t"))
+  call assert_true(py3eval("b'd' not in p_t"))
+
+  py3 x = vim.eval('("a", (2), [3], {})')
+  call assert_equal(('a', '2', ['3'], {}), py3eval('x'))
+
+  " Using a keyword argument for a tuple
+  call AssertException(['py3 x = vim.Tuple(a=1)'],
+        \ 'Vim(py3):TypeError: tuple constructor does not accept keyword arguments')
+
+  " Using dict as an index
+  call AssertException(['py3 x = tt[{}]'],
+        \ 'Vim(py3):TypeError: index must be int or slice, not dict')
+  call AssertException(['py3 x = tt["abc"]'],
+        \ 'Vim(py3):TypeError: index must be int or slice, not str')
+
+  call AssertException(['py3 del tt.locked'],
+        \ 'Vim(py3):AttributeError: cannot delete vim.Tuple attributes')
+
+  call AssertException(['py3 tt.foobar = 1'],
+        \ 'Vim(py3):AttributeError: cannot set attribute foobar')
+endfunc
+
 " Test for the python Dict object
 func Test_python3_dict()
   " Try to convert a null Dict
@@ -1005,11 +1110,24 @@
         \ 'Vim(py3):TypeError: cannot modify fixed list')
 endfunc
 
+" Test for locking/unlocking a tuple
+func Test_tuple_lock()
+  let t = (1, 2, 3)
+  py3 t = vim.bindeval('t')
+  py3 t.locked = True
+  call assert_equal(1, islocked('t'))
+  py3 t.locked = False
+  call assert_equal(0, islocked('t'))
+endfunc
+
 " Test for py3eval()
 func Test_python3_pyeval()
   let l = py3eval('[0, 1, 2]')
   call assert_equal([0, 1, 2], l)
 
+  let t = py3eval('("a", "b", "c")')
+  call assert_equal(("a", "b", "c"), t)
+
   let d = py3eval('{"a": "b", "c": 1, "d": ["e"]}')
   call assert_equal([['a', 'b'], ['c', 1], ['d', ['e']]], sort(items(d)))
 
@@ -1036,12 +1154,14 @@
   let num = 0xbadb33f
   let d = {'a': 1, 'b': 2, 'c': str}
   let l = [ str, num, d ]
+  let t = ( str, num, d )
 
   let locals = #{
         \ s: str,
         \ n: num,
         \ d: d,
         \ l: l,
+        \ t: t,
         \ }
 
   " check basics
@@ -1049,9 +1169,11 @@
   call assert_equal(0xbadb33f, py3eval('n', locals))
   call assert_equal(d, py3eval('d', locals))
   call assert_equal(l, py3eval('l', locals))
+  call assert_equal(t, py3eval('t', locals))
   call assert_equal('a,b,c', py3eval('b",".join(l)', {'l': ['a', 'b', 'c']}))
   call assert_equal('hello', 's'->py3eval({'s': 'hello'}))
   call assert_equal('a,b,c', 'b",".join(l)'->py3eval({'l': ['a', 'b', 'c']}))
+  call assert_equal('a-b-c', 'b"-".join(t)'->py3eval({'t': ('a', 'b', 'c')}))
 
   py3 << trim EOF
   def __UpdateDict(d, upd):
@@ -1218,6 +1340,92 @@
         \ s:system_error_pat)
 endfunc
 
+" Slice
+func Test_python3_tuple_slice()
+  py3 tt = vim.bindeval('(0, 1, 2, 3, 4, 5)')
+  py3 t = tt[:4]
+  call assert_equal((0, 1, 2, 3), py3eval('t'))
+  py3 t = tt[2:]
+  call assert_equal((2, 3, 4, 5), py3eval('t'))
+  py3 t = tt[:-4]
+  call assert_equal((0, 1), py3eval('t'))
+  py3 t = tt[-2:]
+  call assert_equal((4, 5), py3eval('t'))
+  py3 t = tt[2:4]
+  call assert_equal((2, 3), py3eval('t'))
+  py3 t = tt[4:2]
+  call assert_equal((), py3eval('t'))
+  py3 t = tt[-4:-2]
+  call assert_equal((2, 3), py3eval('t'))
+  py3 t = tt[-2:-4]
+  call assert_equal((), py3eval('t'))
+  py3 t = tt[:]
+  call assert_equal((0, 1, 2, 3, 4, 5), py3eval('t'))
+  py3 t = tt[0:6]
+  call assert_equal((0, 1, 2, 3, 4, 5), py3eval('t'))
+  py3 t = tt[-10:10]
+  call assert_equal((0, 1, 2, 3, 4, 5), py3eval('t'))
+  py3 t = tt[4:2:-1]
+  call assert_equal((4, 3), py3eval('t'))
+  py3 t = tt[::2]
+  call assert_equal((0, 2, 4), py3eval('t'))
+  py3 t = tt[4:2:1]
+  call assert_equal((), py3eval('t'))
+
+  " Error case: Use an invalid index
+  call AssertException(['py3 x = tt[-10]'], 'Vim(py3):IndexError: tuple index out of range')
+
+  " Use a step value of 0
+  call AssertException(['py3 x = tt[0:3:0]'],
+        \ 'Vim(py3):ValueError: slice step cannot be zero')
+
+  " Error case: Invalid slice type
+  call AssertException(["py3 x = tt['abc']"],
+        \ "Vim(py3):TypeError: index must be int or slice, not str")
+
+  " Error case: List with a null tuple item
+  let t = (test_null_tuple(),)
+  py3 tt = vim.bindeval('t')
+  call AssertException(["py3 x = tt[:]"], s:system_error_pat)
+endfunc
+
+func Test_python3_pytuple_to_vimtuple()
+  let t = py3eval("('a', 'b')")
+  call assert_equal(('a', 'b'), t)
+  let t = py3eval("()")
+  call assert_equal((), t)
+  let t = py3eval("('x',)")
+  call assert_equal(('x',), t)
+  let t = py3eval("((1, 2), (), (3, 4))")
+  call assert_equal(((1, 2), (), (3, 4)), t)
+  let t = py3eval("((1, 2), {'a': 10}, [5, 6])")
+  call assert_equal(((1, 2), {'a': 10}, [5, 6]), t)
+
+  " Invalid python tuple
+  py3 << trim END
+    class FailingIter(object):
+      def __iter__(self):
+        raise NotImplementedError('iter')
+  END
+  call assert_fails('call py3eval("(1, FailingIter, 2)")',
+        \ 'E859: Failed to convert returned python object to a Vim value')
+
+  py3 del FailingIter
+endfunc
+
+" Test for tuple garbage collection
+func Test_python3_tuple_garbage_collect()
+  let t = (1, (2, 3), [4, 5], {'a': 6})
+  py3 py_t = vim.bindeval('t')
+  let save_testing = v:testing
+  let v:testing = 1
+  call test_garbagecollect_now()
+  let v:testing = save_testing
+
+  let new_t = py3eval('py_t')
+  call assert_equal((1, (2, 3), [4, 5], {'a': 6}), new_t)
+endfunc
+
 " Vars
 func Test_python3_vars()
   let g:foo = 'bac'
@@ -2189,6 +2397,7 @@
             ('range',      vim.current.range),
             ('dictionary', vim.bindeval('{}')),
             ('list',       vim.bindeval('[]')),
+            ('tuple',       vim.bindeval('()')),
             ('function',   vim.bindeval('function("tr")')),
             ('output',     sys.stdout),
         ):
@@ -2204,6 +2413,7 @@
     range:__dir__,append,end,start
     dictionary:__dir__,get,has_key,items,keys,locked,pop,popitem,scope,update,values
     list:__dir__,extend,locked
+    tuple:__dir__,locked
     function:__dir__,args,auto_rebind,self,softspace
     output:__dir__,close,closed,flush,isatty,readable,seekable,softspace,writable,write,writelines
   END
@@ -2216,7 +2426,9 @@
   call assert_equal({'a': 1}, py3eval('vim.Dictionary(a=1)'))
   call assert_equal({'a': 1}, py3eval('vim.Dictionary(((''a'', 1),))'))
   call assert_equal([], py3eval('vim.List()'))
+  call assert_equal((), py3eval('vim.Tuple()'))
   call assert_equal(['a', 'b', 'c', '7'], py3eval('vim.List(iter(''abc7''))'))
+  call assert_equal(('a', 'b', 'c', '7'), py3eval('vim.Tuple(iter(''abc7''))'))
   call assert_equal(function('tr'), py3eval('vim.Function(''tr'')'))
   call assert_equal(function('tr', [123, 3, 4]),
         \ py3eval('vim.Function(''tr'', args=[123, 3, 4])'))
@@ -4065,6 +4277,7 @@
 " Regression: Iterator for a Vim object should hold a reference.
 func Test_python3_iter_ref()
   let g:list_iter_ref_count_increase = -1
+  let g:tuple_iter_ref_count_increase = -1
   let g:dict_iter_ref_count_increase = -1
   let g:bufmap_iter_ref_count_increase = -1
   let g:options_iter_ref_count_increase = -1
@@ -4080,6 +4293,12 @@
       for el in v:
         vim.vars['list_iter_ref_count_increase'] = sys.getrefcount(v) - base_ref_count
 
+      create_tuple = vim.Function('Create_vim_tuple')
+      v = create_tuple()
+      base_ref_count = sys.getrefcount(v)
+      for el in v:
+        vim.vars['tuple_iter_ref_count_increase'] = sys.getrefcount(v) - base_ref_count
+
       create_dict = vim.Function('Create_vim_dict')
       v = create_dict()
       base_ref_count = sys.getrefcount(v)
@@ -4100,6 +4319,7 @@
   EOF
 
   call assert_equal(1, g:list_iter_ref_count_increase)
+  call assert_equal(1, g:tuple_iter_ref_count_increase)
   call assert_equal(1, g:dict_iter_ref_count_increase)
   if py3eval('sys.version_info[:2] < (3, 13)')
     call assert_equal(1, g:bufmap_iter_ref_count_increase)
diff --git a/src/testdir/test_tuple.vim b/src/testdir/test_tuple.vim
index fce5292..875031f 100644
--- a/src/testdir/test_tuple.vim
+++ b/src/testdir/test_tuple.vim
@@ -1241,12 +1241,43 @@
 
 " Test for locking and unlocking a tuple variable
 func Test_tuple_lock()
+  " lockvar 0
+  let g:t = ([0, 1],)
   let lines =<< trim END
-    VAR t = ([0, 1],)
-    call add(t[0], 2)
-    call assert_equal(([0, 1, 2], ), t)
+    lockvar 0 g:t
+    LET g:t = ()
   END
-  call v9.CheckSourceLegacyAndVim9Success(lines)
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E1122: Variable is locked: g:t',
+        \ 'E1122: Variable is locked: t',
+        \ 'E1122: Variable is locked: g:t'])
+  unlet g:t
+
+  " Tuple is immutable.  So "lockvar 1" is not applicable to a tuple.
+
+  " lockvar 2
+  let g:t = ([0, 1],)
+  let lines =<< trim END
+    lockvar 2 g:t
+    call add(g:t[0], 2)
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E741: Value is locked: add() argument',
+        \ 'E741: Value is locked: add() argument',
+        \ 'E741: Value is locked: add() argument'])
+  unlet g:t
+
+  " lockvar 3
+  let g:t = ([0, 1],)
+  let lines =<< trim END
+    lockvar 3 g:t
+    LET g:t[0][0] = 10
+  END
+  call v9.CheckSourceLegacyAndVim9Failure(lines, [
+        \ 'E741: Value is locked: g:t[0][0] = 10',
+        \ 'E1119: Cannot change locked list item',
+        \ 'E741: Value is locked: g:t[0][0] = 10'])
+  unlet g:t
 
   let lines =<< trim END
     VAR t = ([0, 1],)
@@ -1810,6 +1841,25 @@
         \ 'E1226: List or Blob required for argument 1'])
 endfunc
 
+" Test for islocked()
+func Test_tuple_islocked()
+  let lines =<< trim END
+    let t = (1, [2], 3)
+    call assert_equal(0, islocked('t'))
+    call assert_equal(0, islocked('t[1]'))
+    lockvar 1 t
+    call assert_equal(1, islocked('t'))
+    call assert_equal(0, islocked('t[1]'))
+    unlockvar t
+    call assert_equal(0, islocked('t'))
+    lockvar 2 t
+    call assert_equal(1, islocked('t[1]'))
+    unlockvar t
+    call assert_equal(0, islocked('t[1]'))
+  END
+  call v9.CheckSourceSuccess(lines)
+endfunc
+
 " Test for items()
 func Test_tuple_items()
   let lines =<< trim END
diff --git a/src/typval.c b/src/typval.c
index 59ac611..b9f8f1c 100644
--- a/src/typval.c
+++ b/src/typval.c
@@ -2095,6 +2095,9 @@
 	|| (tv->v_type == VAR_LIST
 		&& tv->vval.v_list != NULL
 		&& (tv->vval.v_list->lv_lock & VAR_LOCKED))
+	|| (tv->v_type == VAR_TUPLE
+		&& tv->vval.v_tuple != NULL
+		&& (tv->vval.v_tuple->tv_lock & VAR_LOCKED))
 	|| (tv->v_type == VAR_DICT
 		&& tv->vval.v_dict != NULL
 		&& (tv->vval.v_dict->dv_lock & VAR_LOCKED));
diff --git a/src/version.c b/src/version.c
index 2f4c3b3..dcb428e 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1239,
+/**/
     1238,
 /**/
     1237,