diff --git a/py/builtin.c b/py/builtin.c
index 0d18508afffcdca8e1768cd327d6f8e8c970e23e..a5193bf7604eb415d0e0fc6bd9ce0b4f5867747f 100644
--- a/py/builtin.c
+++ b/py/builtin.c
@@ -154,7 +154,7 @@ mp_obj_t mp_builtin_divmod(mp_obj_t o1_in, mp_obj_t o2_in) {
         revs_args[0] = MP_OBJ_NEW_SMALL_INT(i1 % i2);
         return rt_build_tuple(2, revs_args);
     } else {
-        nlr_jump(mp_obj_new_exception_msg_2_args(MP_QSTR_TypeError, "unsupported operand type(s) for divmod(): '%s' and '%s'", mp_obj_get_type_str(o1_in), mp_obj_get_type_str(o2_in)));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "unsupported operand type(s) for divmod(): '%s' and '%s'", mp_obj_get_type_str(o1_in), mp_obj_get_type_str(o2_in)));
     }
 }
 
@@ -188,7 +188,7 @@ mp_obj_t mp_builtin_len(mp_obj_t o_in) {
     } else if (MP_OBJ_IS_TYPE(o_in, &dict_type)) {
         len = mp_obj_dict_len(o_in);
     } else {
-        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "object of type '%s' has no len()", mp_obj_get_type_str(o_in)));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "object of type '%s' has no len()", mp_obj_get_type_str(o_in)));
     }
     return MP_OBJ_NEW_SMALL_INT(len);
 }
@@ -263,7 +263,7 @@ mp_obj_t mp_builtin_ord(mp_obj_t o_in) {
     if (strlen(str) == 1) {
         return mp_obj_new_int(str[0]);
     } else {
-        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "ord() expected a character, but string of length %d found", (void*)(machine_int_t)strlen(str)));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "ord() expected a character, but string of length %d found", strlen(str)));
     }
 }
 
@@ -271,7 +271,7 @@ mp_obj_t mp_builtin_pow(int n_args, const mp_obj_t *args) {
     switch (n_args) {
         case 2: return rt_binary_op(RT_BINARY_OP_POWER, args[0], args[1]);
         case 3: return rt_binary_op(RT_BINARY_OP_MODULO, rt_binary_op(RT_BINARY_OP_POWER, args[0], args[1]), args[2]); // TODO optimise...
-        default: nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "pow expected at most 3 arguments, got %d", (void*)(machine_int_t)n_args));
+        default: nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "pow expected at most 3 arguments, got %d", n_args));
     }
 }
 
@@ -297,7 +297,7 @@ mp_obj_t mp_builtin_range(int n_args, const mp_obj_t *args) {
         case 1: return mp_obj_new_range(0, mp_obj_get_int(args[0]), 1);
         case 2: return mp_obj_new_range(mp_obj_get_int(args[0]), mp_obj_get_int(args[1]), 1);
         case 3: return mp_obj_new_range(mp_obj_get_int(args[0]), mp_obj_get_int(args[1]), mp_obj_get_int(args[2]));
-        default: nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "range expected at most 3 arguments, got %d", (void*)(machine_int_t)n_args));
+        default: nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "range expected at most 3 arguments, got %d", n_args));
     }
 }
 
@@ -306,7 +306,7 @@ mp_obj_t mp_builtin_sum(int n_args, const mp_obj_t *args) {
     switch (n_args) {
         case 1: value = mp_obj_new_int(0); break;
         case 2: value = args[1]; break;
-        default: nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "sum expected at most 2 arguments, got %d", (void*)(machine_int_t)n_args));
+        default: nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "sum expected at most 2 arguments, got %d", n_args));
     }
     mp_obj_t iterable = rt_getiter(args[0]);
     mp_obj_t item;
diff --git a/py/obj.c b/py/obj.c
index 866be227aa9b0bde1e9337f5fa2166e5a0e7c2dd..3e4164d5d2c96302d91f04ef35d6c3d43b2999da 100644
--- a/py/obj.c
+++ b/py/obj.c
@@ -145,7 +145,7 @@ machine_int_t mp_obj_get_int(mp_obj_t arg) {
         return (machine_int_t)mp_obj_float_get(arg);
 #endif
     } else {
-        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "can't convert %s to int", mp_obj_get_type_str(arg)));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "can't convert %s to int", mp_obj_get_type_str(arg)));
     }
 }
 
