diff --git a/py/runtime.c b/py/runtime.c
index e13adfaa4220b70fb07f64ae35b721c44d453b15..e80791df3388d1ab1cd19c8c51f87de469846586 100644
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -1,3 +1,6 @@
+// in principle, rt_xxx functions are called only by vm/native/viper and make assumptions about args
+// py_xxx functions are safer and can be called by anyone
+
 #include <stdint.h>
 #include <stdlib.h>
 #include <stdio.h>
@@ -48,7 +51,10 @@ typedef enum {
     O_FUN_BC,
     O_FUN_ASM,
     O_BOUND_METH,
+    O_TUPLE,
     O_LIST,
+    O_TUPLE_IT,
+    O_LIST_IT,
     O_SET,
     O_MAP,
     O_CLASS,
@@ -121,11 +127,15 @@ struct _py_obj_base_t {
             py_obj_t meth;
             py_obj_t self;
         } u_bound_meth;
-        struct { // for O_LIST
+        struct { // for O_TUPLE, O_LIST
             int alloc;
             int len;
             py_obj_t *items;
-        } u_list;
+        } u_tuple_list;
+        struct { // for O_TUPLE_IT, O_LIST_IT
+            py_obj_base_t *obj;
+            int cur;
+        } u_tuple_list_it;
         struct { // for O_SET
             int alloc;
             int used;
@@ -330,14 +340,30 @@ py_obj_t py_obj_new_range_iterator(int cur, int stop, int step) {
     return o;
 }
 
-py_obj_t list_append(py_obj_t self_in, py_obj_t arg) {
+py_obj_t py_obj_new_tuple_iterator(py_obj_base_t *tuple, int cur) {
+    py_obj_base_t *o = m_new(py_obj_base_t, 1);
+    o->kind = O_TUPLE_IT;
+    o->u_tuple_list_it.obj = tuple;
+    o->u_tuple_list_it.cur = cur;
+    return o;
+}
+
+py_obj_t py_obj_new_list_iterator(py_obj_base_t *list, int cur) {
+    py_obj_base_t *o = m_new(py_obj_base_t, 1);
+    o->kind = O_LIST_IT;
+    o->u_tuple_list_it.obj = list;
+    o->u_tuple_list_it.cur = cur;
+    return o;
+}
+
+py_obj_t rt_list_append(py_obj_t self_in, py_obj_t arg) {
     assert(IS_O(self_in, O_LIST));
     py_obj_base_t *self = self_in;
-    if (self->u_list.len >= self->u_list.alloc) {
-        self->u_list.alloc *= 2;
-        self->u_list.items = m_renew(py_obj_t, self->u_list.items, self->u_list.alloc);
+    if (self->u_tuple_list.len >= self->u_tuple_list.alloc) {
+        self->u_tuple_list.alloc *= 2;
+        self->u_tuple_list.items = m_renew(py_obj_t, self->u_tuple_list.items, self->u_tuple_list.alloc);
     }
-    self->u_list.items[self->u_list.len++] = arg;
+    self->u_tuple_list.items[self->u_tuple_list.len++] = arg;
     return arg;
 }
 
@@ -346,6 +372,7 @@ static qstr q_print;
 static qstr q_len;
 static qstr q___build_class__;
 static qstr q_AttributeError;
+static qstr q_IndexError;
 static qstr q_NameError;
 static qstr q_TypeError;
 
@@ -392,9 +419,9 @@ py_obj_t py_builtin_print(py_obj_t o) {
 
 py_obj_t py_builtin_len(py_obj_t o_in) {
     py_small_int_t len = 0;
-    if (IS_O(o_in, O_LIST)) {
+    if (IS_O(o_in, O_TUPLE) || IS_O(o_in, O_LIST)) {
         py_obj_base_t *o = o_in;
-        len = o->u_list.len;
+        len = o->u_tuple_list.len;
     } else if (IS_O(o_in, O_MAP)) {
         py_obj_base_t *o = o_in;
         len = o->u_map.used;
@@ -437,6 +464,7 @@ void rt_init() {
     q_len = qstr_from_str_static("len");
     q___build_class__ = qstr_from_str_static("__build_class__");
     q_AttributeError = qstr_from_str_static("AttributeError");
+    q_IndexError = qstr_from_str_static("IndexError");
     q_NameError = qstr_from_str_static("NameError");
     q_TypeError = qstr_from_str_static("TypeError");
 
@@ -458,7 +486,7 @@ void rt_init() {
     next_unique_code_id = 1;
     unique_codes = NULL;
 
-    fun_list_append = rt_make_function_2(list_append);
+    fun_list_append = rt_make_function_2(rt_list_append);
 
 #ifdef WRITE_NATIVE
     fp_native = fopen("out-native", "wb");
@@ -597,8 +625,14 @@ const char *py_obj_get_type_str(py_obj_t o_in) {
             case O_FUN_N:
             case O_FUN_BC:
                 return "function";
+            case O_TUPLE:
+                return "tuple";
             case O_LIST:
                 return "list";
+            case O_TUPLE_IT:
+                return "tuple_iterator";
+            case O_LIST_IT:
+                return "list_iterator";
             case O_SET:
                 return "set";
             case O_MAP:
@@ -639,13 +673,26 @@ void py_obj_print(py_obj_t o_in) {
                 printf("%s: ", qstr_str(o->u_exc2.id));
                 printf(o->u_exc2.fmt, o->u_exc2.s1, o->u_exc2.s2);
                 break;
+            case O_TUPLE:
+                printf("(");
+                for (int i = 0; i < o->u_tuple_list.len; i++) {
+                    if (i > 0) {
+                        printf(", ");
+                    }
+                    py_obj_print(o->u_tuple_list.items[i]);
+                }
+                if (o->u_tuple_list.len == 1) {
+                    printf(",");
+                }
+                printf(")");
+                break;
             case O_LIST:
                 printf("[");
-                for (int i = 0; i < o->u_list.len; i++) {
+                for (int i = 0; i < o->u_tuple_list.len; i++) {
                     if (i > 0) {
                         printf(", ");
                     }
-                    py_obj_print(o->u_list.items[i]);
+                    py_obj_print(o->u_tuple_list.items[i]);
                 }
                 printf("]");
                 break;
@@ -711,7 +758,11 @@ int rt_is_true(py_obj_t arg) {
 }
 
 int rt_get_int(py_obj_t arg) {
-    if (IS_SMALL_INT(arg)) {
+    if (arg == py_const_false) {
+        return 0;
+    } else if (arg == py_const_true) {
+        return 1;
+    } else if (IS_SMALL_INT(arg)) {
         return FROM_SMALL_INT(arg);
     } else {
         assert(0);
@@ -778,11 +829,30 @@ py_obj_t rt_unary_op(int op, py_obj_t arg) {
     return py_const_none;
 }
 
+uint get_index(py_obj_base_t *base, py_obj_t index) {
+    // assumes base is O_TUPLE or O_LIST
+    // TODO False and True are considered 0 and 1 for indexing purposes
+    int len = base->u_tuple_list.len;
+    if (IS_SMALL_INT(index)) {
+        int i = FROM_SMALL_INT(index);
+        if (i < 0) {
+            i += len;
+        }
+        if (i < 0 || i >= len) {
+            nlr_jump(py_obj_new_exception_2(q_IndexError, "%s index out of range", py_obj_get_type_str(base), NULL));
+        }
+        return i;
+    } else {
+        nlr_jump(py_obj_new_exception_2(q_TypeError, "%s indices must be integers, not %s", py_obj_get_type_str(base), py_obj_get_type_str(index)));
+    }
+}
+
 py_obj_t rt_binary_op(int op, py_obj_t lhs, py_obj_t rhs) {
     DEBUG_OP_printf("binary %d %p %p\n", op, lhs, rhs);
     if (op == RT_BINARY_OP_SUBSCR) {
-        if (IS_O(lhs, O_LIST) && IS_SMALL_INT(rhs)) {
-            return ((py_obj_base_t*)lhs)->u_list.items[FROM_SMALL_INT(rhs)];
+        if ((IS_O(lhs, O_TUPLE) || IS_O(lhs, O_LIST))) {
+            uint index = get_index(lhs, rhs);
+            return ((py_obj_base_t*)lhs)->u_tuple_list.items[index];
         } else {
             assert(0);
         }
@@ -945,9 +1015,10 @@ machine_uint_t rt_convert_obj_for_inline_asm(py_obj_t obj) {
                 return (machine_int_t)o->u_flt;
 #endif
 
+            case O_TUPLE:
             case O_LIST:
-                // pointer to start of list (could pass length, but then could use len(x) for that)
-                return (machine_uint_t)o->u_list.items;
+                // pointer to start of tuple/list (could pass length, but then could use len(x) for that)
+                return (machine_uint_t)o->u_tuple_list.items;
 
             default:
                 // just pass along a pointer to the object
@@ -1073,18 +1144,28 @@ py_obj_t rt_call_method_n(int n_args, const py_obj_t *args) {
     return rt_call_function_n(args[n_args + 1], n_args + ((args[n_args] == NULL) ? 0 : 1), args);
 }
 
+// items are in reverse order
+py_obj_t rt_build_tuple(int n_args, py_obj_t *items) {
+    py_obj_base_t *o = m_new(py_obj_base_t, 1);
+    o->kind = O_TUPLE;
+    o->u_tuple_list.alloc = n_args < 4 ? 4 : n_args;
+    o->u_tuple_list.len = n_args;
+    o->u_tuple_list.items = m_new(py_obj_t, o->u_tuple_list.alloc);
+    for (int i = 0; i < n_args; i++) {
+        o->u_tuple_list.items[i] = items[n_args - i - 1];
+    }
+    return o;
+}
+
 // items are in reverse order
 py_obj_t rt_build_list(int n_args, py_obj_t *items) {
     py_obj_base_t *o = m_new(py_obj_base_t, 1);
     o->kind = O_LIST;
-    o->u_list.alloc = n_args;
-    if (o->u_list.alloc < 4) {
-        o->u_list.alloc = 4;
-    }
-    o->u_list.len = n_args;
-    o->u_list.items = m_new(py_obj_t, o->u_list.alloc);
+    o->u_tuple_list.alloc = n_args < 4 ? 4 : n_args;
+    o->u_tuple_list.len = n_args;
+    o->u_tuple_list.items = m_new(py_obj_t, o->u_tuple_list.alloc);
     for (int i = 0; i < n_args; i++) {
-        o->u_list.items[i] = items[n_args - i - 1];
+        o->u_tuple_list.items[i] = items[n_args - i - 1];
     }
     return o;
 }
@@ -1260,18 +1341,10 @@ void rt_store_attr(py_obj_t base, qstr attr, py_obj_t val) {
 }
 
 void rt_store_subscr(py_obj_t base, py_obj_t index, py_obj_t value) {
-    if (IS_O(base, O_LIST) && IS_SMALL_INT(index)) {
+    if (IS_O(base, O_LIST)) {
         // list store
-        py_obj_base_t *o = base;
-        int idx = FROM_SMALL_INT(index);
-        if (idx < 0) {
-            idx += o->u_list.len;
-        }
-        if (0 <= idx && idx < o->u_list.len) {
-            o->u_list.items[idx] = value;
-        } else {
-            assert(0);
-        }
+        uint i = get_index(base, index);
+        ((py_obj_base_t*)base)->u_tuple_list.items[i] = value;
     } else if (IS_O(base, O_MAP)) {
         // map store
         py_map_lookup(base, index, true)->value = value;
@@ -1284,6 +1357,10 @@ py_obj_t rt_getiter(py_obj_t o_in) {
     if (IS_O(o_in, O_RANGE)) {
         py_obj_base_t *o = o_in;
         return py_obj_new_range_iterator(o->u_range.start, o->u_range.stop, o->u_range.step);
+    } else if (IS_O(o_in, O_TUPLE)) {
+        return py_obj_new_tuple_iterator(o_in, 0);
+    } else if (IS_O(o_in, O_LIST)) {
+        return py_obj_new_list_iterator(o_in, 0);
     } else {
         nlr_jump(py_obj_new_exception_2(q_TypeError, "'%s' object is not iterable", py_obj_get_type_str(o_in), NULL));
     }
@@ -1299,6 +1376,15 @@ py_obj_t rt_iternext(py_obj_t o_in) {
         } else {
             return py_const_stop_iteration;
         }
+    } else if (IS_O(o_in, O_TUPLE_IT) || IS_O(o_in, O_LIST_IT)) {
+        py_obj_base_t *o = o_in;
+        if (o->u_tuple_list_it.cur < o->u_tuple_list_it.obj->u_tuple_list.len) {
+            py_obj_t o_out = o->u_tuple_list_it.obj->u_tuple_list.items[o->u_tuple_list_it.cur];
+            o->u_tuple_list_it.cur += 1;
+            return o_out;
+        } else {
+            return py_const_stop_iteration;
+        }
     } else {
         nlr_jump(py_obj_new_exception_2(q_TypeError, "? '%s' object is not iterable", py_obj_get_type_str(o_in), NULL));
     }
diff --git a/py/runtime.h b/py/runtime.h
index c36f8d8e7e852d7fb517ab52d49f2914e64bf48c..e9adbe1f0e0a8c4d661d3c370a546b89040f4ad6 100644
--- a/py/runtime.h
+++ b/py/runtime.h
@@ -116,7 +116,9 @@ py_obj_t rt_call_function_n(py_obj_t fun, int n_args, const py_obj_t *args);
 py_obj_t rt_call_method_1(py_obj_t fun, py_obj_t self);
 py_obj_t rt_call_method_2(py_obj_t fun, py_obj_t self, py_obj_t arg);
 py_obj_t rt_call_method_n(int n_args, const py_obj_t *args);
+py_obj_t rt_build_tuple(int n_args, py_obj_t *items);
 py_obj_t rt_build_list(int n_args, py_obj_t *items);
+py_obj_t rt_list_append(py_obj_t list, py_obj_t arg);
 py_obj_t rt_build_map(int n_args);
 py_obj_t rt_store_map(py_obj_t map, py_obj_t key, py_obj_t value);
 py_obj_t rt_build_set(int n_args, py_obj_t *items);
diff --git a/py/vm.c b/py/vm.c
index adf84c0e0228a133d8eccb7d1ed00f1547e028b0..2821d40470dd198042447003e237d60ea91d36f2 100644
--- a/py/vm.c
+++ b/py/vm.c
@@ -268,6 +268,13 @@ py_obj_t py_execute_byte_code(const byte *code, uint len, const py_obj_t *args,
                         *sp = rt_compare_op(unum, obj1, obj2);
                         break;
 
+                    case PYBC_BUILD_TUPLE:
+                        DECODE_UINT;
+                        obj1 = rt_build_tuple(unum, sp);
+                        sp += unum - 1;
+                        *sp = obj1;
+                        break;
+
                     case PYBC_BUILD_LIST:
                         DECODE_UINT;
                         obj1 = rt_build_list(unum, sp);
@@ -275,6 +282,13 @@ py_obj_t py_execute_byte_code(const byte *code, uint len, const py_obj_t *args,
                         *sp = obj1;
                         break;
 
+                    case PYBC_LIST_APPEND:
+                        DECODE_UINT;
+                        // I think it's guaranteed by the compiler that sp[unum] is a list
+                        rt_list_append(sp[unum], sp[0]);
+                        sp++;
+                        break;
+
                     case PYBC_BUILD_MAP:
                         DECODE_UINT;
                         PUSH(rt_build_map(unum));