diff --git a/py/builtin.c b/py/builtin.c
index 145bc65b14c10ced19e973e80532e4e75590ca9a..2bf12e4928c85c680ba4829d802a72b0c578dd9e 100644
--- a/py/builtin.c
+++ b/py/builtin.c
@@ -400,9 +400,13 @@ STATIC mp_obj_t mp_builtin_id(mp_obj_t o_in) {
 
 MP_DEFINE_CONST_FUN_OBJ_1(mp_builtin_id_obj, mp_builtin_id);
 
-STATIC mp_obj_t mp_builtin_getattr(mp_obj_t o_in, mp_obj_t attr) {
-    assert(MP_OBJ_IS_QSTR(attr));
-    return mp_load_attr(o_in, MP_OBJ_QSTR_VALUE(attr));
+STATIC mp_obj_t mp_builtin_getattr(uint n_args, const mp_obj_t *args) {
+    assert(MP_OBJ_IS_QSTR(args[1]));
+    mp_obj_t defval = MP_OBJ_NULL;
+    if (n_args > 2) {
+        defval = args[2];
+    }
+    return mp_load_attr_default(args[0], MP_OBJ_QSTR_VALUE(args[1]), defval);
 }
 
-MP_DEFINE_CONST_FUN_OBJ_2(mp_builtin_getattr_obj, mp_builtin_getattr);
+MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mp_builtin_getattr_obj, 2, 3, mp_builtin_getattr);
diff --git a/py/runtime.c b/py/runtime.c
index 773f998d34caccf56ca492e437a7e7fbd6541589..df7de3e54f9a5e876ca69aa8b861158b1afc1a64 100644
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -689,12 +689,14 @@ too_long:
     nlr_jump(mp_obj_new_exception_msg_varg(&mp_type_ValueError, "too many values to unpack (expected %d)", num));
 }
 
-mp_obj_t mp_load_attr(mp_obj_t base, qstr attr) {
+mp_obj_t mp_load_attr_default(mp_obj_t base, qstr attr, mp_obj_t defval) {
     DEBUG_OP_printf("load attr %p.%s\n", base, qstr_str(attr));
-    // use load_method
     mp_obj_t dest[2];
-    mp_load_method(base, attr, dest);
-    if (dest[1] == MP_OBJ_NULL) {
+    // use load_method, raising or not raising exception
+    ((defval == MP_OBJ_NULL) ? mp_load_method : mp_load_method_maybe)(base, attr, dest);
+    if (dest[0] == MP_OBJ_NULL) {
+        return defval;
+    } else if (dest[1] == MP_OBJ_NULL) {
         // load_method returned just a normal attribute
         return dest[0];
     } else {
@@ -703,6 +705,10 @@ mp_obj_t mp_load_attr(mp_obj_t base, qstr attr) {
     }
 }
 
+mp_obj_t mp_load_attr(mp_obj_t base, qstr attr) {
+    return mp_load_attr_default(base, attr, MP_OBJ_NULL);
+}
+
 // no attribute found, returns:     dest[0] == MP_OBJ_NULL, dest[1] == MP_OBJ_NULL
 // normal attribute found, returns: dest[0] == <attribute>, dest[1] == MP_OBJ_NULL
 // method attribute found, returns: dest[0] == <method>,    dest[1] == <self>
diff --git a/py/runtime.h b/py/runtime.h
index b233d23b4a7c4576898a86828321a0a455167d15..22e7781be7b042e3baf730fd648a0e978ffd0ec9 100644
--- a/py/runtime.h
+++ b/py/runtime.h
@@ -45,6 +45,7 @@ mp_obj_t mp_call_method_n_kw_var(bool have_self, uint n_args_n_kw, const mp_obj_
 void mp_unpack_sequence(mp_obj_t seq, uint num, mp_obj_t *items);
 mp_obj_t mp_store_map(mp_obj_t map, mp_obj_t key, mp_obj_t value);
 mp_obj_t mp_load_attr(mp_obj_t base, qstr attr);
+mp_obj_t mp_load_attr_default(mp_obj_t base, qstr attr, mp_obj_t defval);
 void mp_load_method(mp_obj_t base, qstr attr, mp_obj_t *dest);
 void mp_load_method_maybe(mp_obj_t base, qstr attr, mp_obj_t *dest);
 void mp_store_attr(mp_obj_t base, qstr attr, mp_obj_t val);
diff --git a/tests/basics/getattr1.py b/tests/basics/getattr1.py
index 7b7e073922431d8810cf0a344e5d5bd37080080c..9a96154ca5e56034c4fa12f7d88943629596769d 100644
--- a/tests/basics/getattr1.py
+++ b/tests/basics/getattr1.py
@@ -13,3 +13,5 @@ a = A()
 print(getattr(a, "var"))
 print(getattr(a, "var2"))
 print(getattr(a, "meth")(5))
+print(getattr(a, "_none_such", 123))
+print(getattr(list, "foo", 456))