@@ -160,7 +160,7 @@ machine_float_t mp_obj_get_float(mp_obj_t arg) {
     } else if (MP_OBJ_IS_TYPE(arg, &float_type)) {
         return mp_obj_float_get(arg);
     } else {
-        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "can't convert %s to float", mp_obj_get_type_str(arg)));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "can't convert %s to float", mp_obj_get_type_str(arg)));
     }
 }
 
@@ -180,7 +180,7 @@ void mp_obj_get_complex(mp_obj_t arg, mp_float_t *real, mp_float_t *imag) {
     } else if (MP_OBJ_IS_TYPE(arg, &complex_type)) {
         mp_obj_complex_get(arg, real, imag);
     } else {
-        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "can't convert %s to complex", mp_obj_get_type_str(arg)));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "can't convert %s to complex", mp_obj_get_type_str(arg)));
     }
 }
 #endif
@@ -204,11 +204,11 @@ mp_obj_t *mp_obj_get_array_fixed_n(mp_obj_t o_in, machine_int_t n) {
             mp_obj_list_get(o_in, &seq_len, &seq_items);
         }
         if (seq_len != n) {
-            nlr_jump(mp_obj_new_exception_msg_2_args(MP_QSTR_IndexError, "requested length %d but object has length %d", (void*)n, (void*)(machine_uint_t)seq_len));
+            nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_IndexError, "requested length %d but object has length %d", n, seq_len));
         }
         return seq_items;
     } else {
-        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "object '%s' is not a tuple or list", mp_obj_get_type_str(o_in)));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "object '%s' is not a tuple or list", mp_obj_get_type_str(o_in)));
     }
 }
 
@@ -220,10 +220,10 @@ uint mp_get_index(const mp_obj_type_t *type, machine_uint_t len, mp_obj_t index)
             i += len;
         }
         if (i < 0 || i >= len) {
-            nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_IndexError, "%s index out of range", type->name));
+            nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_IndexError, "%s index out of range", type->name));
         }
         return i;
     } else {
-        nlr_jump(mp_obj_new_exception_msg_2_args(MP_QSTR_TypeError, "%s indices must be integers, not %s", type->name, mp_obj_get_type_str(index)));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "%s indices must be integers, not %s", type->name, mp_obj_get_type_str(index)));
     }
 }
diff --git a/py/obj.h b/py/obj.h
index a3af7f556b86a17e00af7fb80e373566d629f70f..5eb9fee75a91ea85187d0026b55e6e97638f6d5f 100644
--- a/py/obj.h
+++ b/py/obj.h
@@ -191,6 +191,7 @@ mp_obj_t mp_obj_new_exception(qstr id);
 mp_obj_t mp_obj_new_exception_msg(qstr id, const char *msg);
 mp_obj_t mp_obj_new_exception_msg_1_arg(qstr id, const char *fmt, const char *a1);
 mp_obj_t mp_obj_new_exception_msg_2_args(qstr id, const char *fmt, const char *a1, const char *a2);
+mp_obj_t mp_obj_new_exception_msg_varg(qstr id, const char *fmt, ...); // counts args by number of % symbols in fmt, excluding %%; can only handle void* sizes (ie no float/double!)
 mp_obj_t mp_obj_new_range(int start, int stop, int step);
 mp_obj_t mp_obj_new_range_iterator(int cur, int stop, int step);
 mp_obj_t mp_obj_new_fun_bc(int n_args, uint n_state, const byte *code);
diff --git a/py/objbool.c b/py/objbool.c
index cfab7db068e5a295841a611a94c747fa20601ef3..da9a67806301fc521a33d89e6d0f93378211ecee 100644
--- a/py/objbool.c
+++ b/py/objbool.c
@@ -27,7 +27,7 @@ static mp_obj_t bool_make_new(mp_obj_t type_in, int n_args, const mp_obj_t *args
     switch (n_args) {
         case 0: return mp_const_false;
         case 1: if (rt_is_true(args[0])) { return mp_const_true; } else { return mp_const_false; }
-        default: nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "bool takes at most 1 argument, %d given", (void*)(machine_int_t)n_args));
+        default: nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "bool takes at most 1 argument, %d given", n_args));
     }
 }
 
diff --git a/py/objexcept.c b/py/objexcept.c
index 829b147853070c83182cb97d13bb51b0196fdba6..4708a27bfc6d6e471872b2ddb94b38163476861e 100644
--- a/py/objexcept.c
+++ b/py/objexcept.c
@@ -1,6 +1,7 @@
 #include <stdlib.h>
 #include <stdint.h>
 #include <string.h>
+#include <stdarg.h>
 #include <assert.h>
 
 #include "nlr.h"
@@ -79,6 +80,37 @@ mp_obj_t mp_obj_new_exception_msg_2_args(qstr id, const char *fmt, const char *a
     return o;
 }
 
