diff --git a/py/objtype.c b/py/objtype.c
index 9eb228ce3fe66bf322f9fd1511b61aa9b6c3a7ac..6480a99f6c89269330181e3c3221fb0cc25a376a 100644
--- a/py/objtype.c
+++ b/py/objtype.c
@@ -78,6 +78,21 @@ STATIC mp_obj_t mp_obj_class_lookup(const mp_obj_type_t *type, qstr attr) {
 }
 
 STATIC void class_print(void (*print)(void *env, const char *fmt, ...), void *env, mp_obj_t self_in, mp_print_kind_t kind) {
+    mp_obj_class_t *self = self_in;
+    qstr meth = (kind == PRINT_STR) ? MP_QSTR___str__ : MP_QSTR___repr__;
+    mp_obj_t member = mp_obj_class_lookup(self->base.type, meth);
+    if (member == MP_OBJ_NULL && kind == PRINT_STR) {
+        // If there's no __str__, fall back to __repr__
+        member = mp_obj_class_lookup(self->base.type, MP_QSTR___repr__);
+    }
+
+    if (member != MP_OBJ_NULL) {
+        mp_obj_t r = rt_call_function_1(member, self_in);
+        mp_obj_print_helper(print, env, r, PRINT_STR);
+        return;
+    }
+
+    // TODO: CPython prints fully-qualified type name
     print(env, "<%s object at %p>", mp_obj_get_type_str(self_in), self_in);
 }
 
diff --git a/py/qstrdefs.h b/py/qstrdefs.h
index 2678526cda2b3a4625aac1cdb4227e7fda570275..8f6cc1167fea8a7f1c9f76e89dc7ff99d69249b8 100644
--- a/py/qstrdefs.h
+++ b/py/qstrdefs.h
@@ -21,6 +21,8 @@ Q(__getitem__)
 Q(__setitem__)
 Q(__add__)
 Q(__sub__)
+Q(__repr__)
+Q(__str__)
 
 Q(micropython)
 Q(byte_code)
diff --git a/tests/basics/class_str.py b/tests/basics/class_str.py
new file mode 100644
index 0000000000000000000000000000000000000000..46dae2b5e6bc3d3c27f5b5506fde50d72ed3e58e
--- /dev/null
+++ b/tests/basics/class_str.py
@@ -0,0 +1,44 @@
+class C1:
+    def __init__(self, value):
+        self.value = value
+
+    def __str__(self):
+        return "str<C1 {}>".format(self.value)
+
+class C2:
+    def __init__(self, value):
+        self.value = value
+
+    def __repr__(self):
+        return "repr<C2 {}>".format(self.value)
+
+class C3:
+    def __init__(self, value):
+        self.value = value
+
+    def __str__(self):
+        return "str<C3 {}>".format(self.value)
+
+    def __repr__(self):
+        return "repr<C3 {}>".format(self.value)
+
+c1 = C1(1)
+print(c1)
+
+c2 = C2(2)
+print(c2)
+
+s11 = str(c1)
+print(s11)
+# This will use builtin repr(), which uses id(), which is of course different
+# between CPython and MicroPython
+s12 = repr(c1)
+print("C1 object at" in s12)
+
+s21 = str(c2)
+print(s21)
+s22 = repr(c2)
+print(s22)
+
+c3 = C3(1)
+print(c3)