diff --git a/py/objtype.c b/py/objtype.c
index 06bc803ebe0a354e08a4e5e60c6ced2a7da3c040..11200c91352e2806cd13b031480b2e562304af76 100644
--- a/py/objtype.c
+++ b/py/objtype.c
@@ -16,6 +16,7 @@
 typedef struct _mp_obj_class_t {
     mp_obj_base_t base;
     mp_map_t members;
+    // TODO maybe cache __getattr__ and __setattr__ for efficient lookup of them
 } mp_obj_class_t;
 
 STATIC mp_obj_t mp_obj_new_class(mp_obj_t class) {
@@ -225,6 +226,19 @@ STATIC void class_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
         } else {
             // class member is a value, so just return that value
             dest[0] = member;
+        }
+        return;
+    }
+
+    // try __getattr__
+    if (attr != MP_QSTR___getattr__) {
+        mp_obj_t dest2[3];
+        mp_load_method_maybe(self_in, MP_QSTR___getattr__, dest2);
+        if (dest2[0] != MP_OBJ_NULL) {
+            // __getattr__ exists, call it and return its result
+            // XXX if this fails to load the requested attr, should we catch the attribute error and return silently?
+            dest2[2] = MP_OBJ_NEW_QSTR(attr);
+            dest[0] = mp_call_method_n_kw(1, 0, dest2);
             return;
         }
     }
diff --git a/py/qstrdefs.h b/py/qstrdefs.h
index 457043938b981e17d5c9a103538614805e72b767..5c29ba1de0a2adbadfd685fb207ec339b143fcb9 100644
--- a/py/qstrdefs.h
+++ b/py/qstrdefs.h
@@ -26,6 +26,7 @@ Q(__add__)
 Q(__sub__)
 Q(__repr__)
 Q(__str__)
+Q(__getattr__)
 
 Q(micropython)
 Q(byte_code)
diff --git a/py/runtime.c b/py/runtime.c
index f7a55545afb9f967cdf1cc2d49f1d2ee37bd6b53..0c75d4cd3120e9f2a41cd489b190152b25a7cd67 100644
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -701,7 +701,7 @@ mp_obj_t mp_load_attr(mp_obj_t base, qstr attr) {
 // 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>
-STATIC void mp_load_method_maybe(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) {
     // clear output to indicate no attribute/method found yet
     dest[0] = MP_OBJ_NULL;
     dest[1] = MP_OBJ_NULL;
@@ -709,48 +709,45 @@ STATIC void mp_load_method_maybe(mp_obj_t base, qstr attr, mp_obj_t *dest) {
     // get the type
     mp_obj_type_t *type = mp_obj_get_type(base);
 
-    // if this type can do its own load, then call it
-    if (type->load_attr != NULL) {
-        type->load_attr(base, attr, dest);
-    }
-
-    // if nothing found yet, look for built-in and generic names
-    if (dest[0] == MP_OBJ_NULL) {
+    // look for built-in names
+    if (0) {
 #if MICROPY_CPYTHON_COMPAT
-        if (attr == MP_QSTR___class__) {
-            // a.__class__ is equivalent to type(a)
-            dest[0] = type;
-        } else
+    } else if (attr == MP_QSTR___class__) {
+        // a.__class__ is equivalent to type(a)
+        dest[0] = type;
 #endif
-        if (attr == MP_QSTR___next__ && type->iternext != NULL) {
-            dest[0] = (mp_obj_t)&mp_builtin_next_obj;
-            dest[1] = base;
-        } else if (type->load_attr == NULL) {
-            // generic method lookup if type didn't provide a specific one
-            // this is a lookup in the object (ie not class or type)
-            if (type->locals_dict != NULL) {
-                assert(MP_OBJ_IS_TYPE(type->locals_dict, &mp_type_dict)); // Micro Python restriction, for now
-                mp_map_t *locals_map = mp_obj_dict_get_map(type->locals_dict);
-                mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP);
-                if (elem != NULL) {
-                    // check if the methods are functions, static or class methods
-                    // see http://docs.python.org/3.3/howto/descriptor.html
-                    if (MP_OBJ_IS_TYPE(elem->value, &mp_type_staticmethod)) {
-                        // return just the function
-                        dest[0] = ((mp_obj_static_class_method_t*)elem->value)->fun;
-                    } else if (MP_OBJ_IS_TYPE(elem->value, &mp_type_classmethod)) {
-                        // return a bound method, with self being the type of this object
-                        dest[0] = ((mp_obj_static_class_method_t*)elem->value)->fun;
-                        dest[1] = mp_obj_get_type(base);
-                    } else if (mp_obj_is_callable(elem->value)) {
-                        // return a bound method, with self being this object
-                        dest[0] = elem->value;
-                        dest[1] = base;
-                    } else {
-                        // class member is a value, so just return that value
-                        dest[0] = elem->value;
-                    }
-                }
+
+    } else if (attr == MP_QSTR___next__ && type->iternext != NULL) {
+        dest[0] = (mp_obj_t)&mp_builtin_next_obj;
+        dest[1] = base;
+
+    } else if (type->load_attr != NULL) {
+        // this type can do its own load, so call it
+        type->load_attr(base, attr, dest);
+
+    } else if (type->locals_dict != NULL) {
+        // generic method lookup
+        // this is a lookup in the object (ie not class or type)
+        assert(MP_OBJ_IS_TYPE(type->locals_dict, &mp_type_dict)); // Micro Python restriction, for now
+        mp_map_t *locals_map = mp_obj_dict_get_map(type->locals_dict);
+        mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP);
+        if (elem != NULL) {
+            // check if the methods are functions, static or class methods
+            // see http://docs.python.org/3.3/howto/descriptor.html
+            if (MP_OBJ_IS_TYPE(elem->value, &mp_type_staticmethod)) {
+                // return just the function
+                dest[0] = ((mp_obj_static_class_method_t*)elem->value)->fun;
+            } else if (MP_OBJ_IS_TYPE(elem->value, &mp_type_classmethod)) {
+                // return a bound method, with self being the type of this object
+                dest[0] = ((mp_obj_static_class_method_t*)elem->value)->fun;
+                dest[1] = mp_obj_get_type(base);
+            } else if (mp_obj_is_callable(elem->value)) {
+                // return a bound method, with self being this object
+                dest[0] = elem->value;
+                dest[1] = base;
+            } else {
+                // class member is a value, so just return that value
+                dest[0] = elem->value;
             }
         }
     }
diff --git a/py/runtime.h b/py/runtime.h
index b817d61aecdb1b9c89571611606acb3113556773..b233d23b4a7c4576898a86828321a0a455167d15 100644
--- a/py/runtime.h
+++ b/py/runtime.h
@@ -46,6 +46,7 @@ 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);
 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);
 void mp_store_subscr(mp_obj_t base, mp_obj_t index, mp_obj_t val);
 
diff --git a/tests/basics/getattr.py b/tests/basics/getattr.py
new file mode 100644
index 0000000000000000000000000000000000000000..a021e38fb0aefdbde6236ec715c2e6743a3eb378
--- /dev/null
+++ b/tests/basics/getattr.py
@@ -0,0 +1,11 @@
+# test __getattr__
+
+class A:
+    def __init__(self, d):
+        self.d = d
+
+    def __getattr__(self, attr):
+        return self.d[attr]
+
+a = A({'a':1, 'b':2})
+print(a.a, a.b)