+mp_obj_t mp_obj_new_exception_msg_varg(qstr id, const char *fmt, ...) {
+    // count number of arguments by number of % signs, excluding %%
+    int n_args = 1; // count fmt
+    for (const char *s = fmt; *s; s++) {
+        if (*s == '%') {
+            if (s[1] == '%') {
+                s += 1;
+            } else {
+                n_args += 1;
+            }
+        }
+    }
+
+    // make exception object
+    mp_obj_exception_t *o = m_new_obj_var(mp_obj_exception_t, void*, n_args);
+    o->base.type = &exception_type;
+    o->id = id;
+    o->n_args = n_args;
+    o->args[0] = fmt;
+
+    // extract args and store them
+    va_list ap;
+    va_start(ap, fmt);
+    for (int i = 1; i < n_args; i++) {
+        o->args[i] = va_arg(ap, void*);
+    }
+    va_end(ap);
+
+    return o;
+}
+
 qstr mp_obj_exception_get_type(mp_obj_t self_in) {
     assert(MP_OBJ_IS_TYPE(self_in, &exception_type));
     mp_obj_exception_t *self = self_in;
diff --git a/py/runtime.c b/py/runtime.c
index 84a902f2f42324a58130e091f73672ae44cccb0d..387e10abbe9011b66e1bccd7f44d189d121190d9 100644
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -373,7 +373,7 @@ mp_obj_t rt_load_name(qstr qstr) {
         if (elem == NULL) {
             elem = mp_map_lookup(&map_builtins, MP_OBJ_NEW_QSTR(qstr), MP_MAP_LOOKUP);
             if (elem == NULL) {
-                nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_NameError, "name '%s' is not defined", qstr_str(qstr)));
+                nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_NameError, "name '%s' is not defined", qstr_str(qstr)));
             }
         }
     }
@@ -387,7 +387,7 @@ mp_obj_t rt_load_global(qstr qstr) {
     if (elem == NULL) {
         elem = mp_map_lookup(&map_builtins, MP_OBJ_NEW_QSTR(qstr), MP_MAP_LOOKUP);
         if (elem == NULL) {
-            nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_NameError, "name '%s' is not defined", qstr_str(qstr)));
+            nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_NameError, "name '%s' is not defined", qstr_str(qstr)));
         }
     }
     return elem->value;
@@ -447,7 +447,7 @@ mp_obj_t rt_unary_op(int op, mp_obj_t arg) {
             }
         }
         // TODO specify in error message what the operator is
-        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "bad operand type for unary operator: '%s'", o->type->name));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "bad operand type for unary operator: '%s'", o->type->name));
     }
 }
 
@@ -532,7 +532,7 @@ mp_obj_t rt_binary_op(int op, mp_obj_t lhs, mp_obj_t rhs) {
     }
 
     // TODO specify in error message what the operator is
-    nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "unsupported operand type for binary operator: '%s'", mp_obj_get_type_str(lhs)));
+    nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "unsupported operand type for binary operator: '%s'", mp_obj_get_type_str(lhs)));
 }
 
 mp_obj_t rt_compare_op(int op, mp_obj_t lhs, mp_obj_t rhs) {
@@ -687,7 +687,7 @@ mp_obj_t rt_call_function_n(mp_obj_t fun_in, int n_args, const mp_obj_t *args) {
         if (fun->type->call_n != NULL) {
             return fun->type->call_n(fun_in, n_args, args);
         } else {
-            nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "'%s' object is not callable", fun->type->name));
+            nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "'%s' object is not callable", fun->type->name));
         }
     }
 }
@@ -705,7 +705,7 @@ mp_obj_t rt_call_function_n_kw(mp_obj_t fun_in, uint n_args, uint n_kw, const mp
         if (fun->type->call_n_kw != NULL) {
             return fun->type->call_n_kw(fun_in, n_args, n_kw, args);
         } else {
-            nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "'%s' object is not callable", fun->type->name));
+            nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "'%s' object is not callable", fun->type->name));
         }
     }
 }
@@ -754,14 +754,14 @@ void rt_unpack_sequence(mp_obj_t seq_in, uint num, mp_obj_t *items) {
             mp_obj_list_get(seq_in, &seq_len, &seq_items);
         }
         if (seq_len < num) {
-            nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_ValueError, "need more than %d values to unpack", (void*)(machine_uint_t)seq_len));
+            nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_ValueError, "need more than %d values to unpack", (void*)(machine_uint_t)seq_len));
         } else if (seq_len > num) {
-            nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_ValueError, "too many values to unpack (expected %d)", (void*)(machine_uint_t)num));
+            nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_ValueError, "too many values to unpack (expected %d)", (void*)(machine_uint_t)num));
         }
         memcpy(items, seq_items, num * sizeof(mp_obj_t));
     } else {
         // TODO call rt_getiter and extract via rt_iternext
-        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "'%s' object is not iterable", mp_obj_get_type_str(seq_in)));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "'%s' object is not iterable", mp_obj_get_type_str(seq_in)));
     }
 }
 
@@ -807,7 +807,7 @@ mp_obj_t rt_load_attr(mp_obj_t base, qstr attr) {
     }
 
 no_attr:
-    nlr_jump(mp_obj_new_exception_msg_2_args(MP_QSTR_AttributeError, "'%s' object has no attribute '%s'", mp_obj_get_type_str(base), qstr_str(attr)));
+    nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_AttributeError, "'%s' object has no attribute '%s'", mp_obj_get_type_str(base), qstr_str(attr)));
 }
 
 void rt_load_method(mp_obj_t base, qstr attr, mp_obj_t *dest) {
@@ -852,7 +852,7 @@ void rt_store_attr(mp_obj_t base, qstr attr, mp_obj_t value) {
         mp_map_t *globals = mp_obj_module_get_globals(base);
         mp_map_lookup(globals, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP_ADD_IF_NOT_FOUND)->value = value;
     } else {
-        nlr_jump(mp_obj_new_exception_msg_2_args(MP_QSTR_AttributeError, "'%s' object has no attribute '%s'", mp_obj_get_type_str(base), qstr_str(attr)));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_AttributeError, "'%s' object has no attribute '%s'", mp_obj_get_type_str(base), qstr_str(attr)));
     }
 }
 
@@ -877,7 +877,7 @@ mp_obj_t rt_getiter(mp_obj_t o_in) {
         if (o->type->getiter != NULL) {
             return o->type->getiter(o_in);
         } else {
-            nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "'%s' object is not iterable", o->type->name));
+            nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "'%s' object is not iterable", o->type->name));
         }
     }
 }
@@ -890,7 +890,7 @@ mp_obj_t rt_iternext(mp_obj_t o_in) {
         if (o->type->iternext != NULL) {
             return o->type->iternext(o_in);
         } else {
-            nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_TypeError, "'%s' object is not an iterator", o->type->name));
+            nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_TypeError, "'%s' object is not an iterator", o->type->name));
         }
     }
 }
diff --git a/py/stream.c b/py/stream.c
index 82a5aac10b349b88f26bc24ebc6c18b6f2d25754..f883efcd256fb76d342a007ba6fdea9072f98022 100644
--- a/py/stream.c
+++ b/py/stream.c
@@ -23,7 +23,7 @@ static mp_obj_t stream_read(mp_obj_t self_in, mp_obj_t arg) {
     int error;
     machine_int_t out_sz = o->type->stream_p.read(self_in, buf, sz, &error);
     if (out_sz == -1) {
-        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_OSError, "[Errno %d]", (const char *)error));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_OSError, "[Errno %d]", error));
     } else {
         buf[out_sz] = 0;
         return mp_obj_new_str(qstr_from_str_take(buf, /*out_sz,*/ sz + 1));
@@ -42,7 +42,7 @@ static mp_obj_t stream_write(mp_obj_t self_in, mp_obj_t arg) {
     int error;
     machine_int_t out_sz = o->type->stream_p.write(self_in, buf, sz, &error);
     if (out_sz == -1) {
-        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_OSError, "[Errno %d]", (const char *)error));
+        nlr_jump(mp_obj_new_exception_msg_varg(MP_QSTR_OSError, "[Errno %d]", error));
     } else {
         // http://docs.python.org/3/library/io.html#io.RawIOBase.write
         // "None is returned if the raw stream is set not to block and no single byte could be readily written to it."
diff --git a/unix/file.c b/unix/file.c
index b81111afdd146da12b8fb952b9effdf0079223cc..398aac0759f60234bdd1e719b8aef32aa62e0093 100644
--- a/unix/file.c
+++ b/unix/file.c
@@ -83,7 +83,7 @@ static mp_obj_t fdfile_make_new(mp_obj_t type_in, int n_args, const mp_obj_t *ar
 
     o->fd = open(fname, mode, 0644);
     if (o->fd == -1) {
-        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_OSError, "[Errno %d]", (const char *)errno));
+        nlr_jump(mp_obj_new_exception_msg_1_arg(MP_QSTR_OSError, "[Errno %d]", (const char *)(machine_int_t)errno));
     }
     return o;
 }