Repository: RustPython/RustPython (language: rust)

Issue:
[Draft] Make builtin method type same as that of builtin function
Fixes #3100 



Discussion / hints:
I missed that __qualname__ of PyClassMethod must be considered too. (it is the cause of failing tests) Turning this PR to draft now and will look into PyClassMethod.

Fix patch:
diff --git a/derive-impl/src/pyclass.rs b/derive-impl/src/pyclass.rs
index 2b8689669d..3f78811073 100644
--- a/derive-impl/src/pyclass.rs
+++ b/derive-impl/src/pyclass.rs
@@ -6,7 +6,7 @@ use crate::util::{
 };
 use proc_macro2::{Span, TokenStream};
 use quote::{quote, quote_spanned, ToTokens};
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 use std::str::FromStr;
 use syn::{
     parse_quote, spanned::Spanned, Attribute, AttributeArgs, Ident, Item, Meta, NestedMeta, Result,
@@ -62,7 +62,8 @@ impl FromStr for AttrName {
 
 #[derive(Default)]
 struct ImplContext {
-    impl_extend_items: ItemNursery,
+    attribute_items: ItemNursery,
+    method_items: MethodNursery,
     getset_items: GetSetNursery,
     member_items: MemberNursery,
     extend_slots_items: ItemNursery,
@@ -92,6 +93,7 @@ fn extract_items_into_context<'a, Item>(
         });
         context.errors.ok_or_push(r);
     }
+    context.errors.ok_or_push(context.method_items.validate());
     context.errors.ok_or_push(context.getset_items.validate());
     context.errors.ok_or_push(context.member_items.validate());
 }
@@ -157,18 +159,28 @@ pub(crate) fn impl_pyclass_impl(attr: AttributeArgs, item: Item) -> Result<Token
 
             let ExtractedImplAttrs {
                 payload: attr_payload,
-                with_impl,
                 flags,
+                with_impl,
+                with_method_defs,
                 with_slots,
             } = extract_impl_attrs(attr, &impl_ty)?;
             let payload_ty = attr_payload.unwrap_or(payload_guess);
+            let method_def = &context.method_items;
             let getset_impl = &context.getset_items;
             let member_impl = &context.member_items;
-            let extend_impl = context.impl_extend_items.validate()?;
+            let extend_impl = context.attribute_items.validate()?;
             let slots_impl = context.extend_slots_items.validate()?;
             let class_extensions = &context.class_extensions;
 
             let extra_methods = iter_chain![
+                parse_quote! {
+                    #[allow(clippy::ptr_arg)]
+                    fn __extend_method_def(
+                        method_defs: &mut Vec<::rustpython_vm::function::PyMethodDef>,
+                    ) {
+                        #method_def
+                    }
+                },
                 parse_quote! {
                     fn __extend_py_class(
                         ctx: &::rustpython_vm::Context,
@@ -202,6 +214,14 @@ pub(crate) fn impl_pyclass_impl(attr: AttributeArgs, item: Item) -> Result<Token
                             #with_impl
                         }
 
+                        #[allow(clippy::ptr_arg)]
+                        fn impl_extend_method_def(
+                            method_defs: &mut Vec<::rustpython_vm::function::PyMethodDef>,
+                        ) {
+                            #impl_ty::__extend_method_def(method_defs);
+                            #with_method_defs
+                        }
+
                         fn extend_slots(slots: &mut ::rustpython_vm::types::PyTypeSlots) {
                             #impl_ty::__extend_slots(slots);
                             #with_slots
@@ -235,9 +255,10 @@ pub(crate) fn impl_pyclass_impl(attr: AttributeArgs, item: Item) -> Result<Token
                 ..
             } = extract_impl_attrs(attr, &trai.ident)?;
 
+            let method_def = &context.method_items;
             let getset_impl = &context.getset_items;
             let member_impl = &context.member_items;
-            let extend_impl = &context.impl_extend_items.validate()?;
+            let extend_impl = &context.attribute_items.validate()?;
             let slots_impl = &context.extend_slots_items.validate()?;
             let class_extensions = &context.class_extensions;
             let call_extend_slots = if has_extend_slots {
@@ -248,6 +269,14 @@ pub(crate) fn impl_pyclass_impl(attr: AttributeArgs, item: Item) -> Result<Token
                 quote! {}
             };
             let extra_methods = iter_chain![
+                parse_quote! {
+                    #[allow(clippy::ptr_arg)]
+                    fn __extend_method_def(
+                        method_defs: &mut Vec<::rustpython_vm::function::PyMethodDef>,
+                    ) {
+                        #method_def
+                    }
+                },
                 parse_quote! {
                     fn __extend_py_class(
                         ctx: &::rustpython_vm::Context,
@@ -732,51 +761,14 @@ where
         let py_name = item_meta.method_name()?;
         let sig_doc = text_signature(func.sig(), &py_name);
 
-        let tokens = {
-            let doc = args.attrs.doc().map_or_else(TokenStream::new, |mut doc| {
-                doc = format_doc(&sig_doc, &doc);
-                quote!(.with_doc(#doc.to_owned(), ctx))
-            });
-            let build_func = match self.inner.attr_name {
-                AttrName::Method => quote!(.build_method(ctx, class)),
-                AttrName::ClassMethod => quote!(.build_classmethod(ctx, class)),
-                AttrName::StaticMethod => quote!(.build_staticmethod(ctx, class)),
-                other => unreachable!(
-                    "Only 'method', 'classmethod' and 'staticmethod' are supported, got {:?}",
-                    other
-                ),
-            };
-            if py_name.starts_with("__") && py_name.ends_with("__") {
-                let name_ident = Ident::new(&py_name, ident.span());
-                quote_spanned! { ident.span() =>
-                    class.set_attr(
-                        ctx.names.#name_ident,
-                        ctx.make_func_def(ctx.intern_str(#py_name), Self::#ident)
-                            #doc
-                            #build_func
-                        .into(),
-                    );
-                }
-            } else {
-                quote_spanned! { ident.span() =>
-                    class.set_str_attr(
-                        #py_name,
-                        ctx.make_func_def(ctx.intern_str(#py_name), Self::#ident)
-                            #doc
-                            #build_func,
-                        ctx,
-                    );
-                }
-            }
-        };
-
-        args.context.impl_extend_items.add_item(
-            ident.clone(),
-            vec![py_name],
-            args.cfgs.to_vec(),
-            tokens,
-            5,
-        )?;
+        let doc = args.attrs.doc().map(|doc| format_doc(&sig_doc, &doc));
+        args.context.method_items.add_item(MethodNurseryItem {
+            py_name,
+            cfgs: args.cfgs.to_vec(),
+            ident: ident.to_owned(),
+            doc,
+            attr_name: self.inner.attr_name,
+        });
         Ok(())
     }
 }
@@ -898,7 +890,7 @@ where
             };
 
         args.context
-            .impl_extend_items
+            .attribute_items
             .add_item(ident.clone(), vec![py_name], cfgs, tokens, 1)?;
 
         Ok(())
@@ -960,6 +952,78 @@ where
     }
 }
 
+#[derive(Default)]
+struct MethodNursery {
+    items: Vec<MethodNurseryItem>,
+}
+
+struct MethodNurseryItem {
+    py_name: String,
+    cfgs: Vec<Attribute>,
+    ident: Ident,
+    doc: Option<String>,
+    attr_name: AttrName,
+}
+
+impl MethodNursery {
+    fn add_item(&mut self, item: MethodNurseryItem) {
+        self.items.push(item);
+    }
+
+    fn validate(&mut self) -> Result<()> {
+        let mut name_set = HashSet::new();
+        for item in &self.items {
+            if !name_set.insert((&item.py_name, &item.cfgs)) {
+                bail_span!(item.ident, "duplicate method name `{}`", item.py_name);
+            }
+        }
+        Ok(())
+    }
+}
+
+impl ToTokens for MethodNursery {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        for item in &self.items {
+            let py_name = &item.py_name;
+            let ident = &item.ident;
+            let cfgs = &item.cfgs;
+            let doc = if let Some(doc) = item.doc.as_ref() {
+                quote! { Some(#doc) }
+            } else {
+                quote! { None }
+            };
+            let flags = match &item.attr_name {
+                AttrName::Method => {
+                    quote! { rustpython_vm::function::PyMethodFlags::METHOD }
+                }
+                AttrName::ClassMethod => {
+                    quote! { rustpython_vm::function::PyMethodFlags::CLASS }
+                }
+                AttrName::StaticMethod => {
+                    quote! { rustpython_vm::function::PyMethodFlags::STATIC }
+                }
+                _ => unreachable!(),
+            };
+            // TODO: intern
+            // let py_name = if py_name.starts_with("__") && py_name.ends_with("__") {
+            //     let name_ident = Ident::new(&py_name, ident.span());
+            //     quote_spanned! { ident.span() => ctx.names.#name_ident }
+            // } else {
+            //     quote_spanned! { ident.span() => #py_name }
+            // };
+            tokens.extend(quote! {
+                #(#cfgs)*
+                method_defs.push(rustpython_vm::function::PyMethodDef {
+                    name: #py_name,
+                    func: rustpython_vm::function::IntoPyNativeFn::into_func(Self::#ident),
+                    flags: #flags,
+                    doc: #doc,
+                });
+            });
+        }
+    }
+}
+
 #[derive(Default)]
 #[allow(clippy::type_complexity)]
 struct GetSetNursery {
@@ -1367,13 +1431,15 @@ impl MemberItemMeta {
 
 struct ExtractedImplAttrs {
     payload: Option<Ident>,
+    flags: TokenStream,
     with_impl: TokenStream,
+    with_method_defs: TokenStream,
     with_slots: TokenStream,
-    flags: TokenStream,
 }
 
 fn extract_impl_attrs(attr: AttributeArgs, item: &Ident) -> Result<ExtractedImplAttrs> {
     let mut withs = Vec::new();
+    let mut with_method_defs = Vec::new();
     let mut with_slots = Vec::new();
     let mut flags = vec![quote! {
         {
@@ -1396,16 +1462,18 @@ fn extract_impl_attrs(attr: AttributeArgs, item: &Ident) -> Result<ExtractedImpl
                         let NestedMeta::Meta(Meta::Path(path)) = meta else {
                             bail_span!(meta, "#[pyclass(with(...))] arguments should be paths")
                         };
-                        let (extend_class, extend_slots) =
+                        let (extend_class, extend_method_def, extend_slots) =
                             if path.is_ident("PyRef") || path.is_ident("Py") {
                                 // special handling for PyRef
                                 (
                                     quote!(#path::<Self>::__extend_py_class),
+                                    quote!(#path::<Self>::__extend_method_def),
                                     quote!(#path::<Self>::__extend_slots),
                                 )
                             } else {
                                 (
                                     quote!(<Self as #path>::__extend_py_class),
+                                    quote!(<Self as #path>::__extend_method_def),
                                     quote!(<Self as #path>::__extend_slots),
                                 )
                             };
@@ -1413,6 +1481,9 @@ fn extract_impl_attrs(attr: AttributeArgs, item: &Ident) -> Result<ExtractedImpl
                         withs.push(quote_spanned! { path.span() =>
                             #extend_class(ctx, class);
                         });
+                        with_method_defs.push(quote_spanned! { path.span() =>
+                            #extend_method_def(method_defs);
+                        });
                         with_slots.push(quote_spanned! { item_span =>
                             #extend_slots(slots);
                         });
@@ -1450,11 +1521,14 @@ fn extract_impl_attrs(attr: AttributeArgs, item: &Ident) -> Result<ExtractedImpl
 
     Ok(ExtractedImplAttrs {
         payload,
+        flags: quote! {
+            #(#flags)*
+        },
         with_impl: quote! {
             #(#withs)*
         },
-        flags: quote! {
-            #(#flags)*
+        with_method_defs: quote! {
+            #(#with_method_defs)*
         },
         with_slots: quote! {
             #(#with_slots)*
diff --git a/derive-impl/src/pymodule.rs b/derive-impl/src/pymodule.rs
index 329c1dcd8d..fe3581279e 100644
--- a/derive-impl/src/pymodule.rs
+++ b/derive-impl/src/pymodule.rs
@@ -4,7 +4,7 @@ use crate::util::{
     AttributeExt, ClassItemMeta, ContentItem, ContentItemInner, ErrorVec, ItemMeta, ItemNursery,
     ModuleItemMeta, SimpleItemMeta, ALL_ALLOWED_NAMES,
 };
-use proc_macro2::{Span, TokenStream};
+use proc_macro2::TokenStream;
 use quote::{quote, quote_spanned, ToTokens};
 use std::{collections::HashSet, str::FromStr};
 use syn::{parse_quote, spanned::Spanned, Attribute, AttributeArgs, Ident, Item, Result};
@@ -122,9 +122,16 @@ pub fn impl_pymodule(attr: AttributeArgs, module_item: Item) -> Result<TokenStre
                     ctx: &::rustpython_vm::Context,
                 ) -> &'static ::rustpython_vm::builtins::PyModuleDef {
                     DEF.get_or_init(|| {
+                        #[allow(clippy::ptr_arg)]
+                        let method_defs = {
+                            let mut method_defs = Vec::new();
+                            extend_method_def(ctx, &mut method_defs);
+                            method_defs
+                        };
                         let mut def = ::rustpython_vm::builtins::PyModuleDef {
                             name: ctx.intern_str(MODULE_NAME),
                             doc: DOC.map(|doc| ctx.intern_str(doc)),
+                            methods: Box::leak(method_defs.into_boxed_slice()),
                             slots: Default::default(),
                         };
                         def.slots.exec = Some(extend_module);
@@ -160,6 +167,18 @@ pub fn impl_pymodule(attr: AttributeArgs, module_item: Item) -> Result<TokenStre
                 pub(crate) static DEF: ::rustpython_vm::builtins::PyModuleDef;
             }
         },
+        parse_quote! {
+            #[allow(clippy::ptr_arg)]
+            pub(crate) fn extend_method_def(
+                ctx: &::rustpython_vm::Context,
+                method_defs: &mut Vec<::rustpython_vm::function::PyMethodDef>,
+            ) {
+                #(
+                    super::#withs::extend_method_def(ctx, method_defs);
+                )*
+                #function_items
+            }
+        },
         parse_quote! {
             pub(crate) fn __init_attributes(
                 vm: &::rustpython_vm::VirtualMachine,
@@ -177,23 +196,10 @@ pub fn impl_pymodule(attr: AttributeArgs, module_item: Item) -> Result<TokenStre
                 vm: &::rustpython_vm::VirtualMachine,
                 module: &::rustpython_vm::Py<::rustpython_vm::builtins::PyModule>,
             ) {
-                __init_methods(vm, module);
+                module.__init_methods(vm).unwrap();
                 __init_attributes(vm, module);
             }
         },
-        parse_quote! {
-            // TODO: remove once PyMethodDef done
-            pub(crate) fn __init_methods(
-                vm: &::rustpython_vm::VirtualMachine,
-                module: &::rustpython_vm::Py<::rustpython_vm::builtins::PyModule>,
-            ) {
-                #(
-                    super::#withs::__init_methods(vm, module);
-                )*
-                let ctx = &vm.ctx;
-                #function_items
-            }
-        },
         parse_quote! {
             pub(crate) fn __init_dict(
                 vm: &::rustpython_vm::VirtualMachine,
@@ -330,9 +336,7 @@ struct FunctionNurseryItem {
     py_names: Vec<String>,
     cfgs: Vec<Attribute>,
     ident: Ident,
-    #[allow(dead_code)]
     doc: String,
-    tokens: TokenStream,
 }
 
 impl FunctionNursery {
@@ -358,11 +362,23 @@ struct ValidatedFunctionNursery(FunctionNursery);
 impl ToTokens for ValidatedFunctionNursery {
     fn to_tokens(&self, tokens: &mut TokenStream) {
         for item in &self.0.items {
+            let ident = &item.ident;
             let cfgs = &item.cfgs;
-            let item_tokens = &item.tokens;
+            let py_names = &item.py_names;
+            let doc = &item.doc;
+            let flags = quote! { rustpython_vm::function::PyMethodFlags::empty() };
+
             tokens.extend(quote! {
                 #(#cfgs)*
-                #item_tokens
+                {
+                    let doc = Some(#doc);
+                    #(method_defs.push(rustpython_vm::function::PyMethodDef::new(
+                        (#py_names),
+                        #ident,
+                        #flags,
+                        doc,
+                    ));)*
+                }
             });
         }
     }
@@ -438,38 +454,23 @@ impl ModuleItem for FunctionItem {
         let py_name = item_meta.simple_name()?;
         let sig_doc = text_signature(func.sig(), &py_name);
 
-        let (tokens, py_names, doc) = {
-            let module = args.module_name();
-            let doc = args.attrs.doc().or_else(|| {
-                crate::doc::Database::shared()
-                    .try_module_item(module, &py_name)
-                    .ok() // TODO: doc must exist at least one of code or CPython
-                    .flatten()
-                    .map(str::to_owned)
-            });
-            let doc = if let Some(doc) = doc {
-                format_doc(&sig_doc, &doc)
-            } else {
-                sig_doc
-            };
-            let with_doc = quote!(.with_doc(#doc.to_owned(), &vm.ctx));
-            let new_func = quote_spanned!(ident.span()=>
-                vm.ctx.make_func_def(vm.ctx.intern_str(#py_name), #ident)
-                    #with_doc
-                    .into_function()
-                    .with_module(vm.new_pyobj(#module.to_owned()))
-                    .into_ref(&vm.ctx)
-            );
+        let module = args.module_name();
+        let doc = args.attrs.doc().or_else(|| {
+            crate::doc::Database::shared()
+                .try_module_item(module, &py_name)
+                .ok() // TODO: doc must exist at least one of code or CPython
+                .flatten()
+                .map(str::to_owned)
+        });
+        let doc = if let Some(doc) = doc {
+            format_doc(&sig_doc, &doc)
+        } else {
+            sig_doc
+        };
 
+        let py_names = {
             if self.py_attrs.is_empty() {
-                (
-                    quote_spanned! { ident.span() => {
-                        let func = #new_func;
-                        vm.__module_set_attr(module, #py_name, func).unwrap();
-                    }},
-                    vec![py_name],
-                    doc,
-                )
+                vec![py_name]
             } else {
                 let mut py_names = HashSet::new();
                 py_names.insert(py_name);
@@ -494,16 +495,7 @@ impl ModuleItem for FunctionItem {
                     args.context.errors.ok_or_push(r);
                 }
                 let py_names: Vec<_> = py_names.into_iter().collect();
-                (
-                    quote_spanned! { ident.span().resolved_at(Span::call_site()) => {
-                        let func = #new_func;
-                        for name in [#(#py_names,)*] {
-                            vm.__module_set_attr(module, name, func.clone()).unwrap();
-                        }
-                    }},
-                    py_names,
-                    doc,
-                )
+                py_names
             }
         };
 
@@ -512,7 +504,6 @@ impl ModuleItem for FunctionItem {
             py_names,
             cfgs: args.cfgs.to_vec(),
             doc,
-            tokens,
         });
         Ok(())
     }
@@ -585,12 +576,12 @@ impl ModuleItem for ClassItem {
             1 => {
                 let py_name = &py_names[0];
                 quote! {
-                    vm.__module_set_attr(&module, #py_name, new_class).unwrap();
+                    vm.__module_set_attr(&module, vm.ctx.intern_str(#py_name), new_class).unwrap();
                 }
             }
             _ => quote! {
                 for name in [#(#py_names,)*] {
-                    vm.__module_set_attr(&module, name, new_class.clone()).unwrap();
+                    vm.__module_set_attr(&module, vm.ctx.intern_str(name), new_class.clone()).unwrap();
                 }
             },
         };
@@ -681,7 +672,7 @@ impl ModuleItem for AttributeItem {
                         ident.to_string()
                     };
                     let tokens = quote_spanned! { ident.span() =>
-                        vm.__module_set_attr(module, #py_name, vm.new_pyobj(#ident)).unwrap();
+                        vm.__module_set_attr(module, vm.ctx.intern_str(#py_name), vm.new_pyobj(#ident)).unwrap();
                     };
                     args.context.attribute_items.add_item(
                         ident.clone(),
@@ -705,7 +696,7 @@ impl ModuleItem for AttributeItem {
             (
                 quote_spanned! { ident.span() => {
                     #let_obj
-                    vm.__module_set_attr(module, #py_name, obj).unwrap();
+                    vm.__module_set_attr(module, vm.ctx.intern_str(#py_name), obj).unwrap();
                 }},
                 vec![py_name],
             )
@@ -738,7 +729,7 @@ impl ModuleItem for AttributeItem {
                 quote_spanned! { ident.span() => {
                     #let_obj
                     for name in [(#(#names,)*)] {
-                        vm.__module_set_attr(module, name, obj.clone()).unwrap();
+                        vm.__module_set_attr(module, vm.ctx.intern_str(name), obj.clone()).unwrap();
                     }
                 }},
                 names,
diff --git a/derive-impl/src/util.rs b/derive-impl/src/util.rs
index c0979f0807..c7b98ab2ce 100644
--- a/derive-impl/src/util.rs
+++ b/derive-impl/src/util.rs
@@ -320,6 +320,7 @@ impl ItemMeta for ClassItemMeta {
         "base",
         "metaclass",
         "unhashable",
+        "ctx",
         "impl",
         "traverse",
     ];
diff --git a/examples/mini_repl.rs b/examples/mini_repl.rs
index 4e64419512..c5c2114acf 100644
--- a/examples/mini_repl.rs
+++ b/examples/mini_repl.rs
@@ -41,7 +41,7 @@ fn run(vm: &vm::VirtualMachine) -> vm::PyResult<()> {
     // typing `quit()` is too long, let's make `on(False)` work instead.
     scope
         .globals
-        .set_item("on", vm.ctx.new_function("on", on).into(), vm)?;
+        .set_item("on", vm.new_function("on", on).into(), vm)?;
 
     // let's include a fibonacci function, but let's be lazy and write it in Python
     add_python_function!(
diff --git a/stdlib/src/array.rs b/stdlib/src/array.rs
index aee4ab65ae..762573ba67 100644
--- a/stdlib/src/array.rs
+++ b/stdlib/src/array.rs
@@ -61,8 +61,8 @@ mod array {
                 SliceableSequenceOp,
             },
             types::{
-                AsBuffer, AsMapping, AsSequence, Comparable, Constructor, IterNext,
-                IterNextIterable, Iterable, PyComparisonOp, Representable,
+                AsBuffer, AsMapping, AsSequence, Comparable, Constructor, IterNext, Iterable,
+                PyComparisonOp, Representable, SelfIter,
             },
             AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
         },
@@ -1405,7 +1405,7 @@ mod array {
         internal: PyMutex<PositionIterInternal<PyArrayRef>>,
     }
 
-    #[pyclass(with(IterNext), flags(HAS_DICT))]
+    #[pyclass(with(IterNext, Iterable), flags(HAS_DICT))]
     impl PyArrayIter {
         #[pymethod(magic)]
         fn setstate(&self, state: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
@@ -1422,7 +1422,7 @@ mod array {
         }
     }
 
-    impl IterNextIterable for PyArrayIter {}
+    impl SelfIter for PyArrayIter {}
     impl IterNext for PyArrayIter {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             zelf.internal.lock().next(|array, pos| {
diff --git a/stdlib/src/csv.rs b/stdlib/src/csv.rs
index c49dec0798..bee3fd5faa 100644
--- a/stdlib/src/csv.rs
+++ b/stdlib/src/csv.rs
@@ -8,7 +8,7 @@ mod _csv {
         function::{ArgIterable, ArgumentError, FromArgs, FuncArgs},
         match_class,
         protocol::{PyIter, PyIterReturn},
-        types::{IterNext, IterNextIterable},
+        types::{IterNext, Iterable, SelfIter},
         AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine,
     };
     use itertools::{self, Itertools};
@@ -166,9 +166,9 @@ mod _csv {
         }
     }
 
-    #[pyclass(with(IterNext))]
+    #[pyclass(with(IterNext, Iterable))]
     impl Reader {}
-    impl IterNextIterable for Reader {}
+    impl SelfIter for Reader {}
     impl IterNext for Reader {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let string = match zelf.iter.next(vm)? {
diff --git a/stdlib/src/pystruct.rs b/stdlib/src/pystruct.rs
index 96c6d9dd2c..2d83e9570d 100644
--- a/stdlib/src/pystruct.rs
+++ b/stdlib/src/pystruct.rs
@@ -15,7 +15,7 @@ pub(crate) mod _struct {
         function::{ArgBytesLike, ArgMemoryBuffer, PosArgs},
         match_class,
         protocol::PyIterReturn,
-        types::{Constructor, IterNext, IterNextIterable},
+        types::{Constructor, IterNext, Iterable, SelfIter},
         AsObject, Py, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine,
     };
     use crossbeam_utils::atomic::AtomicCell;
@@ -194,14 +194,14 @@ pub(crate) mod _struct {
         }
     }
 
-    #[pyclass(with(IterNext))]
+    #[pyclass(with(IterNext, Iterable))]
     impl UnpackIterator {
         #[pymethod(magic)]
         fn length_hint(&self) -> usize {
             self.buffer.len().saturating_sub(self.offset.load()) / self.format_spec.size
         }
     }
-    impl IterNextIterable for UnpackIterator {}
+    impl SelfIter for UnpackIterator {}
     impl IterNext for UnpackIterator {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let size = zelf.format_spec.size;
diff --git a/stdlib/src/sqlite.rs b/stdlib/src/sqlite.rs
index addc781ac2..32ea0d0cd5 100644
--- a/stdlib/src/sqlite.rs
+++ b/stdlib/src/sqlite.rs
@@ -62,8 +62,8 @@ mod _sqlite {
         protocol::{PyBuffer, PyIterReturn, PyMappingMethods, PySequence, PySequenceMethods},
         sliceable::{SaturatedSliceIter, SliceableSequenceOp},
         types::{
-            AsMapping, AsSequence, Callable, Comparable, Constructor, Hashable, IterNext,
-            IterNextIterable, Iterable, PyComparisonOp,
+            AsMapping, AsSequence, Callable, Comparable, Constructor, Hashable, IterNext, Iterable,
+            PyComparisonOp, SelfIter,
         },
         utils::ToCString,
         AsObject, Py, PyAtomicRef, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
@@ -1377,7 +1377,7 @@ mod _sqlite {
         statement: Option<PyRef<Statement>>,
     }
 
-    #[pyclass(with(Constructor, IterNext), flags(BASETYPE))]
+    #[pyclass(with(Constructor, IterNext, Iterable), flags(BASETYPE))]
     impl Cursor {
         fn new(
             connection: PyRef<Connection>,
@@ -1708,7 +1708,7 @@ mod _sqlite {
         }
     }
 
-    impl IterNextIterable for Cursor {}
+    impl SelfIter for Cursor {}
     impl IterNext for Cursor {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let mut inner = zelf.inner(vm)?;
diff --git a/vm/src/builtins/asyncgenerator.rs b/vm/src/builtins/asyncgenerator.rs
index 287ab920c7..d58744da0a 100644
--- a/vm/src/builtins/asyncgenerator.rs
+++ b/vm/src/builtins/asyncgenerator.rs
@@ -6,7 +6,7 @@ use crate::{
     frame::FrameRef,
     function::OptionalArg,
     protocol::PyIterReturn,
-    types::{Constructor, IterNext, IterNextIterable, Representable, Unconstructible},
+    types::{Constructor, IterNext, Iterable, Representable, SelfIter, Unconstructible},
     AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
 };
 
@@ -195,7 +195,7 @@ impl PyPayload for PyAsyncGenASend {
     }
 }
 
-#[pyclass(with(IterNext))]
+#[pyclass(with(IterNext, Iterable))]
 impl PyAsyncGenASend {
     #[pymethod(name = "__await__")]
     fn r#await(zelf: PyRef<Self>, _vm: &VirtualMachine) -> PyRef<Self> {
@@ -268,7 +268,7 @@ impl PyAsyncGenASend {
     }
 }
 
-impl IterNextIterable for PyAsyncGenASend {}
+impl SelfIter for PyAsyncGenASend {}
 impl IterNext for PyAsyncGenASend {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         PyIterReturn::from_pyresult(zelf.send(vm.ctx.none(), vm), vm)
@@ -290,7 +290,7 @@ impl PyPayload for PyAsyncGenAThrow {
     }
 }
 
-#[pyclass(with(IterNext))]
+#[pyclass(with(IterNext, Iterable))]
 impl PyAsyncGenAThrow {
     #[pymethod(name = "__await__")]
     fn r#await(zelf: PyRef<Self>, _vm: &VirtualMachine) -> PyRef<Self> {
@@ -414,7 +414,7 @@ impl PyAsyncGenAThrow {
     }
 }
 
-impl IterNextIterable for PyAsyncGenAThrow {}
+impl SelfIter for PyAsyncGenAThrow {}
 impl IterNext for PyAsyncGenAThrow {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         PyIterReturn::from_pyresult(zelf.send(vm.ctx.none(), vm), vm)
diff --git a/vm/src/builtins/builtin_func.rs b/vm/src/builtins/builtin_func.rs
index 39a5353aa7..515ae7d726 100644
--- a/vm/src/builtins/builtin_func.rs
+++ b/vm/src/builtins/builtin_func.rs
@@ -1,94 +1,42 @@
-use super::{type_, PyClassMethod, PyStaticMethod, PyStr, PyStrInterned, PyStrRef, PyType};
+use super::{type_, PyStrInterned, PyStrRef, PyType};
 use crate::{
-    builtins::PyBoundMethod,
     class::PyClassImpl,
-    function::{FuncArgs, IntoPyNativeFunc, PyNativeFunc},
-    types::{Callable, Constructor, GetDescriptor, Representable, Unconstructible},
-    AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
+    convert::TryFromObject,
+    function::{FuncArgs, PyComparisonValue, PyMethodDef, PyMethodFlags, PyNativeFn},
+    types::{Callable, Comparable, Constructor, PyComparisonOp, Representable, Unconstructible},
+    AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
 };
 use std::fmt;
 
-pub struct PyNativeFuncDef {
-    pub func: PyNativeFunc,
-    pub name: &'static PyStrInterned,
-    pub doc: Option<PyStrRef>,
-}
-
-impl PyNativeFuncDef {
-    pub fn new(func: PyNativeFunc, name: &'static PyStrInterned) -> Self {
-        Self {
-            func,
-            name,
-            doc: None,
-        }
-    }
-
-    pub fn with_doc(mut self, doc: String, ctx: &Context) -> Self {
-        self.doc = Some(PyStr::new_ref(doc, ctx));
-        self
-    }
-
-    pub fn into_function(self) -> PyBuiltinFunction {
-        self.into()
-    }
-    pub fn build_function(self, ctx: &Context) -> PyRef<PyBuiltinFunction> {
-        self.into_function().into_ref(ctx)
-    }
-    pub fn build_method(self, ctx: &Context, class: &'static Py<PyType>) -> PyRef<PyBuiltinMethod> {
-        PyRef::new_ref(
-            PyBuiltinMethod { value: self, class },
-            ctx.types.method_descriptor_type.to_owned(),
-            None,
-        )
-    }
-    pub fn build_classmethod(
-        self,
-        ctx: &Context,
-        class: &'static Py<PyType>,
-    ) -> PyRef<PyClassMethod> {
-        // TODO: classmethod_descriptor
-        let callable = self.build_method(ctx, class).into();
-        PyClassMethod::new_ref(callable, ctx)
-    }
-    pub fn build_staticmethod(
-        self,
-        ctx: &Context,
-        class: &'static Py<PyType>,
-    ) -> PyRef<PyStaticMethod> {
-        let callable = self.build_method(ctx, class).into();
-        PyStaticMethod::new_ref(callable, ctx)
-    }
-}
-
+// PyCFunctionObject in CPython
 #[pyclass(name = "builtin_function_or_method", module = false)]
-pub struct PyBuiltinFunction {
-    value: PyNativeFuncDef,
-    module: Option<PyObjectRef>,
+pub struct PyNativeFunction {
+    pub(crate) value: &'static PyMethodDef,
+    pub(crate) zelf: Option<PyObjectRef>,
+    pub(crate) module: Option<&'static PyStrInterned>, // None for bound method
 }
 
-impl PyPayload for PyBuiltinFunction {
+impl PyPayload for PyNativeFunction {
     fn class(ctx: &Context) -> &'static Py<PyType> {
         ctx.types.builtin_function_or_method_type
     }
 }
 
-impl fmt::Debug for PyBuiltinFunction {
+impl fmt::Debug for PyNativeFunction {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "builtin function {}", self.value.name.as_str())
-    }
-}
-
-impl From<PyNativeFuncDef> for PyBuiltinFunction {
-    fn from(value: PyNativeFuncDef) -> Self {
-        Self {
-            value,
-            module: None,
-        }
+        write!(
+            f,
+            "builtin function {}.{} ({:?}) self as instance of {:?}",
+            self.module.map_or("<unknown>", |m| m.as_str()),
+            self.value.name,
+            self.value.flags,
+            self.zelf.as_ref().map(|z| z.class().name().to_owned())
+        )
     }
 }
 
-impl PyBuiltinFunction {
-    pub fn with_module(mut self, module: PyObjectRef) -> Self {
+impl PyNativeFunction {
+    pub fn with_module(mut self, module: &'static PyStrInterned) -> Self {
         self.module = Some(module);
         self
     }
@@ -101,183 +49,202 @@ impl PyBuiltinFunction {
         )
     }
 
-    pub fn as_func(&self) -> &PyNativeFunc {
-        &self.value.func
+    pub fn as_func(&self) -> &'static PyNativeFn {
+        self.value.func
     }
 }
 
-impl Callable for PyBuiltinFunction {
+impl Callable for PyNativeFunction {
     type Args = FuncArgs;
     #[inline]
-    fn call(zelf: &Py<Self>, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
+    fn call(zelf: &Py<Self>, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult {
+        if let Some(z) = &zelf.zelf {
+            args.prepend_arg(z.clone());
+        }
         (zelf.value.func)(vm, args)
     }
 }
 
 #[pyclass(with(Callable, Constructor), flags(HAS_DICT))]
-impl PyBuiltinFunction {
+impl PyNativeFunction {
     #[pygetset(magic)]
-    fn module(&self, vm: &VirtualMachine) -> PyObjectRef {
-        vm.unwrap_or_none(self.module.clone())
+    fn module(zelf: NativeFunctionOrMethod) -> Option<&'static PyStrInterned> {
+        zelf.0.module
     }
     #[pygetset(magic)]
-    fn name(&self) -> PyStrRef {
-        self.value.name.to_owned()
+    fn name(zelf: NativeFunctionOrMethod) -> &'static str {
+        zelf.0.value.name
     }
     #[pygetset(magic)]
-    fn qualname(&self) -> PyStrRef {
-        self.name()
+    fn qualname(zelf: NativeFunctionOrMethod, vm: &VirtualMachine) -> PyResult<PyStrRef> {
+        let zelf = zelf.0;
+        let flags = zelf.value.flags;
+        // if flags.contains(PyMethodFlags::CLASS) || flags.contains(PyMethodFlags::STATIC) {
+        let qualname = if let Some(bound) = &zelf.zelf {
+            let prefix = if flags.contains(PyMethodFlags::CLASS) {
+                bound
+                    .get_attr("__qualname__", vm)
+                    .unwrap()
+                    .str(vm)
+                    .unwrap()
+                    .to_string()
+            } else {
+                bound.class().name().to_string()
+            };
+            vm.ctx.new_str(format!("{}.{}", prefix, &zelf.value.name))
+        } else {
+            vm.ctx.intern_str(zelf.value.name).to_owned()
+        };
+        Ok(qualname)
     }
     #[pygetset(magic)]
-    fn doc(&self) -> Option<PyStrRef> {
-        self.value.doc.clone()
+    fn doc(zelf: NativeFunctionOrMethod) -> Option<&'static str> {
+        zelf.0.value.doc
     }
     #[pygetset(name = "__self__")]
-    fn get_self(&self, vm: &VirtualMachine) -> PyObjectRef {
+    fn __self__(_zelf: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
         vm.ctx.none()
     }
     #[pymethod(magic)]
-    fn reduce(&self) -> PyStrRef {
+    fn reduce(&self) -> &'static str {
         // TODO: return (getattr, (self.object, self.name)) if this is a method
-        self.name()
+        self.value.name
     }
     #[pymethod(magic)]
-    fn reduce_ex(&self, _ver: PyObjectRef) -> PyStrRef {
-        self.name()
+    fn reduce_ex(zelf: PyObjectRef, _ver: PyObjectRef, vm: &VirtualMachine) -> PyResult {
+        vm.call_special_method(&zelf, identifier!(vm, __reduce__), ())
     }
     #[pygetset(magic)]
-    fn text_signature(&self) -> Option<String> {
-        let doc = self.value.doc.as_ref()?;
-        let signature =
-            type_::get_text_signature_from_internal_doc(self.value.name.as_str(), doc.as_str())?;
-        Some(signature.to_owned())
+    fn text_signature(zelf: NativeFunctionOrMethod) -> Option<&'static str> {
+        let doc = zelf.0.value.doc?;
+        let signature = type_::get_text_signature_from_internal_doc(zelf.0.value.name, doc)?;
+        Some(signature)
     }
 }
 
-impl Representable for PyBuiltinFunction {
+impl Representable for PyNativeFunction {
     #[inline]
     fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
         Ok(format!("<built-in function {}>", zelf.value.name))
     }
 }
 
-impl Unconstructible for PyBuiltinFunction {}
+impl Unconstructible for PyNativeFunction {}
 
-// `PyBuiltinMethod` is similar to both `PyMethodDescrObject` in
-// https://github.com/python/cpython/blob/main/Include/descrobject.h
-// https://github.com/python/cpython/blob/main/Objects/descrobject.c
-// and `PyCMethodObject` in
-// https://github.com/python/cpython/blob/main/Include/cpython/methodobject.h
-// https://github.com/python/cpython/blob/main/Objects/methodobject.c
-#[pyclass(module = false, name = "method_descriptor")]
-pub struct PyBuiltinMethod {
-    value: PyNativeFuncDef,
-    class: &'static Py<PyType>,
+// `PyCMethodObject` in CPython
+#[pyclass(name = "builtin_method", module = false, base = "PyNativeFunction")]
+pub struct PyNativeMethod {
+    pub(crate) func: PyNativeFunction,
+    pub(crate) class: &'static Py<PyType>, // TODO: the actual life is &'self
 }
 
-impl PyPayload for PyBuiltinMethod {
-    fn class(ctx: &Context) -> &'static Py<PyType> {
-        ctx.types.method_descriptor_type
+#[pyclass(with(Callable, Comparable, Representable), flags(HAS_DICT))]
+impl PyNativeMethod {
+    #[pygetset(magic)]
+    fn qualname(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
+        let prefix = zelf.class.name().to_string();
+        Ok(vm
+            .ctx
+            .new_str(format!("{}.{}", prefix, &zelf.func.value.name)))
     }
-}
 
-impl fmt::Debug for PyBuiltinMethod {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "method descriptor for '{}'", self.value.name)
+    #[pymethod(magic)]
+    fn reduce(&self, vm: &VirtualMachine) -> PyResult<(PyObjectRef, (PyObjectRef, &'static str))> {
+        // TODO: return (getattr, (self.object, self.name)) if this is a method
+        let getattr = vm.builtins.get_attr("getattr", vm)?;
+        let target = self
+            .func
+            .zelf
+            .clone()
+            .unwrap_or_else(|| self.class.to_owned().into());
+        let name = self.func.value.name;
+        Ok((getattr, (target, name)))
     }
-}
 
-impl GetDescriptor for PyBuiltinMethod {
-    fn descr_get(
-        zelf: PyObjectRef,
-        obj: Option<PyObjectRef>,
-        cls: Option<PyObjectRef>,
-        vm: &VirtualMachine,
-    ) -> PyResult {
-        let (_zelf, obj) = match Self::_check(&zelf, obj, vm) {
-            Some(obj) => obj,
-            None => return Ok(zelf),
-        };
-        let r = if vm.is_none(&obj) && !Self::_cls_is(&cls, obj.class()) {
-            zelf
-        } else {
-            PyBoundMethod::new_ref(obj, zelf, &vm.ctx).into()
-        };
-        Ok(r)
+    #[pygetset(name = "__self__")]
+    fn __self__(zelf: PyRef<Self>, _vm: &VirtualMachine) -> Option<PyObjectRef> {
+        zelf.func.zelf.clone()
     }
 }
 
-impl Callable for PyBuiltinMethod {
-    type Args = FuncArgs;
-    #[inline]
-    fn call(zelf: &Py<Self>, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
-        (zelf.value.func)(vm, args)
+impl PyPayload for PyNativeMethod {
+    fn class(ctx: &Context) -> &'static Py<PyType> {
+        ctx.types.builtin_method_type
     }
 }
 
-impl PyBuiltinMethod {
-    pub fn new_ref<F, FKind>(
-        name: &'static PyStrInterned,
-        class: &'static Py<PyType>,
-        f: F,
-        ctx: &Context,
-    ) -> PyRef<Self>
-    where
-        F: IntoPyNativeFunc<FKind>,
-    {
-        ctx.make_func_def(name, f).build_method(ctx, class)
+impl fmt::Debug for PyNativeMethod {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "builtin method of {:?} with {:?}",
+            &*self.class.name(),
+            &self.func
+        )
     }
 }
 
-#[pyclass(
-    with(GetDescriptor, Callable, Constructor, Representable),
-    flags(METHOD_DESCR)
-)]
-impl PyBuiltinMethod {
-    #[pygetset(magic)]
-    fn name(&self) -> PyStrRef {
-        self.value.name.to_owned()
-    }
-    #[pygetset(magic)]
-    fn qualname(&self) -> String {
-        format!("{}.{}", self.class.name(), &self.value.name)
-    }
-    #[pygetset(magic)]
-    fn doc(&self) -> Option<PyStrRef> {
-        self.value.doc.clone()
-    }
-    #[pygetset(magic)]
-    fn text_signature(&self) -> Option<String> {
-        self.value.doc.as_ref().and_then(|doc| {
-            type_::get_text_signature_from_internal_doc(self.value.name.as_str(), doc.as_str())
-                .map(|signature| signature.to_string())
+impl Comparable for PyNativeMethod {
+    fn cmp(
+        zelf: &Py<Self>,
+        other: &PyObject,
+        op: PyComparisonOp,
+        _vm: &VirtualMachine,
+    ) -> PyResult<PyComparisonValue> {
+        op.eq_only(|| {
+            if let Some(other) = other.payload::<Self>() {
+                let eq = match (zelf.func.zelf.as_ref(), other.func.zelf.as_ref()) {
+                    (Some(z), Some(o)) => z.is(o),
+                    (None, None) => true,
+                    _ => false,
+                };
+                let eq = eq && std::ptr::eq(zelf.func.value, other.func.value);
+                Ok(eq.into())
+            } else {
+                Ok(PyComparisonValue::NotImplemented)
+            }
         })
     }
-    #[pymethod(magic)]
-    fn reduce(
-        &self,
-        vm: &VirtualMachine,
-    ) -> (Option<PyObjectRef>, (Option<PyObjectRef>, PyStrRef)) {
-        let builtins_getattr = vm.builtins.get_attr("getattr", vm).ok();
-        let classname = vm.builtins.get_attr(&self.class.__name__(vm), vm).ok();
-        (builtins_getattr, (classname, self.value.name.to_owned()))
+}
+
+impl Callable for PyNativeMethod {
+    type Args = FuncArgs;
+    #[inline]
+    fn call(zelf: &Py<Self>, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult {
+        if let Some(zelf) = &zelf.func.zelf {
+            args.prepend_arg(zelf.clone());
+        }
+        (zelf.func.value.func)(vm, args)
     }
 }
 
-impl Representable for PyBuiltinMethod {
+impl Representable for PyNativeMethod {
     #[inline]
     fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
         Ok(format!(
-            "<method '{}' of '{}' objects>",
-            &zelf.value.name,
+            "<built-in method {} of {} object at ...>",
+            &zelf.func.value.name,
             zelf.class.name()
         ))
     }
 }
 
-impl Unconstructible for PyBuiltinMethod {}
+impl Unconstructible for PyNativeMethod {}
 
 pub fn init(context: &Context) {
-    PyBuiltinFunction::extend_class(context, context.types.builtin_function_or_method_type);
-    PyBuiltinMethod::extend_class(context, context.types.method_descriptor_type);
+    PyNativeFunction::extend_class(context, context.types.builtin_function_or_method_type);
+    PyNativeMethod::extend_class(context, context.types.builtin_method_type);
+}
+
+struct NativeFunctionOrMethod(PyRef<PyNativeFunction>);
+
+impl TryFromObject for NativeFunctionOrMethod {
+    fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
+        let class = vm.ctx.types.builtin_function_or_method_type;
+        if obj.fast_isinstance(class) {
+            Ok(NativeFunctionOrMethod(unsafe { obj.downcast_unchecked() }))
+        } else {
+            Err(vm.new_downcast_type_error(class, &obj))
+        }
+    }
 }
diff --git a/vm/src/builtins/bytearray.rs b/vm/src/builtins/bytearray.rs
index 87757fae36..f1279389e4 100644
--- a/vm/src/builtins/bytearray.rs
+++ b/vm/src/builtins/bytearray.rs
@@ -31,7 +31,7 @@ use crate::{
     sliceable::{SequenceIndex, SliceableSequenceMutOp, SliceableSequenceOp},
     types::{
         AsBuffer, AsMapping, AsNumber, AsSequence, Callable, Comparable, Constructor, Initializer,
-        IterNext, IterNextIterable, Iterable, PyComparisonOp, Representable, Unconstructible,
+        IterNext, Iterable, PyComparisonOp, Representable, SelfIter, Unconstructible,
     },
     AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
     VirtualMachine,
@@ -891,7 +891,7 @@ impl PyPayload for PyByteArrayIterator {
     }
 }
 
-#[pyclass(with(Constructor, IterNext))]
+#[pyclass(with(Constructor, IterNext, Iterable))]
 impl PyByteArrayIterator {
     #[pymethod(magic)]
     fn length_hint(&self) -> usize {
@@ -914,7 +914,7 @@ impl PyByteArrayIterator {
 
 impl Unconstructible for PyByteArrayIterator {}
 
-impl IterNextIterable for PyByteArrayIterator {}
+impl SelfIter for PyByteArrayIterator {}
 impl IterNext for PyByteArrayIterator {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         zelf.internal.lock().next(|bytearray, pos| {
diff --git a/vm/src/builtins/bytes.rs b/vm/src/builtins/bytes.rs
index 18ea0d01ca..b27a8a2df3 100644
--- a/vm/src/builtins/bytes.rs
+++ b/vm/src/builtins/bytes.rs
@@ -21,7 +21,7 @@ use crate::{
     sliceable::{SequenceIndex, SliceableSequenceOp},
     types::{
         AsBuffer, AsMapping, AsNumber, AsSequence, Callable, Comparable, Constructor, Hashable,
-        IterNext, IterNextIterable, Iterable, PyComparisonOp, Representable, Unconstructible,
+        IterNext, Iterable, PyComparisonOp, Representable, SelfIter, Unconstructible,
     },
     AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
     TryFromBorrowedObject, TryFromObject, VirtualMachine,
@@ -688,7 +688,7 @@ impl PyPayload for PyBytesIterator {
     }
 }
 
-#[pyclass(with(Constructor, IterNext))]
+#[pyclass(with(Constructor, IterNext, Iterable))]
 impl PyBytesIterator {
     #[pymethod(magic)]
     fn length_hint(&self) -> usize {
@@ -711,7 +711,7 @@ impl PyBytesIterator {
 }
 impl Unconstructible for PyBytesIterator {}
 
-impl IterNextIterable for PyBytesIterator {}
+impl SelfIter for PyBytesIterator {}
 impl IterNext for PyBytesIterator {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         zelf.internal.lock().next(|bytes, pos| {
diff --git a/vm/src/builtins/coroutine.rs b/vm/src/builtins/coroutine.rs
index 440ff46b50..2454e27e2c 100644
--- a/vm/src/builtins/coroutine.rs
+++ b/vm/src/builtins/coroutine.rs
@@ -5,7 +5,7 @@ use crate::{
     frame::FrameRef,
     function::OptionalArg,
     protocol::PyIterReturn,
-    types::{Constructor, IterNext, IterNextIterable, Representable, Unconstructible},
+    types::{Constructor, IterNext, Iterable, Representable, SelfIter, Unconstructible},
     AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
 };
 
@@ -108,7 +108,7 @@ impl Representable for PyCoroutine {
     }
 }
 
-impl IterNextIterable for PyCoroutine {}
+impl SelfIter for PyCoroutine {}
 impl IterNext for PyCoroutine {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         Self::send(zelf, vm.ctx.none(), vm)
@@ -128,7 +128,7 @@ impl PyPayload for PyCoroutineWrapper {
     }
 }
 
-#[pyclass(with(IterNext))]
+#[pyclass(with(IterNext, Iterable))]
 impl PyCoroutineWrapper {
     #[pymethod]
     fn send(&self, val: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
@@ -147,7 +147,7 @@ impl PyCoroutineWrapper {
     }
 }
 
-impl IterNextIterable for PyCoroutineWrapper {}
+impl SelfIter for PyCoroutineWrapper {}
 impl IterNext for PyCoroutineWrapper {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         Self::send(zelf, vm.ctx.none(), vm)
diff --git a/vm/src/builtins/descriptor.rs b/vm/src/builtins/descriptor.rs
index 2425f3ffe4..6b92e4c36b 100644
--- a/vm/src/builtins/descriptor.rs
+++ b/vm/src/builtins/descriptor.rs
@@ -1,19 +1,155 @@
-use super::{PyStr, PyStrInterned, PyType, PyTypeRef};
+use super::{PyStr, PyStrInterned, PyType};
 use crate::{
+    builtins::{builtin_func::PyNativeMethod, type_},
     class::PyClassImpl,
-    function::PySetterValue,
-    types::{Constructor, GetDescriptor, Representable, Unconstructible},
-    AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine,
+    function::{FuncArgs, PyMethodDef, PyMethodFlags, PySetterValue},
+    types::{Callable, Constructor, GetDescriptor, Representable, Unconstructible},
+    AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
 };
 use rustpython_common::lock::PyRwLock;
 
 #[derive(Debug)]
-pub struct DescrObject {
-    pub typ: PyTypeRef,
+pub struct PyDescriptor {
+    pub typ: &'static Py<PyType>,
     pub name: &'static PyStrInterned,
     pub qualname: PyRwLock<Option<String>>,
 }
 
+#[derive(Debug)]
+pub struct PyDescriptorOwned {
+    pub typ: PyRef<PyType>,
+    pub name: &'static PyStrInterned,
+    pub qualname: PyRwLock<Option<String>>,
+}
+
+#[pyclass(name = "method_descriptor", module = false)]
+pub struct PyMethodDescriptor {
+    pub common: PyDescriptor,
+    pub method: &'static PyMethodDef,
+    // vectorcall: vectorcallfunc,
+}
+
+impl PyMethodDescriptor {
+    pub fn new(method: &'static PyMethodDef, typ: &'static Py<PyType>, ctx: &Context) -> Self {
+        Self {
+            common: PyDescriptor {
+                typ,
+                name: ctx.intern_str(method.name),
+                qualname: PyRwLock::new(None),
+            },
+            method,
+        }
+    }
+}
+
+impl PyPayload for PyMethodDescriptor {
+    fn class(ctx: &Context) -> &'static Py<PyType> {
+        ctx.types.method_descriptor_type
+    }
+}
+
+impl std::fmt::Debug for PyMethodDescriptor {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "method descriptor for '{}'", self.common.name)
+    }
+}
+
+impl GetDescriptor for PyMethodDescriptor {
+    fn descr_get(
+        zelf: PyObjectRef,
+        obj: Option<PyObjectRef>,
+        cls: Option<PyObjectRef>,
+        vm: &VirtualMachine,
+    ) -> PyResult {
+        let descr = Self::_as_pyref(&zelf, vm).unwrap();
+        let bound = match obj {
+            Some(obj) => {
+                if descr.method.flags.contains(PyMethodFlags::METHOD) {
+                    if cls.map_or(false, |c| c.fast_isinstance(vm.ctx.types.type_type)) {
+                        obj
+                    } else {
+                        return Err(vm.new_type_error(format!(
+                            "descriptor '{}' needs a type, not '{}', as arg 2",
+                            descr.common.name.as_str(),
+                            obj.class().name()
+                        )));
+                    }
+                } else if descr.method.flags.contains(PyMethodFlags::CLASS) {
+                    obj.class().to_owned().into()
+                } else {
+                    unimplemented!()
+                }
+            }
+            None if descr.method.flags.contains(PyMethodFlags::CLASS) => cls.unwrap(),
+            None => return Ok(zelf),
+        };
+        // Ok(descr.method.build_bound_method(&vm.ctx, bound, class).into())
+        Ok(descr.bind(bound, &vm.ctx).into())
+    }
+}
+
+impl Callable for PyMethodDescriptor {
+    type Args = FuncArgs;
+    #[inline]
+    fn call(zelf: &Py<Self>, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
+        (zelf.method.func)(vm, args)
+    }
+}
+
+impl PyMethodDescriptor {
+    pub fn bind(&self, obj: PyObjectRef, ctx: &Context) -> PyRef<PyNativeMethod> {
+        self.method.build_bound_method(ctx, obj, self.common.typ)
+    }
+}
+
+#[pyclass(
+    with(GetDescriptor, Callable, Constructor, Representable),
+    flags(METHOD_DESCRIPTOR)
+)]
+impl PyMethodDescriptor {
+    #[pygetset(magic)]
+    fn name(&self) -> &'static PyStrInterned {
+        self.common.name
+    }
+    #[pygetset(magic)]
+    fn qualname(&self) -> String {
+        format!("{}.{}", self.common.typ.name(), &self.common.name)
+    }
+    #[pygetset(magic)]
+    fn doc(&self) -> Option<&'static str> {
+        self.method.doc
+    }
+    #[pygetset(magic)]
+    fn text_signature(&self) -> Option<String> {
+        self.method.doc.and_then(|doc| {
+            type_::get_text_signature_from_internal_doc(self.method.name, doc)
+                .map(|signature| signature.to_string())
+        })
+    }
+    #[pymethod(magic)]
+    fn reduce(
+        &self,
+        vm: &VirtualMachine,
+    ) -> (Option<PyObjectRef>, (Option<PyObjectRef>, &'static str)) {
+        let builtins_getattr = vm.builtins.get_attr("getattr", vm).ok();
+        let classname = vm.builtins.get_attr(&self.common.typ.__name__(vm), vm).ok();
+        (builtins_getattr, (classname, self.method.name))
+    }
+}
+
+impl Representable for PyMethodDescriptor {
+    #[inline]
+    fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
+        Ok(format!(
+            "<method '{}' of '{}' objects>",
+            &zelf.method.name,
+            zelf.common.typ.name()
+        ))
+    }
+}
+
+impl Unconstructible for PyMethodDescriptor {}
+
 #[derive(Debug)]
 pub enum MemberKind {
     Bool = 14,
@@ -78,7 +214,7 @@ impl std::fmt::Debug for PyMemberDef {
 #[pyclass(name = "member_descriptor", module = false)]
 #[derive(Debug)]
 pub struct PyMemberDescriptor {
-    pub common: DescrObject,
+    pub common: PyDescriptorOwned,
     pub member: PyMemberDef,
 }
 
@@ -88,8 +224,8 @@ impl PyPayload for PyMemberDescriptor {
     }
 }
 
-fn calculate_qualname(descr: &DescrObject, vm: &VirtualMachine) -> PyResult<Option<String>> {
-    if let Some(qualname) = vm.get_attribute_opt(descr.typ.to_owned().into(), "__qualname__")? {
+fn calculate_qualname(descr: &PyDescriptorOwned, vm: &VirtualMachine) -> PyResult<Option<String>> {
+    if let Some(qualname) = vm.get_attribute_opt(descr.typ.clone().into(), "__qualname__")? {
         let str = qualname.downcast::<PyStr>().map_err(|_| {
             vm.new_type_error(
                 "<descriptor>.__objclass__.__qualname__ is not a unicode object".to_owned(),
@@ -217,7 +353,7 @@ impl GetDescriptor for PyMemberDescriptor {
     }
 }
 
-pub fn init(context: &Context) {
-    let member_descriptor_type = &context.types.member_descriptor_type;
-    PyMemberDescriptor::extend_class(context, member_descriptor_type);
+pub fn init(ctx: &Context) {
+    PyMemberDescriptor::extend_class(ctx, ctx.types.member_descriptor_type);
+    PyMethodDescriptor::extend_class(ctx, ctx.types.method_descriptor_type);
 }
diff --git a/vm/src/builtins/dict.rs b/vm/src/builtins/dict.rs
index b2a191fd93..a164fc11b6 100644
--- a/vm/src/builtins/dict.rs
+++ b/vm/src/builtins/dict.rs
@@ -20,7 +20,7 @@ use crate::{
     recursion::ReprGuard,
     types::{
         AsMapping, AsNumber, AsSequence, Callable, Comparable, Constructor, Initializer, IterNext,
-        IterNextIterable, Iterable, PyComparisonOp, Representable, Unconstructible,
+        Iterable, PyComparisonOp, Representable, SelfIter, Unconstructible,
     },
     vm::VirtualMachine,
     AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult,
@@ -839,7 +839,7 @@ macro_rules! dict_view {
             }
         }
 
-        #[pyclass(with(Constructor, IterNext))]
+        #[pyclass(with(Constructor, IterNext, Iterable))]
         impl $iter_name {
             fn new(dict: PyDictRef) -> Self {
                 $iter_name {
@@ -870,7 +870,7 @@ macro_rules! dict_view {
         }
         impl Unconstructible for $iter_name {}
 
-        impl IterNextIterable for $iter_name {}
+        impl SelfIter for $iter_name {}
         impl IterNext for $iter_name {
             #[allow(clippy::redundant_closure_call)]
             fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
@@ -912,7 +912,7 @@ macro_rules! dict_view {
             }
         }
 
-        #[pyclass(with(Constructor, IterNext))]
+        #[pyclass(with(Constructor, IterNext, Iterable))]
         impl $reverse_iter_name {
             fn new(dict: PyDictRef) -> Self {
                 let size = dict.size();
@@ -948,7 +948,7 @@ macro_rules! dict_view {
         }
         impl Unconstructible for $reverse_iter_name {}
 
-        impl IterNextIterable for $reverse_iter_name {}
+        impl SelfIter for $reverse_iter_name {}
         impl IterNext for $reverse_iter_name {
             #[allow(clippy::redundant_closure_call)]
             fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
diff --git a/vm/src/builtins/enumerate.rs b/vm/src/builtins/enumerate.rs
index c1582f08d5..00a3215ad6 100644
--- a/vm/src/builtins/enumerate.rs
+++ b/vm/src/builtins/enumerate.rs
@@ -7,7 +7,7 @@ use crate::{
     convert::ToPyObject,
     function::OptionalArg,
     protocol::{PyIter, PyIterReturn},
-    types::{Constructor, IterNext, IterNextIterable},
+    types::{Constructor, IterNext, Iterable, SelfIter},
     AsObject, Context, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine,
 };
 use num_bigint::BigInt;
@@ -52,7 +52,7 @@ impl Constructor for PyEnumerate {
     }
 }
 
-#[pyclass(with(Py, IterNext, Constructor), flags(BASETYPE))]
+#[pyclass(with(Py, IterNext, Iterable, Constructor), flags(BASETYPE))]
 impl PyEnumerate {
     #[pyclassmethod(magic)]
     fn class_getitem(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias {
@@ -71,7 +71,7 @@ impl Py<PyEnumerate> {
     }
 }
 
-impl IterNextIterable for PyEnumerate {}
+impl SelfIter for PyEnumerate {}
 impl IterNext for PyEnumerate {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         let next_obj = match zelf.iterator.next(vm)? {
@@ -97,7 +97,7 @@ impl PyPayload for PyReverseSequenceIterator {
     }
 }
 
-#[pyclass(with(IterNext))]
+#[pyclass(with(IterNext, Iterable))]
 impl PyReverseSequenceIterator {
     pub fn new(obj: PyObjectRef, len: usize) -> Self {
         let position = len.saturating_sub(1);
@@ -130,7 +130,7 @@ impl PyReverseSequenceIterator {
     }
 }
 
-impl IterNextIterable for PyReverseSequenceIterator {}
+impl SelfIter for PyReverseSequenceIterator {}
 impl IterNext for PyReverseSequenceIterator {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         zelf.internal
diff --git a/vm/src/builtins/filter.rs b/vm/src/builtins/filter.rs
index 660be88ea8..3b33ff766f 100644
--- a/vm/src/builtins/filter.rs
+++ b/vm/src/builtins/filter.rs
@@ -2,7 +2,7 @@ use super::{PyType, PyTypeRef};
 use crate::{
     class::PyClassImpl,
     protocol::{PyIter, PyIterReturn},
-    types::{Constructor, IterNext, IterNextIterable},
+    types::{Constructor, IterNext, Iterable, SelfIter},
     Context, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine,
 };
 
@@ -32,7 +32,7 @@ impl Constructor for PyFilter {
     }
 }
 
-#[pyclass(with(IterNext, Constructor), flags(BASETYPE))]
+#[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
 impl PyFilter {
     #[pymethod(magic)]
     fn reduce(&self, vm: &VirtualMachine) -> (PyTypeRef, (PyObjectRef, PyIter)) {
@@ -43,7 +43,7 @@ impl PyFilter {
     }
 }
 
-impl IterNextIterable for PyFilter {}
+impl SelfIter for PyFilter {}
 impl IterNext for PyFilter {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         let predicate = &zelf.predicate;
diff --git a/vm/src/builtins/function.rs b/vm/src/builtins/function.rs
index dedec7fc9a..dc1764f48a 100644
--- a/vm/src/builtins/function.rs
+++ b/vm/src/builtins/function.rs
@@ -350,7 +350,7 @@ impl PyPayload for PyFunction {
 
 #[pyclass(
     with(GetDescriptor, Callable, Representable),
-    flags(HAS_DICT, METHOD_DESCR)
+    flags(HAS_DICT, METHOD_DESCRIPTOR)
 )]
 impl PyFunction {
     #[pygetset(magic)]
diff --git a/vm/src/builtins/generator.rs b/vm/src/builtins/generator.rs
index 745c33aad9..eceac5ba93 100644
--- a/vm/src/builtins/generator.rs
+++ b/vm/src/builtins/generator.rs
@@ -9,7 +9,7 @@ use crate::{
     frame::FrameRef,
     function::OptionalArg,
     protocol::PyIterReturn,
-    types::{Constructor, IterNext, IterNextIterable, Representable, Unconstructible},
+    types::{Constructor, IterNext, Iterable, Representable, SelfIter, Unconstructible},
     AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
 };
 
@@ -25,7 +25,7 @@ impl PyPayload for PyGenerator {
     }
 }
 
-#[pyclass(with(Py, Constructor, IterNext))]
+#[pyclass(with(Py, Constructor, IterNext, Iterable))]
 impl PyGenerator {
     pub fn as_coro(&self) -> &Coro {
         &self.inner
@@ -104,7 +104,7 @@ impl Representable for PyGenerator {
     }
 }
 
-impl IterNextIterable for PyGenerator {}
+impl SelfIter for PyGenerator {}
 impl IterNext for PyGenerator {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         zelf.send(vm.ctx.none(), vm)
diff --git a/vm/src/builtins/iter.rs b/vm/src/builtins/iter.rs
index 6f8a01e057..2fed23a5c4 100644
--- a/vm/src/builtins/iter.rs
+++ b/vm/src/builtins/iter.rs
@@ -8,7 +8,7 @@ use crate::{
     function::ArgCallable,
     object::{Traverse, TraverseFn},
     protocol::{PyIterReturn, PySequence, PySequenceMethods},
-    types::{IterNext, IterNextIterable},
+    types::{IterNext, Iterable, SelfIter},
     Context, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine,
 };
 use rustpython_common::{
@@ -189,7 +189,7 @@ impl PyPayload for PySequenceIterator {
     }
 }
 
-#[pyclass(with(IterNext))]
+#[pyclass(with(IterNext, Iterable))]
 impl PySequenceIterator {
     pub fn new(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<Self> {
         let seq = PySequence::try_protocol(&obj, vm)?;
@@ -226,7 +226,7 @@ impl PySequenceIterator {
     }
 }
 
-impl IterNextIterable for PySequenceIterator {}
+impl SelfIter for PySequenceIterator {}
 impl IterNext for PySequenceIterator {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         zelf.internal.lock().next(|obj, pos| {
@@ -252,7 +252,7 @@ impl PyPayload for PyCallableIterator {
     }
 }
 
-#[pyclass(with(IterNext))]
+#[pyclass(with(IterNext, Iterable))]
 impl PyCallableIterator {
     pub fn new(callable: ArgCallable, sentinel: PyObjectRef) -> Self {
         Self {
@@ -262,7 +262,7 @@ impl PyCallableIterator {
     }
 }
 
-impl IterNextIterable for PyCallableIterator {}
+impl SelfIter for PyCallableIterator {}
 impl IterNext for PyCallableIterator {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         let status = zelf.status.upgradable_read();
diff --git a/vm/src/builtins/list.rs b/vm/src/builtins/list.rs
index 581c0fc9e5..03503a0cea 100644
--- a/vm/src/builtins/list.rs
+++ b/vm/src/builtins/list.rs
@@ -13,8 +13,8 @@ use crate::{
     sequence::{MutObjectSequenceOp, OptionalRangeArgs, SequenceExt, SequenceMutExt},
     sliceable::{SequenceIndex, SliceableSequenceMutOp, SliceableSequenceOp},
     types::{
-        AsMapping, AsSequence, Comparable, Constructor, Initializer, IterNext, IterNextIterable,
-        Iterable, PyComparisonOp, Representable, Unconstructible,
+        AsMapping, AsSequence, Comparable, Constructor, Initializer, IterNext, Iterable,
+        PyComparisonOp, Representable, SelfIter, Unconstructible,
     },
     utils::collection_repr,
     vm::VirtualMachine,
@@ -543,7 +543,7 @@ impl PyPayload for PyListIterator {
     }
 }
 
-#[pyclass(with(Constructor, IterNext))]
+#[pyclass(with(Constructor, IterNext, Iterable))]
 impl PyListIterator {
     #[pymethod(magic)]
     fn length_hint(&self) -> usize {
@@ -566,7 +566,7 @@ impl PyListIterator {
 }
 impl Unconstructible for PyListIterator {}
 
-impl IterNextIterable for PyListIterator {}
+impl SelfIter for PyListIterator {}
 impl IterNext for PyListIterator {
     fn next(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         zelf.internal.lock().next(|list, pos| {
@@ -588,7 +588,7 @@ impl PyPayload for PyListReverseIterator {
     }
 }
 
-#[pyclass(with(Constructor, IterNext))]
+#[pyclass(with(Constructor, IterNext, Iterable))]
 impl PyListReverseIterator {
     #[pymethod(magic)]
     fn length_hint(&self) -> usize {
@@ -611,7 +611,7 @@ impl PyListReverseIterator {
 }
 impl Unconstructible for PyListReverseIterator {}
 
-impl IterNextIterable for PyListReverseIterator {}
+impl SelfIter for PyListReverseIterator {}
 impl IterNext for PyListReverseIterator {
     fn next(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         zelf.internal.lock().rev_next(|list, pos| {
diff --git a/vm/src/builtins/map.rs b/vm/src/builtins/map.rs
index 6fb842f413..44bacf587e 100644
--- a/vm/src/builtins/map.rs
+++ b/vm/src/builtins/map.rs
@@ -4,7 +4,7 @@ use crate::{
     class::PyClassImpl,
     function::PosArgs,
     protocol::{PyIter, PyIterReturn},
-    types::{Constructor, IterNext, IterNextIterable},
+    types::{Constructor, IterNext, Iterable, SelfIter},
     Context, Py, PyObjectRef, PyPayload, PyResult, VirtualMachine,
 };
 
@@ -32,7 +32,7 @@ impl Constructor for PyMap {
     }
 }
 
-#[pyclass(with(IterNext, Constructor), flags(BASETYPE))]
+#[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
 impl PyMap {
     #[pymethod(magic)]
     fn length_hint(&self, vm: &VirtualMachine) -> PyResult<usize> {
@@ -51,7 +51,7 @@ impl PyMap {
     }
 }
 
-impl IterNextIterable for PyMap {}
+impl SelfIter for PyMap {}
 impl IterNext for PyMap {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         let mut next_objs = Vec::new();
diff --git a/vm/src/builtins/memory.rs b/vm/src/builtins/memory.rs
index 312364a744..2c436ca316 100644
--- a/vm/src/builtins/memory.rs
+++ b/vm/src/builtins/memory.rs
@@ -21,8 +21,8 @@ use crate::{
     },
     sliceable::SequenceIndexOp,
     types::{
-        AsBuffer, AsMapping, AsSequence, Comparable, Constructor, Hashable, IterNext,
-        IterNextIterable, Iterable, PyComparisonOp, Representable, Unconstructible,
+        AsBuffer, AsMapping, AsSequence, Comparable, Constructor, Hashable, IterNext, Iterable,
+        PyComparisonOp, Representable, SelfIter, Unconstructible,
     },
     AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
     TryFromBorrowedObject, TryFromObject, VirtualMachine,
@@ -1137,7 +1137,7 @@ impl PyPayload for PyMemoryViewIterator {
     }
 }
 
-#[pyclass(with(Constructor, IterNext))]
+#[pyclass(with(Constructor, IterNext, Iterable))]
 impl PyMemoryViewIterator {
     #[pymethod(magic)]
     fn reduce(&self, vm: &VirtualMachine) -> PyTupleRef {
@@ -1148,7 +1148,7 @@ impl PyMemoryViewIterator {
 }
 impl Unconstructible for PyMemoryViewIterator {}
 
-impl IterNextIterable for PyMemoryViewIterator {}
+impl SelfIter for PyMemoryViewIterator {}
 impl IterNext for PyMemoryViewIterator {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         zelf.internal.lock().next(|mv, pos| {
diff --git a/vm/src/builtins/module.rs b/vm/src/builtins/module.rs
index 4c77bc2677..58174082d5 100644
--- a/vm/src/builtins/module.rs
+++ b/vm/src/builtins/module.rs
@@ -3,7 +3,7 @@ use crate::{
     builtins::{pystr::AsPyStr, PyStrInterned},
     class::PyClassImpl,
     convert::ToPyObject,
-    function::FuncArgs,
+    function::{FuncArgs, PyMethodDef},
     types::{GetAttr, Initializer, Representable},
     AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
 };
@@ -15,7 +15,7 @@ pub struct PyModuleDef {
     pub name: &'static PyStrInterned,
     pub doc: Option<&'static PyStrInterned>,
     // pub size: isize,
-    // pub methods: &'static [PyMethodDef],
+    pub methods: &'static [PyMethodDef],
     pub slots: PyModuleSlots,
     // traverse: traverseproc
     // clear: inquiry
@@ -82,11 +82,23 @@ impl PyModule {
     }
     pub fn __init_dict_from_def(vm: &VirtualMachine, module: &Py<PyModule>) {
         let doc = module.def.unwrap().doc.map(|doc| doc.to_owned());
-        module.init_module_dict(module.name.unwrap(), doc, vm);
+        module.init_dict(module.name.unwrap(), doc, vm);
     }
 }
 
 impl Py<PyModule> {
+    pub fn __init_methods(&self, vm: &VirtualMachine) -> PyResult<()> {
+        debug_assert!(self.def.is_some());
+        for method in self.def.unwrap().methods {
+            let func = method
+                .to_function()
+                .with_module(self.name.unwrap())
+                .into_ref(&vm.ctx);
+            vm.__module_set_attr(self, vm.ctx.intern_str(method.name), func)?;
+        }
+        Ok(())
+    }
+
     fn getattr_inner(&self, name: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
         if let Some(attr) = self.as_object().generic_getattr_opt(name, None, vm)? {
             return Ok(attr);
@@ -115,7 +127,7 @@ impl Py<PyModule> {
         self.as_object().dict().unwrap()
     }
     // TODO: should be on PyModule, not Py<PyModule>
-    pub(crate) fn init_module_dict(
+    pub(crate) fn init_dict(
         &self,
         name: &'static PyStrInterned,
         doc: Option<PyStrRef>,
@@ -176,7 +188,7 @@ impl Initializer for PyModule {
             .slots
             .flags
             .has_feature(crate::types::PyTypeFlags::HAS_DICT));
-        zelf.init_module_dict(vm.ctx.intern_str(args.name.as_str()), args.doc, vm);
+        zelf.init_dict(vm.ctx.intern_str(args.name.as_str()), args.doc, vm);
         Ok(())
     }
 }
diff --git a/vm/src/builtins/range.rs b/vm/src/builtins/range.rs
index bc15a09093..276c94c1d0 100644
--- a/vm/src/builtins/range.rs
+++ b/vm/src/builtins/range.rs
@@ -8,8 +8,8 @@ use crate::{
     function::{ArgIndex, FuncArgs, OptionalArg, PyComparisonValue},
     protocol::{PyIterReturn, PyMappingMethods, PySequenceMethods},
     types::{
-        AsMapping, AsSequence, Comparable, Constructor, Hashable, IterNext, IterNextIterable,
-        Iterable, PyComparisonOp, Representable, Unconstructible,
+        AsMapping, AsSequence, Comparable, Constructor, Hashable, IterNext, Iterable,
+        PyComparisonOp, Representable, SelfIter, Unconstructible,
     },
     AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
     VirtualMachine,
@@ -537,7 +537,7 @@ impl PyPayload for PyLongRangeIterator {
     }
 }
 
-#[pyclass(with(Constructor, IterNext))]
+#[pyclass(with(Constructor, IterNext, Iterable))]
 impl PyLongRangeIterator {
     #[pymethod(magic)]
     fn length_hint(&self) -> BigInt {
@@ -568,7 +568,7 @@ impl PyLongRangeIterator {
 }
 impl Unconstructible for PyLongRangeIterator {}
 
-impl IterNextIterable for PyLongRangeIterator {}
+impl SelfIter for PyLongRangeIterator {}
 impl IterNext for PyLongRangeIterator {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         // TODO: In pathological case (index == usize::MAX) this can wrap around
@@ -602,7 +602,7 @@ impl PyPayload for PyRangeIterator {
     }
 }
 
-#[pyclass(with(Constructor, IterNext))]
+#[pyclass(with(Constructor, IterNext, Iterable))]
 impl PyRangeIterator {
     #[pymethod(magic)]
     fn length_hint(&self) -> usize {
@@ -634,7 +634,7 @@ impl PyRangeIterator {
 }
 impl Unconstructible for PyRangeIterator {}
 
-impl IterNextIterable for PyRangeIterator {}
+impl SelfIter for PyRangeIterator {}
 impl IterNext for PyRangeIterator {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         // TODO: In pathological case (index == usize::MAX) this can wrap around
diff --git a/vm/src/builtins/set.rs b/vm/src/builtins/set.rs
index 003b0496ed..29ead02915 100644
--- a/vm/src/builtins/set.rs
+++ b/vm/src/builtins/set.rs
@@ -16,8 +16,8 @@ use crate::{
     recursion::ReprGuard,
     types::AsNumber,
     types::{
-        AsSequence, Comparable, Constructor, Hashable, Initializer, IterNext, IterNextIterable,
-        Iterable, PyComparisonOp, Representable, Unconstructible,
+        AsSequence, Comparable, Constructor, Hashable, Initializer, IterNext, Iterable,
+        PyComparisonOp, Representable, SelfIter, Unconstructible,
     },
     utils::collection_repr,
     vm::VirtualMachine,
@@ -1232,7 +1232,7 @@ impl PyPayload for PySetIterator {
     }
 }
 
-#[pyclass(with(Constructor, IterNext))]
+#[pyclass(with(Constructor, IterNext, Iterable))]
 impl PySetIterator {
     #[pymethod(magic)]
     fn length_hint(&self) -> usize {
@@ -1257,7 +1257,7 @@ impl PySetIterator {
 }
 impl Unconstructible for PySetIterator {}
 
-impl IterNextIterable for PySetIterator {}
+impl SelfIter for PySetIterator {}
 impl IterNext for PySetIterator {
     fn next(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         let mut internal = zelf.internal.lock();
diff --git a/vm/src/builtins/staticmethod.rs b/vm/src/builtins/staticmethod.rs
index bb99960ce3..59a5b18b5d 100644
--- a/vm/src/builtins/staticmethod.rs
+++ b/vm/src/builtins/staticmethod.rs
@@ -1,9 +1,8 @@
-use super::{PyStr, PyStrInterned, PyType, PyTypeRef};
+use super::{PyStr, PyType, PyTypeRef};
 use crate::{
-    builtins::builtin_func::PyBuiltinMethod,
     class::PyClassImpl,
     common::lock::PyMutex,
-    function::{FuncArgs, IntoPyNativeFunc},
+    function::FuncArgs,
     types::{Callable, Constructor, GetDescriptor, Initializer, Representable},
     Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
 };
@@ -73,27 +72,6 @@ impl PyStaticMethod {
     }
 }
 
-impl PyStaticMethod {
-    pub fn new_builtin_ref<F, FKind>(
-        name: &'static PyStrInterned,
-        class: &'static Py<PyType>,
-        f: F,
-        ctx: &Context,
-    ) -> PyRef<Self>
-    where
-        F: IntoPyNativeFunc<FKind>,
-    {
-        let callable = PyBuiltinMethod::new_ref(name, class, f, ctx).into();
-        PyRef::new_ref(
-            Self {
-                callable: PyMutex::new(callable),
-            },
-            ctx.types.staticmethod_type.to_owned(),
-            None,
-        )
-    }
-}
-
 impl Initializer for PyStaticMethod {
     type Args = PyObjectRef;
 
diff --git a/vm/src/builtins/str.rs b/vm/src/builtins/str.rs
index 1e29c8d22a..bdb32981de 100644
--- a/vm/src/builtins/str.rs
+++ b/vm/src/builtins/str.rs
@@ -20,8 +20,8 @@ use crate::{
     sequence::SequenceExt,
     sliceable::{SequenceIndex, SliceableSequenceOp},
     types::{
-        AsMapping, AsNumber, AsSequence, Comparable, Constructor, Hashable, IterNext,
-        IterNextIterable, Iterable, PyComparisonOp, Representable, Unconstructible,
+        AsMapping, AsNumber, AsSequence, Comparable, Constructor, Hashable, IterNext, Iterable,
+        PyComparisonOp, Representable, SelfIter, Unconstructible,
     },
     AsObject, Context, Py, PyExact, PyObject, PyObjectRef, PyPayload, PyRef, PyRefExact, PyResult,
     TryFromBorrowedObject, VirtualMachine,
@@ -206,7 +206,7 @@ impl PyPayload for PyStrIterator {
     }
 }
 
-#[pyclass(with(Constructor, IterNext))]
+#[pyclass(with(Constructor, IterNext, Iterable))]
 impl PyStrIterator {
     #[pymethod(magic)]
     fn length_hint(&self) -> usize {
@@ -232,7 +232,7 @@ impl PyStrIterator {
 }
 impl Unconstructible for PyStrIterator {}
 
-impl IterNextIterable for PyStrIterator {}
+impl SelfIter for PyStrIterator {}
 impl IterNext for PyStrIterator {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         let mut internal = zelf.internal.lock();
diff --git a/vm/src/builtins/super.rs b/vm/src/builtins/super.rs
index 19377305ec..a7eefea321 100644
--- a/vm/src/builtins/super.rs
+++ b/vm/src/builtins/super.rs
@@ -153,9 +153,8 @@ impl PySuper {
 impl GetAttr for PySuper {
     fn getattro(zelf: &Py<Self>, name: &Py<PyStr>, vm: &VirtualMachine) -> PyResult {
         let skip = |zelf: &Py<Self>, name| zelf.as_object().generic_getattr(name, vm);
-        let obj = zelf.inner.read().obj.clone();
-        let (obj, start_type): (PyObjectRef, PyTypeRef) = match obj {
-            Some(o) => o,
+        let (obj, start_type): (PyObjectRef, PyTypeRef) = match &zelf.inner.read().obj {
+            Some(o) => o.clone(),
             None => return skip(zelf, name),
         };
         // We want __class__ to return the class of the super object
diff --git a/vm/src/builtins/tuple.rs b/vm/src/builtins/tuple.rs
index 59133b0876..63cd3e4335 100644
--- a/vm/src/builtins/tuple.rs
+++ b/vm/src/builtins/tuple.rs
@@ -12,8 +12,8 @@ use crate::{
     sequence::{OptionalRangeArgs, SequenceExt},
     sliceable::{SequenceIndex, SliceableSequenceOp},
     types::{
-        AsMapping, AsSequence, Comparable, Constructor, Hashable, IterNext, IterNextIterable,
-        Iterable, PyComparisonOp, Representable, Unconstructible,
+        AsMapping, AsSequence, Comparable, Constructor, Hashable, IterNext, Iterable,
+        PyComparisonOp, Representable, SelfIter, Unconstructible,
     },
     utils::collection_repr,
     vm::VirtualMachine,
@@ -434,7 +434,7 @@ impl PyPayload for PyTupleIterator {
     }
 }
 
-#[pyclass(with(Constructor, IterNext))]
+#[pyclass(with(Constructor, IterNext, Iterable))]
 impl PyTupleIterator {
     #[pymethod(magic)]
     fn length_hint(&self) -> usize {
@@ -457,7 +457,7 @@ impl PyTupleIterator {
 }
 impl Unconstructible for PyTupleIterator {}
 
-impl IterNextIterable for PyTupleIterator {}
+impl SelfIter for PyTupleIterator {}
 impl IterNext for PyTupleIterator {
     fn next(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         zelf.internal.lock().next(|tuple, pos| {
diff --git a/vm/src/builtins/type.rs b/vm/src/builtins/type.rs
index 5aca7ed07a..e0da4f7dc4 100644
--- a/vm/src/builtins/type.rs
+++ b/vm/src/builtins/type.rs
@@ -1,11 +1,12 @@
 use super::{
-    mappingproxy::PyMappingProxy, object, union_, PyClassMethod, PyDictRef, PyList, PyStaticMethod,
-    PyStr, PyStrInterned, PyStrRef, PyTuple, PyTupleRef, PyWeak,
+    mappingproxy::PyMappingProxy, object, union_, PyClassMethod, PyDictRef, PyList, PyStr,
+    PyStrInterned, PyStrRef, PyTuple, PyTupleRef, PyWeak,
 };
 use crate::{
     builtins::{
         descriptor::{
-            DescrObject, MemberGetter, MemberKind, MemberSetter, PyMemberDef, PyMemberDescriptor,
+            MemberGetter, MemberKind, MemberSetter, PyDescriptorOwned, PyMemberDef,
+            PyMemberDescriptor,
         },
         function::PyCellRef,
         tuple::{IntoPyTuple, PyTupleTyped},
@@ -18,7 +19,7 @@ use crate::{
         lock::{PyRwLock, PyRwLockReadGuard},
     },
     convert::ToPyResult,
-    function::{FuncArgs, KwArgs, OptionalArg, PySetterValue},
+    function::{FuncArgs, KwArgs, OptionalArg, PyMethodDef, PySetterValue},
     identifier,
     object::{Traverse, TraverseFn},
     protocol::{PyIterReturn, PyMappingMethods, PyNumberMethods, PySequenceMethods},
@@ -426,6 +427,13 @@ impl Py<PyType> {
     pub fn iter_base_chain(&self) -> impl Iterator<Item = &Py<PyType>> {
         std::iter::successors(Some(self), |cls| cls.base.as_deref())
     }
+
+    pub fn extend_methods(&'static self, method_defs: &'static [PyMethodDef], ctx: &Context) {
+        for method_def in method_defs {
+            let method = method_def.to_proper_method(self, ctx);
+            self.set_attr(ctx.intern_str(method_def.name), method);
+        }
+    }
 }
 
 #[pyclass(
@@ -696,11 +704,6 @@ impl PyType {
         };
 
         let mut attributes = dict.to_attributes(vm);
-        if let Some(f) = attributes.get_mut(identifier!(vm, __new__)) {
-            if f.class().is(vm.ctx.types.function_type) {
-                *f = PyStaticMethod::from(f.clone()).into_pyobject(vm);
-            }
-        }
 
         if let Some(f) = attributes.get_mut(identifier!(vm, __init_subclass__)) {
             if f.class().is(vm.ctx.types.function_type) {
@@ -813,7 +816,7 @@ impl PyType {
                 };
                 let member_descriptor: PyRef<PyMemberDescriptor> =
                     vm.ctx.new_pyref(PyMemberDescriptor {
-                        common: DescrObject {
+                        common: PyDescriptorOwned {
                             typ: typ.clone(),
                             name: vm.ctx.intern_str(member.as_str()),
                             qualname: PyRwLock::new(None),
@@ -977,11 +980,7 @@ fn get_signature(doc: &str) -> Option<&str> {
 fn find_signature<'a>(name: &str, doc: &'a str) -> Option<&'a str> {
     let name = name.rsplit('.').next().unwrap();
     let doc = doc.strip_prefix(name)?;
-    if !doc.starts_with('(') {
-        None
-    } else {
-        Some(doc)
-    }
+    doc.starts_with('(').then_some(doc)
 }
 
 pub(crate) fn get_text_signature_from_internal_doc<'a>(
diff --git a/vm/src/builtins/zip.rs b/vm/src/builtins/zip.rs
index d8b28e8eb7..56c88f14c6 100644
--- a/vm/src/builtins/zip.rs
+++ b/vm/src/builtins/zip.rs
@@ -4,7 +4,7 @@ use crate::{
     class::PyClassImpl,
     function::{ArgIntoBool, OptionalArg, PosArgs},
     protocol::{PyIter, PyIterReturn},
-    types::{Constructor, IterNext, IterNextIterable},
+    types::{Constructor, IterNext, Iterable, SelfIter},
     AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine,
 };
 use rustpython_common::atomic::{self, PyAtomic, Radium};
@@ -41,7 +41,7 @@ impl Constructor for PyZip {
     }
 }
 
-#[pyclass(with(IterNext, Constructor), flags(BASETYPE))]
+#[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
 impl PyZip {
     #[pymethod(magic)]
     fn reduce(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
@@ -68,7 +68,7 @@ impl PyZip {
     }
 }
 
-impl IterNextIterable for PyZip {}
+impl SelfIter for PyZip {}
 impl IterNext for PyZip {
     fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
         if zelf.iterators.is_empty() {
diff --git a/vm/src/class.rs b/vm/src/class.rs
index a9a00c4a8d..8b10c5e626 100644
--- a/vm/src/class.rs
+++ b/vm/src/class.rs
@@ -1,7 +1,8 @@
 //! Utilities to define a new Python class
 
 use crate::{
-    builtins::{PyBaseObject, PyBoundMethod, PyType, PyTypeRef},
+    builtins::{PyBaseObject, PyType, PyTypeRef},
+    function::PyMethodDef,
     identifier,
     object::Py,
     types::{hash_not_implemented, PyTypeFlags, PyTypeSlots},
@@ -69,8 +70,6 @@ pub trait PyClassDef {
 pub trait PyClassImpl: PyClassDef {
     const TP_FLAGS: PyTypeFlags = PyTypeFlags::DEFAULT;
 
-    fn impl_extend_class(ctx: &Context, class: &'static Py<PyType>);
-
     fn extend_class(ctx: &Context, class: &'static Py<PyType>) {
         #[cfg(debug_assertions)]
         {
@@ -102,16 +101,21 @@ pub trait PyClassImpl: PyClassDef {
                 ctx.new_str(module_name).into(),
             );
         }
-        let bound_new = PyBoundMethod::new_ref(
-            class.to_owned().into(),
-            ctx.slot_new_wrapper.clone().into(),
-            ctx,
-        );
-        class.set_attr(identifier!(ctx, __new__), bound_new.into());
+
+        if class.slots.new.load().is_some() {
+            let bound_new = Context::genesis().slot_new_wrapper.build_bound_method(
+                ctx,
+                class.to_owned().into(),
+                class,
+            );
+            class.set_attr(identifier!(ctx, __new__), bound_new.into());
+        }
 
         if class.slots.hash.load().map_or(0, |h| h as usize) == hash_not_implemented as usize {
             class.set_attr(ctx.names.__hash__, ctx.none.clone().into());
         }
+
+        class.extend_methods(class.slots.methods, ctx);
     }
 
     fn make_class(ctx: &Context) -> PyTypeRef
@@ -130,14 +134,20 @@ pub trait PyClassImpl: PyClassDef {
         .to_owned()
     }
 
+    fn impl_extend_class(ctx: &Context, class: &'static Py<PyType>);
+    fn impl_extend_method_def(method_defs: &mut Vec<PyMethodDef>);
     fn extend_slots(slots: &mut PyTypeSlots);
 
     fn make_slots() -> PyTypeSlots {
+        let mut method_defs = Vec::new();
+        Self::impl_extend_method_def(&mut method_defs);
+
         let mut slots = PyTypeSlots {
             flags: Self::TP_FLAGS,
             name: Self::TP_NAME,
             basicsize: Self::BASICSIZE,
             doc: Self::DOC,
+            methods: Box::leak(method_defs.into_boxed_slice()),
             ..Default::default()
         };
 
diff --git a/vm/src/codecs.rs b/vm/src/codecs.rs
index 29b58749f0..ff7bc48915 100644
--- a/vm/src/codecs.rs
+++ b/vm/src/codecs.rs
@@ -2,6 +2,7 @@ use crate::{
     builtins::{PyBaseExceptionRef, PyBytesRef, PyStr, PyStrRef, PyTuple, PyTupleRef},
     common::{ascii, lock::PyRwLock},
     convert::ToPyObject,
+    function::PyMethodDef,
     AsObject, Context, PyObject, PyObjectRef, PyPayload, PyResult, TryFromObject, VirtualMachine,
 };
 use std::{borrow::Cow, collections::HashMap, fmt::Write, ops::Range};
@@ -142,33 +143,33 @@ impl ToPyObject for PyCodec {
 
 impl CodecsRegistry {
     pub(crate) fn new(ctx: &Context) -> Self {
+        ::rustpython_vm::common::static_cell! {
+            static METHODS: Box<[PyMethodDef]>;
+        }
+
+        let methods = METHODS.get_or_init(|| {
+            crate::define_methods![
+                "strict_errors" => strict_errors as EMPTY,
+                "ignore_errors" => ignore_errors as EMPTY,
+                "replace_errors" => replace_errors as EMPTY,
+                "xmlcharrefreplace_errors" => xmlcharrefreplace_errors as EMPTY,
+                "backslashreplace_errors" => backslashreplace_errors as EMPTY,
+                "namereplace_errors" => namereplace_errors as EMPTY,
+                "surrogatepass_errors" => surrogatepass_errors as EMPTY,
+                "surrogateescape_errors" => surrogateescape_errors as EMPTY
+            ]
+            .into_boxed_slice()
+        });
+
         let errors = [
-            ("strict", ctx.new_function("strict_errors", strict_errors)),
-            ("ignore", ctx.new_function("ignore_errors", ignore_errors)),
-            (
-                "replace",
-                ctx.new_function("replace_errors", replace_errors),
-            ),
-            (
-                "xmlcharrefreplace",
-                ctx.new_function("xmlcharrefreplace_errors", xmlcharrefreplace_errors),
-            ),
-            (
-                "backslashreplace",
-                ctx.new_function("backslashreplace_errors", backslashreplace_errors),
-            ),
-            (
-                "namereplace",
-                ctx.new_function("namereplace_errors", namereplace_errors),
-            ),
-            (
-                "surrogatepass",
-                ctx.new_function("surrogatepass_errors", surrogatepass_errors),
-            ),
-            (
-                "surrogateescape",
-                ctx.new_function("surrogateescape_errors", surrogateescape_errors),
-            ),
+            ("strict", methods[0].build_function(ctx)),
+            ("ignore", methods[1].build_function(ctx)),
+            ("replace", methods[2].build_function(ctx)),
+            ("xmlcharrefreplace", methods[3].build_function(ctx)),
+            ("backslashreplace", methods[4].build_function(ctx)),
+            ("namereplace", methods[5].build_function(ctx)),
+            ("surrogatepass", methods[6].build_function(ctx)),
+            ("surrogateescape", methods[7].build_function(ctx)),
         ];
         let errors = errors
             .into_iter()
diff --git a/vm/src/exceptions.rs b/vm/src/exceptions.rs
index 3762ee5d06..9de24bb67b 100644
--- a/vm/src/exceptions.rs
+++ b/vm/src/exceptions.rs
@@ -3,8 +3,7 @@ use crate::common::{lock::PyRwLock, str::ReprOverflowError};
 use crate::object::{Traverse, TraverseFn};
 use crate::{
     builtins::{
-        traceback::PyTracebackRef, tuple::IntoPyTuple, PyNone, PyStr, PyStrRef, PyTuple,
-        PyTupleRef, PyType, PyTypeRef,
+        traceback::PyTracebackRef, PyNone, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef,
     },
     class::{PyClassImpl, StaticType},
     convert::{ToPyException, ToPyObject},
@@ -767,9 +766,8 @@ impl ExceptionZoo {
 
         extend_exception!(PyLookupError, ctx, excs.lookup_error);
         extend_exception!(PyIndexError, ctx, excs.index_error);
-        extend_exception!(PyKeyError, ctx, excs.key_error, {
-            "__str__" => ctx.new_method(identifier!(ctx, __str__), excs.key_error, key_error_str),
-        });
+
+        extend_exception!(PyKeyError, ctx, excs.key_error);
 
         extend_exception!(PyMemoryError, ctx, excs.memory_error);
         extend_exception!(PyNameError, ctx, excs.name_error, {
@@ -801,8 +799,6 @@ impl ExceptionZoo {
             "filename" => ctx.none(),
             // second exception filename
             "filename2" => ctx.none(),
-            "__str__" => ctx.new_method(identifier!(ctx, __str__), excs.os_error, os_error_str),
-            "__reduce__" => ctx.new_method(identifier!(ctx, __reduce__), excs.os_error, os_error_reduce),
         });
         // TODO: this isn't really accurate
         #[cfg(windows)]
@@ -895,84 +891,6 @@ fn make_arg_getter(idx: usize) -> impl Fn(PyBaseExceptionRef) -> Option<PyObject
     move |exc| exc.get_arg(idx)
 }
 
-fn key_error_str(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyStrRef {
-    let args = exc.args();
-    if args.len() == 1 {
-        vm.exception_args_as_string(args, false)
-            .into_iter()
-            .exactly_one()
-            .unwrap()
-    } else {
-        exc.str(vm)
-    }
-}
-
-fn os_error_str(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult<PyStrRef> {
-    let args = exc.args();
-    let obj = exc.as_object().to_owned();
-
-    if args.len() == 2 {
-        // SAFETY: len() == 2 is checked so get_arg 1 or 2 won't panic
-        let errno = exc.get_arg(0).unwrap().str(vm)?;
-        let msg = exc.get_arg(1).unwrap().str(vm)?;
-
-        let s = match obj.get_attr("filename", vm) {
-            Ok(filename) => match obj.get_attr("filename2", vm) {
-                Ok(filename2) => format!(
-                    "[Errno {}] {}: '{}' -> '{}'",
-                    errno,
-                    msg,
-                    filename.str(vm)?,
-                    filename2.str(vm)?
-                ),
-                Err(_) => format!("[Errno {}] {}: '{}'", errno, msg, filename.str(vm)?),
-            },
-            Err(_) => {
-                format!("[Errno {errno}] {msg}")
-            }
-        };
-        Ok(vm.ctx.new_str(s))
-    } else {
-        Ok(exc.str(vm))
-    }
-}
-
-fn os_error_reduce(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef {
-    let args = exc.args();
-    let obj = exc.as_object().to_owned();
-    let mut result: Vec<PyObjectRef> = vec![obj.class().to_owned().into()];
-
-    if args.len() >= 2 && args.len() <= 5 {
-        // SAFETY: len() == 2 is checked so get_arg 1 or 2 won't panic
-        let errno = exc.get_arg(0).unwrap();
-        let msg = exc.get_arg(1).unwrap();
-
-        if let Ok(filename) = obj.get_attr("filename", vm) {
-            if !vm.is_none(&filename) {
-                let mut args_reduced: Vec<PyObjectRef> = vec![errno, msg, filename];
-
-                if let Ok(filename2) = obj.get_attr("filename2", vm) {
-                    if !vm.is_none(&filename2) {
-                        args_reduced.push(filename2);
-                    }
-                }
-                result.push(args_reduced.into_pytuple(vm).into());
-            } else {
-                result.push(vm.new_tuple((errno, msg)).into());
-            }
-        } else {
-            result.push(vm.new_tuple((errno, msg)).into());
-        }
-    } else {
-        result.push(args.into());
-    }
-
-    if let Some(dict) = obj.dict().filter(|x| !x.is_empty()) {
-        result.push(dict.into());
-    }
-    result.into_pytuple(vm)
-}
-
 fn system_exit_code(exc: PyBaseExceptionRef) -> Option<PyObjectRef> {
     exc.args.read().first().map(|code| {
         match_class!(match code {
@@ -1137,13 +1055,16 @@ pub(super) mod types {
     use crate::common::lock::PyRwLock;
     #[cfg_attr(target_arch = "wasm32", allow(unused_imports))]
     use crate::{
-        builtins::{traceback::PyTracebackRef, PyInt, PyTupleRef, PyTypeRef},
+        builtins::{
+            traceback::PyTracebackRef, tuple::IntoPyTuple, PyInt, PyStrRef, PyTupleRef, PyTypeRef,
+        },
         convert::ToPyResult,
         function::FuncArgs,
         types::{Constructor, Initializer},
-        PyObjectRef, PyRef, PyResult, VirtualMachine,
+        AsObject, PyObjectRef, PyRef, PyResult, VirtualMachine,
     };
     use crossbeam_utils::atomic::AtomicCell;
+    use itertools::Itertools;
 
     // This module is designed to be used as `use builtins::*;`.
     // Do not add any pub symbols not included in builtins module.
@@ -1275,10 +1196,26 @@ pub(super) mod types {
     #[derive(Debug)]
     pub struct PyIndexError {}
 
-    #[pyexception(name, base = "PyLookupError", ctx = "key_error", impl)]
+    #[pyexception(name, base = "PyLookupError", ctx = "key_error")]
     #[derive(Debug)]
     pub struct PyKeyError {}
 
+    #[pyexception]
+    impl PyKeyError {
+        #[pymethod(magic)]
+        fn str(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyStrRef {
+            let args = exc.args();
+            if args.len() == 1 {
+                vm.exception_args_as_string(args, false)
+                    .into_iter()
+                    .exactly_one()
+                    .unwrap()
+            } else {
+                exc.str(vm)
+            }
+        }
+    }
+
     #[pyexception(name, base = "PyException", ctx = "memory_error", impl)]
     #[derive(Debug)]
     pub struct PyMemoryError {}
@@ -1347,6 +1284,74 @@ pub(super) mod types {
             }
             PyBaseException::slot_init(zelf, new_args, vm)
         }
+
+        #[pymethod(magic)]
+        fn str(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult<PyStrRef> {
+            let args = exc.args();
+            let obj = exc.as_object().to_owned();
+
+            if args.len() == 2 {
+                // SAFETY: len() == 2 is checked so get_arg 1 or 2 won't panic
+                let errno = exc.get_arg(0).unwrap().str(vm)?;
+                let msg = exc.get_arg(1).unwrap().str(vm)?;
+
+                let s = match obj.get_attr("filename", vm) {
+                    Ok(filename) => match obj.get_attr("filename2", vm) {
+                        Ok(filename2) => format!(
+                            "[Errno {}] {}: '{}' -> '{}'",
+                            errno,
+                            msg,
+                            filename.str(vm)?,
+                            filename2.str(vm)?
+                        ),
+                        Err(_) => format!("[Errno {}] {}: '{}'", errno, msg, filename.str(vm)?),
+                    },
+                    Err(_) => {
+                        format!("[Errno {errno}] {msg}")
+                    }
+                };
+                Ok(vm.ctx.new_str(s))
+            } else {
+                Ok(exc.str(vm))
+            }
+        }
+
+        #[pymethod(magic)]
+        fn reduce(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef {
+            let args = exc.args();
+            let obj = exc.as_object().to_owned();
+            let mut result: Vec<PyObjectRef> = vec![obj.class().to_owned().into()];
+
+            if args.len() >= 2 && args.len() <= 5 {
+                // SAFETY: len() == 2 is checked so get_arg 1 or 2 won't panic
+                let errno = exc.get_arg(0).unwrap();
+                let msg = exc.get_arg(1).unwrap();
+
+                if let Ok(filename) = obj.get_attr("filename", vm) {
+                    if !vm.is_none(&filename) {
+                        let mut args_reduced: Vec<PyObjectRef> = vec![errno, msg, filename];
+
+                        if let Ok(filename2) = obj.get_attr("filename2", vm) {
+                            if !vm.is_none(&filename2) {
+                                args_reduced.push(filename2);
+                            }
+                        }
+                        result.push(args_reduced.into_pytuple(vm).into());
+                    } else {
+                        result.push(vm.new_tuple((errno, msg)).into());
+                    }
+                } else {
+                    result.push(vm.new_tuple((errno, msg)).into());
+                }
+            } else {
+                result.push(args.into());
+            }
+
+            if let Some(dict) = obj.dict().filter(|x| !x.is_empty()) {
+                result.push(dict.into());
+            }
+            result.into_pytuple(vm)
+        }
     }
 
     #[pyexception(name, base = "PyOSError", ctx = "blocking_io_error", impl)]
diff --git a/vm/src/frame.rs b/vm/src/frame.rs
index 3ec6ab7887..f2fd1756b3 100644
--- a/vm/src/frame.rs
+++ b/vm/src/frame.rs
@@ -1390,13 +1390,20 @@ impl ExecutingFrame<'_> {
         let func = self.pop_value();
         let is_method = self.pop_value().is(&vm.ctx.true_value);
         let target = self.pop_value();
-        let method = if is_method {
-            PyMethod::Function { target, func }
+
+        // TODO: It was PyMethod before #4873. Check if it's correct.
+        let func = if is_method {
+            if let Some(descr_get) = func.class().mro_find_map(|cls| cls.slots.descr_get.load()) {
+                let cls = target.class().to_owned().into();
+                descr_get(func, Some(target), Some(cls), vm)?
+            } else {
+                func
+            }
         } else {
             drop(target); // should be None
-            PyMethod::Attribute(func)
+            func
         };
-        let value = method.invoke(args, vm)?;
+        let value = func.call(args, vm)?;
         self.push_value(value);
         Ok(None)
     }
diff --git a/vm/src/function/builtin.rs b/vm/src/function/builtin.rs
index 3a1591cb45..e37270bcc9 100644
--- a/vm/src/function/builtin.rs
+++ b/vm/src/function/builtin.rs
@@ -6,7 +6,8 @@ use crate::{
 use std::marker::PhantomData;
 
 /// A built-in Python function.
-pub type PyNativeFunc = Box<py_dyn_fn!(dyn Fn(&VirtualMachine, FuncArgs) -> PyResult)>;
+// PyCFunction in CPython
+pub type PyNativeFn = py_dyn_fn!(dyn Fn(&VirtualMachine, FuncArgs) -> PyResult);
 
 /// Implemented by types that are or can generate built-in functions.
 ///
@@ -19,29 +20,30 @@ pub type PyNativeFunc = Box<py_dyn_fn!(dyn Fn(&VirtualMachine, FuncArgs) -> PyRe
 /// For example, anything from `Fn()` to `Fn(vm: &VirtualMachine) -> u32` to
 /// `Fn(PyIntRef, PyIntRef) -> String` to
 /// `Fn(&self, PyStrRef, FooOptions, vm: &VirtualMachine) -> PyResult<PyInt>`
-/// is `IntoPyNativeFunc`. If you do want a really general function signature, e.g.
+/// is `IntoPyNativeFn`. If you do want a really general function signature, e.g.
 /// to forward the args to another function, you can define a function like
 /// `Fn(FuncArgs [, &VirtualMachine]) -> ...`
 ///
 /// Note that the `Kind` type parameter is meaningless and should be considered
-/// an implementation detail; if you need to use `IntoPyNativeFunc` as a trait bound
+/// an implementation detail; if you need to use `IntoPyNativeFn` as a trait bound
 /// just pass an unconstrained generic type, e.g.
-/// `fn foo<F, FKind>(f: F) where F: IntoPyNativeFunc<FKind>`
-pub trait IntoPyNativeFunc<Kind>: Sized + PyThreadingConstraint + 'static {
+/// `fn foo<F, FKind>(f: F) where F: IntoPyNativeFn<FKind>`
+pub trait IntoPyNativeFn<Kind>: Sized + PyThreadingConstraint + 'static {
     fn call(&self, vm: &VirtualMachine, args: FuncArgs) -> PyResult;
-    /// `IntoPyNativeFunc::into_func()` generates a PyNativeFunc that performs the
+    /// `IntoPyNativeFn::into_func()` generates a PyNativeFn that performs the
     /// appropriate type and arity checking, any requested conversions, and then if
     /// successful calls the function with the extracted parameters.
-    fn into_func(self) -> PyNativeFunc {
-        Box::new(move |vm: &VirtualMachine, args| self.call(vm, args))
+    fn into_func(self) -> &'static PyNativeFn {
+        let boxed = Box::new(move |vm: &VirtualMachine, args| self.call(vm, args));
+        Box::leak(boxed)
     }
 }
 
 // TODO: once higher-rank trait bounds are stabilized, remove the `Kind` type
-// parameter and impl for F where F: for<T, R, VM> PyNativeFuncInternal<T, R, VM>
-impl<F, T, R, VM> IntoPyNativeFunc<(T, R, VM)> for F
+// parameter and impl for F where F: for<T, R, VM> PyNativeFnInternal<T, R, VM>
+impl<F, T, R, VM> IntoPyNativeFn<(T, R, VM)> for F
 where
-    F: PyNativeFuncInternal<T, R, VM>,
+    F: PyNativeFnInternal<T, R, VM>,
 {
     #[inline(always)]
     fn call(&self, vm: &VirtualMachine, args: FuncArgs) -> PyResult {
@@ -51,11 +53,11 @@ where
 
 mod sealed {
     use super::*;
-    pub trait PyNativeFuncInternal<T, R, VM>: Sized + PyThreadingConstraint + 'static {
+    pub trait PyNativeFnInternal<T, R, VM>: Sized + PyThreadingConstraint + 'static {
         fn call_(&self, vm: &VirtualMachine, args: FuncArgs) -> PyResult;
     }
 }
-use sealed::PyNativeFuncInternal;
+use sealed::PyNativeFnInternal;
 
 #[doc(hidden)]
 pub struct OwnedParam<T>(PhantomData<T>);
@@ -68,9 +70,9 @@ pub struct RefParam<T>(PhantomData<T>);
 // generate native python functions.
 //
 // Note that this could be done without a macro - it is simply to avoid repetition.
-macro_rules! into_py_native_func_tuple {
+macro_rules! into_py_native_fn_tuple {
     ($(($n:tt, $T:ident)),*) => {
-        impl<F, $($T,)* R> PyNativeFuncInternal<($(OwnedParam<$T>,)*), R, VirtualMachine> for F
+        impl<F, $($T,)* R> PyNativeFnInternal<($(OwnedParam<$T>,)*), R, VirtualMachine> for F
         where
             F: Fn($($T,)* &VirtualMachine) -> R + PyThreadingConstraint + 'static,
             $($T: FromArgs,)*
@@ -83,7 +85,7 @@ macro_rules! into_py_native_func_tuple {
             }
         }
 
-        impl<F, S, $($T,)* R> PyNativeFuncInternal<(BorrowedParam<S>, $(OwnedParam<$T>,)*), R, VirtualMachine> for F
+        impl<F, S, $($T,)* R> PyNativeFnInternal<(BorrowedParam<S>, $(OwnedParam<$T>,)*), R, VirtualMachine> for F
         where
             F: Fn(&Py<S>, $($T,)* &VirtualMachine) -> R + PyThreadingConstraint + 'static,
             S: PyPayload,
@@ -97,7 +99,7 @@ macro_rules! into_py_native_func_tuple {
             }
         }
 
-        impl<F, S, $($T,)* R> PyNativeFuncInternal<(RefParam<S>, $(OwnedParam<$T>,)*), R, VirtualMachine> for F
+        impl<F, S, $($T,)* R> PyNativeFnInternal<(RefParam<S>, $(OwnedParam<$T>,)*), R, VirtualMachine> for F
         where
             F: Fn(&S, $($T,)* &VirtualMachine) -> R + PyThreadingConstraint + 'static,
             S: PyPayload,
@@ -111,7 +113,7 @@ macro_rules! into_py_native_func_tuple {
             }
         }
 
-        impl<F, $($T,)* R> PyNativeFuncInternal<($(OwnedParam<$T>,)*), R, ()> for F
+        impl<F, $($T,)* R> PyNativeFnInternal<($(OwnedParam<$T>,)*), R, ()> for F
         where
             F: Fn($($T,)*) -> R + PyThreadingConstraint + 'static,
             $($T: FromArgs,)*
@@ -124,7 +126,7 @@ macro_rules! into_py_native_func_tuple {
             }
         }
 
-        impl<F, S, $($T,)* R> PyNativeFuncInternal<(BorrowedParam<S>, $(OwnedParam<$T>,)*), R, ()> for F
+        impl<F, S, $($T,)* R> PyNativeFnInternal<(BorrowedParam<S>, $(OwnedParam<$T>,)*), R, ()> for F
         where
             F: Fn(&Py<S>, $($T,)*) -> R + PyThreadingConstraint + 'static,
             S: PyPayload,
@@ -138,7 +140,7 @@ macro_rules! into_py_native_func_tuple {
             }
         }
 
-        impl<F, S, $($T,)* R> PyNativeFuncInternal<(RefParam<S>, $(OwnedParam<$T>,)*), R, ()> for F
+        impl<F, S, $($T,)* R> PyNativeFnInternal<(RefParam<S>, $(OwnedParam<$T>,)*), R, ()> for F
         where
             F: Fn(&S, $($T,)*) -> R + PyThreadingConstraint + 'static,
             S: PyPayload,
@@ -154,14 +156,14 @@ macro_rules! into_py_native_func_tuple {
     };
 }
 
-into_py_native_func_tuple!();
-into_py_native_func_tuple!((v1, T1));
-into_py_native_func_tuple!((v1, T1), (v2, T2));
-into_py_native_func_tuple!((v1, T1), (v2, T2), (v3, T3));
-into_py_native_func_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4));
-into_py_native_func_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4), (v5, T5));
-into_py_native_func_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4), (v5, T5), (v6, T6));
-into_py_native_func_tuple!(
+into_py_native_fn_tuple!();
+into_py_native_fn_tuple!((v1, T1));
+into_py_native_fn_tuple!((v1, T1), (v2, T2));
+into_py_native_fn_tuple!((v1, T1), (v2, T2), (v3, T3));
+into_py_native_fn_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4));
+into_py_native_fn_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4), (v5, T5));
+into_py_native_fn_tuple!((v1, T1), (v2, T2), (v3, T3), (v4, T4), (v5, T5), (v6, T6));
+into_py_native_fn_tuple!(
     (v1, T1),
     (v2, T2),
     (v3, T3),
@@ -176,8 +178,8 @@ mod tests {
     use super::*;
 
     #[test]
-    fn test_intonativefunc_noalloc() {
-        let check_zst = |f: PyNativeFunc| assert_eq!(std::mem::size_of_val(f.as_ref()), 0);
+    fn test_into_native_fn_noalloc() {
+        let check_zst = |f: &'static PyNativeFn| assert_eq!(std::mem::size_of_val(f), 0);
         fn py_func(_b: bool, _vm: &crate::VirtualMachine) -> i32 {
             1
         }
diff --git a/vm/src/function/method.rs b/vm/src/function/method.rs
new file mode 100644
index 0000000000..8c8146bbd5
--- /dev/null
+++ b/vm/src/function/method.rs
@@ -0,0 +1,259 @@
+use crate::{
+    builtins::{
+        builtin_func::{PyNativeFunction, PyNativeMethod},
+        descriptor::PyMethodDescriptor,
+        PyType,
+    },
+    function::{IntoPyNativeFn, PyNativeFn},
+    Context, Py, PyObjectRef, PyPayload, PyRef, VirtualMachine,
+};
+
+bitflags::bitflags! {
+    // METH_XXX flags in CPython
+    pub struct PyMethodFlags: u32 {
+        // const VARARGS = 0x0001;
+        // const KEYWORDS = 0x0002;
+        // METH_NOARGS and METH_O must not be combined with the flags above.
+        // const NOARGS = 0x0004;
+        // const O = 0x0008;
+
+        // METH_CLASS and METH_STATIC are a little different; these control
+        // the construction of methods for a class.  These cannot be used for
+        // functions in modules.
+        const CLASS = 0x0010;
+        const STATIC = 0x0020;
+
+        // METH_COEXIST allows a method to be entered even though a slot has
+        // already filled the entry.  When defined, the flag allows a separate
+        // method, "__contains__" for example, to coexist with a defined
+        // slot like sq_contains.
+        // const COEXIST = 0x0040;
+
+        // if not Py_LIMITED_API
+        // const FASTCALL = 0x0080;
+
+        // This bit is preserved for Stackless Python
+        // const STACKLESS = 0x0100;
+
+        // METH_METHOD means the function stores an
+        // additional reference to the class that defines it;
+        // both self and class are passed to it.
+        // It uses PyCMethodObject instead of PyCFunctionObject.
+        // May not be combined with METH_NOARGS, METH_O, METH_CLASS or METH_STATIC.
+        const METHOD = 0x0200;
+    }
+}
+
+impl PyMethodFlags {
+    // FIXME: macro temp
+    pub const EMPTY: Self = Self::empty();
+}
+
+#[macro_export]
+macro_rules! define_methods {
+    // TODO: more flexible syntax
+    ($($name:literal => $func:ident as $flags:ident),+) => {
+        vec![ $( $crate::function::PyMethodDef {
+            name: $name,
+            func: $crate::function::IntoPyNativeFn::into_func($func),
+            flags: $crate::function::PyMethodFlags::$flags,
+            doc: None,
+        }),+ ]
+    };
+}
+
+#[derive(Clone)]
+pub struct PyMethodDef {
+    pub name: &'static str, // TODO: interned
+    pub func: &'static PyNativeFn,
+    pub flags: PyMethodFlags,
+    pub doc: Option<&'static str>, // TODO: interned
+}
+
+impl PyMethodDef {
+    #[inline]
+    pub fn new<Kind>(
+        name: &'static str,
+        func: impl IntoPyNativeFn<Kind>,
+        flags: PyMethodFlags,
+        doc: Option<&'static str>,
+    ) -> Self {
+        Self {
+            name,
+            func: func.into_func(),
+            flags,
+            doc,
+        }
+    }
+    pub fn to_proper_method(
+        &'static self,
+        class: &'static Py<PyType>,
+        ctx: &Context,
+    ) -> PyObjectRef {
+        if self.flags.contains(PyMethodFlags::METHOD) {
+            self.build_method(ctx, class).into()
+        } else if self.flags.contains(PyMethodFlags::CLASS) {
+            self.build_classmethod(ctx, class).into()
+        } else if self.flags.contains(PyMethodFlags::STATIC) {
+            self.build_staticmethod(ctx, class).into()
+        } else {
+            unreachable!();
+        }
+    }
+    pub fn to_function(&'static self) -> PyNativeFunction {
+        PyNativeFunction {
+            zelf: None,
+            value: self,
+            module: None,
+        }
+    }
+    pub fn to_method(
+        &'static self,
+        class: &'static Py<PyType>,
+        ctx: &Context,
+    ) -> PyMethodDescriptor {
+        PyMethodDescriptor::new(self, class, ctx)
+    }
+    pub fn to_bound_method(
+        &'static self,
+        obj: PyObjectRef,
+        class: &'static Py<PyType>,
+    ) -> PyNativeMethod {
+        PyNativeMethod {
+            func: PyNativeFunction {
+                zelf: Some(obj),
+                value: self,
+                module: None,
+            },
+            class,
+        }
+    }
+    pub fn build_function(&'static self, ctx: &Context) -> PyRef<PyNativeFunction> {
+        self.to_function().into_ref(ctx)
+    }
+    pub fn build_bound_function(
+        &'static self,
+        ctx: &Context,
+        obj: PyObjectRef,
+    ) -> PyRef<PyNativeFunction> {
+        let function = PyNativeFunction {
+            zelf: Some(obj),
+            value: self,
+            module: None,
+        };
+        PyRef::new_ref(
+            function,
+            ctx.types.builtin_function_or_method_type.to_owned(),
+            None,
+        )
+    }
+    pub fn build_method(
+        &'static self,
+        ctx: &Context,
+        class: &'static Py<PyType>,
+    ) -> PyRef<PyMethodDescriptor> {
+        debug_assert!(self.flags.contains(PyMethodFlags::METHOD));
+        PyRef::new_ref(
+            self.to_method(class, ctx),
+            ctx.types.method_descriptor_type.to_owned(),
+            None,
+        )
+    }
+    pub fn build_bound_method(
+        &'static self,
+        ctx: &Context,
+        obj: PyObjectRef,
+        class: &'static Py<PyType>,
+    ) -> PyRef<PyNativeMethod> {
+        PyRef::new_ref(
+            self.to_bound_method(obj, class),
+            ctx.types.builtin_method_type.to_owned(),
+            None,
+        )
+    }
+    pub fn build_classmethod(
+        &'static self,
+        ctx: &Context,
+        class: &'static Py<PyType>,
+    ) -> PyRef<PyMethodDescriptor> {
+        PyRef::new_ref(
+            self.to_method(class, ctx),
+            ctx.types.method_descriptor_type.to_owned(),
+            None,
+        )
+    }
+    pub fn build_staticmethod(
+        &'static self,
+        ctx: &Context,
+        class: &'static Py<PyType>,
+    ) -> PyRef<PyNativeMethod> {
+        debug_assert!(self.flags.contains(PyMethodFlags::STATIC));
+        let func = self.to_function();
+        PyNativeMethod { func, class }.into_ref(ctx)
+    }
+}
+
+impl std::fmt::Debug for PyMethodDef {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("PyMethodDef")
+            .field("name", &self.name)
+            .field(
+                "func",
+                &(unsafe { std::mem::transmute::<_, [usize; 2]>(self.func)[1] as *const u8 }),
+            )
+            .field("flags", &self.flags)
+            .field("doc", &self.doc)
+            .finish()
+    }
+}
+
+// This is not a part of CPython API.
+// But useful to support dynamically generated methods
+#[pyclass(name, module = false, ctx = "method_def")]
+#[derive(Debug)]
+pub struct HeapMethodDef {
+    method: PyMethodDef,
+}
+
+impl HeapMethodDef {
+    pub fn new(method: PyMethodDef) -> Self {
+        Self { method }
+    }
+}
+
+impl Py<HeapMethodDef> {
+    pub(crate) unsafe fn method(&self) -> &'static PyMethodDef {
+        &*(&self.method as *const _)
+    }
+
+    pub fn build_function(&self, vm: &VirtualMachine) -> PyRef<PyNativeFunction> {
+        let function = unsafe { self.method() }.to_function();
+        let dict = vm.ctx.new_dict();
+        dict.set_item("__method_def__", self.to_owned().into(), vm)
+            .unwrap();
+        PyRef::new_ref(
+            function,
+            vm.ctx.types.builtin_function_or_method_type.to_owned(),
+            Some(dict),
+        )
+    }
+
+    pub fn build_method(
+        &self,
+        class: &'static Py<PyType>,
+        vm: &VirtualMachine,
+    ) -> PyRef<PyMethodDescriptor> {
+        let function = unsafe { self.method() }.to_method(class, &vm.ctx);
+        let dict = vm.ctx.new_dict();
+        dict.set_item("__method_def__", self.to_owned().into(), vm)
+            .unwrap();
+        PyRef::new_ref(
+            function,
+            vm.ctx.types.method_descriptor_type.to_owned(),
+            Some(dict),
+        )
+    }
+}
+
+#[pyclass]
+impl HeapMethodDef {}
diff --git a/vm/src/function/mod.rs b/vm/src/function/mod.rs
index 0521c06e49..409e4f1dbd 100644
--- a/vm/src/function/mod.rs
+++ b/vm/src/function/mod.rs
@@ -5,6 +5,7 @@ mod builtin;
 mod either;
 mod fspath;
 mod getset;
+mod method;
 mod number;
 mod protocol;
 
@@ -15,11 +16,12 @@ pub use argument::{
 pub use arithmetic::{PyArithmeticValue, PyComparisonValue};
 pub use buffer::{ArgAsciiBuffer, ArgBytesLike, ArgMemoryBuffer, ArgStrOrBytesLike};
 pub(self) use builtin::{BorrowedParam, OwnedParam, RefParam};
-pub use builtin::{IntoPyNativeFunc, PyNativeFunc};
+pub use builtin::{IntoPyNativeFn, PyNativeFn};
 pub use either::Either;
 pub use fspath::FsPath;
 pub use getset::PySetterValue;
 pub(super) use getset::{IntoPyGetterFunc, IntoPySetterFunc, PyGetterFunc, PySetterFunc};
+pub use method::{HeapMethodDef, PyMethodDef, PyMethodFlags};
 pub use number::{ArgIndex, ArgIntoBool, ArgIntoComplex, ArgIntoFloat, ArgPrimitiveIndex, ArgSize};
 pub use protocol::{ArgCallable, ArgIterable, ArgMapping, ArgSequence};
 
diff --git a/vm/src/macros.rs b/vm/src/macros.rs
index e92959198c..6e1953436c 100644
--- a/vm/src/macros.rs
+++ b/vm/src/macros.rs
@@ -2,7 +2,7 @@
 macro_rules! extend_module {
     ( $vm:expr, $module:expr, { $($name:expr => $value:expr),* $(,)? }) => {{
         $(
-            $vm.__module_set_attr($module, $name, $value).unwrap();
+            $vm.__module_set_attr($module, $vm.ctx.intern_str($name), $value).unwrap();
         )*
     }};
 }
@@ -218,12 +218,13 @@ macro_rules! named_function {
         #[allow(unused_variables)] // weird lint, something to do with paste probably
         let ctx: &$crate::Context = &$ctx;
         $crate::__exports::paste::expr! {
-            ctx.make_func_def(
-                ctx.intern_str(stringify!($func)),
+            ctx.new_method_def(
+                stringify!($func),
                 [<$module _ $func>],
+                ::rustpython_vm::function::PyMethodFlags::empty(),
             )
-            .into_function()
-            .with_module(ctx.new_str(stringify!($module).to_owned()).into())
+            .to_function()
+            .with_module(ctx.intern_str(stringify!($module)).into())
             .into_ref(ctx)
         }
     }};
diff --git a/vm/src/object/core.rs b/vm/src/object/core.rs
index 0ca1e248f5..c5e42229d3 100644
--- a/vm/src/object/core.rs
+++ b/vm/src/object/core.rs
@@ -551,7 +551,12 @@ impl PyObjectRef {
     /// T must be the exact payload type
     #[inline(always)]
     pub unsafe fn downcast_unchecked<T: PyObjectPayload>(self) -> PyRef<T> {
-        PyRef::from_obj_unchecked(self)
+        // PyRef::from_obj_unchecked(self)
+        // manual impl to avoid assertion
+        let obj = ManuallyDrop::new(self);
+        PyRef {
+            ptr: obj.ptr.cast(),
+        }
     }
 
     /// # Safety
diff --git a/vm/src/object/payload.rs b/vm/src/object/payload.rs
index 0ea79c6741..d5f3d6330b 100644
--- a/vm/src/object/payload.rs
+++ b/vm/src/object/payload.rs
@@ -80,3 +80,7 @@ pub trait PyObjectPayload:
 }
 
 impl<T: PyPayload + 'static> PyObjectPayload for T {}
+
+pub trait SlotOffset {
+    fn offset() -> usize;
+}
diff --git a/vm/src/protocol/callable.rs b/vm/src/protocol/callable.rs
index eb404d0214..ba20f2d326 100644
--- a/vm/src/protocol/callable.rs
+++ b/vm/src/protocol/callable.rs
@@ -16,6 +16,7 @@ impl PyObject {
     }
 
     /// PyObject_Call*Arg* series
+    #[inline]
     pub fn call(&self, args: impl IntoFuncArgs, vm: &VirtualMachine) -> PyResult {
         let args = args.into_args(vm);
         self.call_with_args(args, vm)
diff --git a/vm/src/stdlib/builtins.rs b/vm/src/stdlib/builtins.rs
index a7522f097d..e14a6847bb 100644
--- a/vm/src/stdlib/builtins.rs
+++ b/vm/src/stdlib/builtins.rs
@@ -2,6 +2,7 @@
 //!
 //! Implements the list of [builtin Python functions](https://docs.python.org/3/library/builtins.html).
 use crate::{builtins::PyModule, class::PyClassImpl, Py, VirtualMachine};
+pub(crate) use builtins::{__module_def, DOC};
 pub use builtins::{ascii, print};
 
 #[pymodule]
diff --git a/vm/src/stdlib/collections.rs b/vm/src/stdlib/collections.rs
index 5935b124f9..e416792d05 100644
--- a/vm/src/stdlib/collections.rs
+++ b/vm/src/stdlib/collections.rs
@@ -16,8 +16,8 @@ mod _collections {
         sequence::{MutObjectSequenceOp, OptionalRangeArgs},
         sliceable::SequenceIndexOp,
         types::{
-            AsSequence, Comparable, Constructor, Initializer, IterNext, IterNextIterable, Iterable,
-            PyComparisonOp, Representable,
+            AsSequence, Comparable, Constructor, Initializer, IterNext, Iterable, PyComparisonOp,
+            Representable, SelfIter,
         },
         utils::collection_repr,
         AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine,
@@ -623,7 +623,7 @@ mod _collections {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor))]
+    #[pyclass(with(IterNext, Iterable, Constructor))]
     impl PyDequeIterator {
         pub(crate) fn new(deque: PyDequeRef) -> Self {
             PyDequeIterator {
@@ -654,7 +654,7 @@ mod _collections {
         }
     }
 
-    impl IterNextIterable for PyDequeIterator {}
+    impl SelfIter for PyDequeIterator {}
     impl IterNext for PyDequeIterator {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             zelf.internal.lock().next(|deque, pos| {
@@ -696,7 +696,7 @@ mod _collections {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor))]
+    #[pyclass(with(IterNext, Iterable, Constructor))]
     impl PyReverseDequeIterator {
         #[pymethod(magic)]
         fn length_hint(&self) -> usize {
@@ -720,7 +720,7 @@ mod _collections {
         }
     }
 
-    impl IterNextIterable for PyReverseDequeIterator {}
+    impl SelfIter for PyReverseDequeIterator {}
     impl IterNext for PyReverseDequeIterator {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             zelf.internal.lock().next(|deque, pos| {
diff --git a/vm/src/stdlib/io.rs b/vm/src/stdlib/io.rs
index 3c98c146f3..2a9d224460 100644
--- a/vm/src/stdlib/io.rs
+++ b/vm/src/stdlib/io.rs
@@ -382,7 +382,7 @@ mod _io {
     #[derive(Debug, PyPayload)]
     pub struct _IOBase;
 
-    #[pyclass(with(IterNext, Destructor), flags(BASETYPE, HAS_DICT))]
+    #[pyclass(with(IterNext, Iterable, Destructor), flags(BASETYPE, HAS_DICT))]
     impl _IOBase {
         #[pymethod]
         fn seek(
diff --git a/vm/src/stdlib/itertools.rs b/vm/src/stdlib/itertools.rs
index 66b4eb50d2..cc5cd35df7 100644
--- a/vm/src/stdlib/itertools.rs
+++ b/vm/src/stdlib/itertools.rs
@@ -16,7 +16,7 @@ mod decl {
         identifier,
         protocol::{PyIter, PyIterReturn, PyNumber},
         stdlib::sys,
-        types::{Constructor, IterNext, IterNextIterable, Representable},
+        types::{Constructor, IterNext, Iterable, Representable, SelfIter},
         AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, PyWeakRef, TryFromObject,
         VirtualMachine,
     };
@@ -32,7 +32,7 @@ mod decl {
         active: PyRwLock<Option<PyIter>>,
     }
 
-    #[pyclass(with(IterNext), flags(BASETYPE, HAS_DICT))]
+    #[pyclass(with(IterNext, Iterable), flags(BASETYPE, HAS_DICT))]
     impl PyItertoolsChain {
         #[pyslot]
         fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
@@ -110,7 +110,7 @@ mod decl {
             Ok(())
         }
     }
-    impl IterNextIterable for PyItertoolsChain {}
+    impl SelfIter for PyItertoolsChain {}
     impl IterNext for PyItertoolsChain {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let Some(source) = zelf.source.read().clone() else {
@@ -189,7 +189,7 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor), flags(BASETYPE))]
+    #[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
     impl PyItertoolsCompress {
         #[pymethod(magic)]
         fn reduce(zelf: PyRef<Self>) -> (PyTypeRef, (PyIter, PyIter)) {
@@ -200,7 +200,7 @@ mod decl {
         }
     }
 
-    impl IterNextIterable for PyItertoolsCompress {}
+    impl SelfIter for PyItertoolsCompress {}
     impl IterNext for PyItertoolsCompress {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             loop {
@@ -249,7 +249,7 @@ mod decl {
                 return Err(vm.new_type_error("a number is required".to_owned()));
             }
 
-            PyItertoolsCount {
+            Self {
                 cur: PyRwLock::new(start),
                 step,
             }
@@ -258,7 +258,7 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor, Representable))]
+    #[pyclass(with(IterNext, Iterable, Constructor, Representable))]
     impl PyItertoolsCount {
         // TODO: Implement this
         // if (lz->cnt == PY_SSIZE_T_MAX)
@@ -268,7 +268,7 @@ mod decl {
             (zelf.class().to_owned(), (zelf.cur.read().clone(),))
         }
     }
-    impl IterNextIterable for PyItertoolsCount {}
+    impl SelfIter for PyItertoolsCount {}
     impl IterNext for PyItertoolsCount {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let mut cur = zelf.cur.write();
@@ -314,9 +314,9 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor), flags(BASETYPE))]
+    #[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
     impl PyItertoolsCycle {}
-    impl IterNextIterable for PyItertoolsCycle {}
+    impl SelfIter for PyItertoolsCycle {}
     impl IterNext for PyItertoolsCycle {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let item = if let PyIterReturn::Return(item) = zelf.iter.next(vm)? {
@@ -378,7 +378,7 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor, Representable), flags(BASETYPE))]
+    #[pyclass(with(IterNext, Iterable, Constructor, Representable), flags(BASETYPE))]
     impl PyItertoolsRepeat {
         #[pymethod(magic)]
         fn length_hint(&self, vm: &VirtualMachine) -> PyResult<usize> {
@@ -400,7 +400,7 @@ mod decl {
         }
     }
 
-    impl IterNextIterable for PyItertoolsRepeat {}
+    impl SelfIter for PyItertoolsRepeat {}
     impl IterNext for PyItertoolsRepeat {
         fn next(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             if let Some(ref times) = zelf.times {
@@ -456,7 +456,7 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor), flags(BASETYPE))]
+    #[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
     impl PyItertoolsStarmap {
         #[pymethod(magic)]
         fn reduce(zelf: PyRef<Self>) -> (PyTypeRef, (PyObjectRef, PyIter)) {
@@ -466,7 +466,7 @@ mod decl {
             )
         }
     }
-    impl IterNextIterable for PyItertoolsStarmap {}
+    impl SelfIter for PyItertoolsStarmap {}
     impl IterNext for PyItertoolsStarmap {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let obj = zelf.iterable.next(vm)?;
@@ -519,7 +519,7 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor), flags(BASETYPE))]
+    #[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
     impl PyItertoolsTakewhile {
         #[pymethod(magic)]
         fn reduce(zelf: PyRef<Self>) -> (PyTypeRef, (PyObjectRef, PyIter), u32) {
@@ -537,7 +537,7 @@ mod decl {
             Ok(())
         }
     }
-    impl IterNextIterable for PyItertoolsTakewhile {}
+    impl SelfIter for PyItertoolsTakewhile {}
     impl IterNext for PyItertoolsTakewhile {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             if zelf.stop_flag.load() {
@@ -600,7 +600,7 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor), flags(BASETYPE))]
+    #[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
     impl PyItertoolsDropwhile {
         #[pymethod(magic)]
         fn reduce(zelf: PyRef<Self>) -> (PyTypeRef, (PyObjectRef, PyIter), u32) {
@@ -618,7 +618,7 @@ mod decl {
             Ok(())
         }
     }
-    impl IterNextIterable for PyItertoolsDropwhile {}
+    impl SelfIter for PyItertoolsDropwhile {}
     impl IterNext for PyItertoolsDropwhile {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let predicate = &zelf.predicate;
@@ -719,7 +719,7 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor))]
+    #[pyclass(with(IterNext, Iterable, Constructor))]
     impl PyItertoolsGroupBy {
         pub(super) fn advance(
             &self,
@@ -737,7 +737,7 @@ mod decl {
             Ok(PyIterReturn::Return((new_value, new_key)))
         }
     }
-    impl IterNextIterable for PyItertoolsGroupBy {}
+    impl SelfIter for PyItertoolsGroupBy {}
     impl IterNext for PyItertoolsGroupBy {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let mut state = zelf.state.lock();
@@ -795,9 +795,9 @@ mod decl {
         groupby: PyRef<PyItertoolsGroupBy>,
     }
 
-    #[pyclass(with(IterNext))]
+    #[pyclass(with(IterNext, Iterable))]
     impl PyItertoolsGrouper {}
-    impl IterNextIterable for PyItertoolsGrouper {}
+    impl SelfIter for PyItertoolsGrouper {}
     impl IterNext for PyItertoolsGrouper {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let old_key = {
@@ -865,7 +865,7 @@ mod decl {
         )))
     }
 
-    #[pyclass(with(IterNext), flags(BASETYPE))]
+    #[pyclass(with(IterNext, Iterable), flags(BASETYPE))]
     impl PyItertoolsIslice {
         #[pyslot]
         fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
@@ -959,7 +959,7 @@ mod decl {
         }
     }
 
-    impl IterNextIterable for PyItertoolsIslice {}
+    impl SelfIter for PyItertoolsIslice {}
     impl IterNext for PyItertoolsIslice {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             while zelf.cur.load() < zelf.next.load() {
@@ -1023,7 +1023,7 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor), flags(BASETYPE))]
+    #[pyclass(with(IterNext, Iterable, Constructor), flags(BASETYPE))]
     impl PyItertoolsFilterFalse {
         #[pymethod(magic)]
         fn reduce(zelf: PyRef<Self>) -> (PyTypeRef, (PyObjectRef, PyIter)) {
@@ -1033,7 +1033,7 @@ mod decl {
             )
         }
     }
-    impl IterNextIterable for PyItertoolsFilterFalse {}
+    impl SelfIter for PyItertoolsFilterFalse {}
     impl IterNext for PyItertoolsFilterFalse {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let predicate = &zelf.predicate;
@@ -1091,10 +1091,10 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor))]
+    #[pyclass(with(IterNext, Iterable, Constructor))]
     impl PyItertoolsAccumulate {}
 
-    impl IterNextIterable for PyItertoolsAccumulate {}
+    impl SelfIter for PyItertoolsAccumulate {}
     impl IterNext for PyItertoolsAccumulate {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let iterable = &zelf.iterable;
@@ -1199,7 +1199,7 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor))]
+    #[pyclass(with(IterNext, Iterable, Constructor))]
     impl PyItertoolsTee {
         fn from_iter(iterator: PyIter, vm: &VirtualMachine) -> PyResult {
             let class = PyItertoolsTee::class(&vm.ctx);
@@ -1222,7 +1222,7 @@ mod decl {
             }
         }
     }
-    impl IterNextIterable for PyItertoolsTee {}
+    impl SelfIter for PyItertoolsTee {}
     impl IterNext for PyItertoolsTee {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let value = match zelf.tee_data.get_item(vm, zelf.index.load())? {
@@ -1277,7 +1277,7 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor))]
+    #[pyclass(with(IterNext, Iterable, Constructor))]
     impl PyItertoolsProduct {
         fn update_idxs(&self, mut idxs: PyRwLockWriteGuard<'_, Vec<usize>>) {
             if idxs.len() == 0 {
@@ -1302,7 +1302,7 @@ mod decl {
             }
         }
     }
-    impl IterNextIterable for PyItertoolsProduct {}
+    impl SelfIter for PyItertoolsProduct {}
     impl IterNext for PyItertoolsProduct {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             // stop signal
@@ -1370,7 +1370,7 @@ mod decl {
 
             let n = pool.len();
 
-            PyItertoolsCombinations {
+            Self {
                 pool,
                 indices: PyRwLock::new((0..r).collect()),
                 result: PyRwLock::new(None),
@@ -1382,7 +1382,7 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor))]
+    #[pyclass(with(IterNext, Iterable, Constructor))]
     impl PyItertoolsCombinations {
         #[pymethod(magic)]
         fn reduce(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyTupleRef {
@@ -1413,7 +1413,7 @@ mod decl {
         }
     }
 
-    impl IterNextIterable for PyItertoolsCombinations {}
+    impl SelfIter for PyItertoolsCombinations {}
     impl IterNext for PyItertoolsCombinations {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             // stop signal
@@ -1512,10 +1512,10 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor))]
+    #[pyclass(with(IterNext, Iterable, Constructor))]
     impl PyItertoolsCombinationsWithReplacement {}
 
-    impl IterNextIterable for PyItertoolsCombinationsWithReplacement {}
+    impl SelfIter for PyItertoolsCombinationsWithReplacement {}
     impl IterNext for PyItertoolsCombinationsWithReplacement {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             // stop signal
@@ -1609,7 +1609,7 @@ mod decl {
                 None => n,
             };
 
-            PyItertoolsPermutations {
+            Self {
                 pool,
                 indices: PyRwLock::new((0..n).collect()),
                 cycles: PyRwLock::new((0..r.min(n)).map(|i| n - i).collect()),
@@ -1622,9 +1622,9 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor))]
+    #[pyclass(with(IterNext, Iterable, Constructor))]
     impl PyItertoolsPermutations {}
-    impl IterNextIterable for PyItertoolsPermutations {}
+    impl SelfIter for PyItertoolsPermutations {}
     impl IterNext for PyItertoolsPermutations {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             // stop signal
@@ -1725,7 +1725,7 @@ mod decl {
         fillvalue: PyRwLock<PyObjectRef>,
     }
 
-    #[pyclass(with(IterNext, Constructor))]
+    #[pyclass(with(IterNext, Iterable, Constructor))]
     impl PyItertoolsZipLongest {
         #[pymethod(magic)]
         fn reduce(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult<PyTupleRef> {
@@ -1747,7 +1747,7 @@ mod decl {
             Ok(())
         }
     }
-    impl IterNextIterable for PyItertoolsZipLongest {}
+    impl SelfIter for PyItertoolsZipLongest {}
     impl IterNext for PyItertoolsZipLongest {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             if zelf.iterators.is_empty() {
@@ -1794,9 +1794,9 @@ mod decl {
         }
     }
 
-    #[pyclass(with(IterNext, Constructor))]
+    #[pyclass(with(IterNext, Iterable, Constructor))]
     impl PyItertoolsPairwise {}
-    impl IterNextIterable for PyItertoolsPairwise {}
+    impl SelfIter for PyItertoolsPairwise {}
     impl IterNext for PyItertoolsPairwise {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let old = match zelf.old.read().clone() {
diff --git a/vm/src/stdlib/os.rs b/vm/src/stdlib/os.rs
index e016c21a12..9854bf59ed 100644
--- a/vm/src/stdlib/os.rs
+++ b/vm/src/stdlib/os.rs
@@ -316,7 +316,7 @@ pub(super) mod _os {
         function::{ArgBytesLike, Either, FsPath, FuncArgs, OptionalArg},
         protocol::PyIterReturn,
         recursion::ReprGuard,
-        types::{IterNext, IterNextIterable, PyStructSequence, Representable},
+        types::{IterNext, Iterable, PyStructSequence, Representable, SelfIter},
         vm::VirtualMachine,
         AsObject, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject,
     };
@@ -791,7 +791,7 @@ pub(super) mod _os {
         mode: OutputMode,
     }
 
-    #[pyclass(with(IterNext))]
+    #[pyclass(with(IterNext, Iterable))]
     impl ScandirIterator {
         #[pymethod]
         fn close(&self) {
@@ -809,7 +809,7 @@ pub(super) mod _os {
             zelf.close()
         }
     }
-    impl IterNextIterable for ScandirIterator {}
+    impl SelfIter for ScandirIterator {}
     impl IterNext for ScandirIterator {
         fn next(zelf: &crate::Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             let entryref: &mut Option<fs::ReadDir> = &mut zelf.entries.write();
diff --git a/vm/src/stdlib/sys.rs b/vm/src/stdlib/sys.rs
index 780d7b4ef9..97eae88dbc 100644
--- a/vm/src/stdlib/sys.rs
+++ b/vm/src/stdlib/sys.rs
@@ -1,6 +1,6 @@
 use crate::{builtins::PyModule, convert::ToPyObject, Py, PyResult, VirtualMachine};
 
-pub(crate) use sys::{UnraisableHookArgs, MAXSIZE, MULTIARCH};
+pub(crate) use sys::{UnraisableHookArgs, __module_def, DOC, MAXSIZE, MULTIARCH};
 
 #[pymodule]
 mod sys {
diff --git a/vm/src/types/slot.rs b/vm/src/types/slot.rs
index 0b8ddb6fce..ce983e278f 100644
--- a/vm/src/types/slot.rs
+++ b/vm/src/types/slot.rs
@@ -3,7 +3,9 @@ use crate::{
     bytecode::ComparisonOperator,
     common::hash::PyHash,
     convert::{ToPyObject, ToPyResult},
-    function::{Either, FromArgs, FuncArgs, OptionalArg, PyComparisonValue, PySetterValue},
+    function::{
+        Either, FromArgs, FuncArgs, OptionalArg, PyComparisonValue, PyMethodDef, PySetterValue,
+    },
     identifier,
     protocol::{
         PyBuffer, PyIterReturn, PyMapping, PyMappingMethods, PyNumber, PyNumberMethods,
@@ -62,6 +64,8 @@ pub struct PyTypeSlots {
     pub iter: AtomicCell<Option<IterFunc>>,
     pub iternext: AtomicCell<Option<IterNextFunc>>,
 
+    pub methods: &'static [PyMethodDef],
+
     // Flags to define presence of optional/expanded features
     pub flags: PyTypeFlags,
 
@@ -119,7 +123,7 @@ bitflags! {
         const IMMUTABLETYPE = 1 << 8;
         const HEAPTYPE = 1 << 9;
         const BASETYPE = 1 << 10;
-        const METHOD_DESCR = 1 << 17;
+        const METHOD_DESCRIPTOR = 1 << 17;
         const HAS_DICT = 1 << 40;
 
         #[cfg(debug_assertions)]
@@ -305,6 +309,11 @@ fn iter_wrapper(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
     vm.call_special_method(&zelf, identifier!(vm, __iter__), ())
 }
 
+// PyObject_SelfIter in CPython
+fn self_iter(zelf: PyObjectRef, _vm: &VirtualMachine) -> PyResult {
+    Ok(zelf)
+}
+
 fn iternext_wrapper(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
     PyIterReturn::from_pyresult(
         vm.call_special_method(zelf, identifier!(vm, __next__), ()),
@@ -344,7 +353,7 @@ fn init_wrapper(obj: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResu
     Ok(())
 }
 
-fn new_wrapper(cls: PyTypeRef, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult {
+pub(crate) fn new_wrapper(cls: PyTypeRef, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult {
     let new = cls.get_attr(identifier!(vm, __new__)).unwrap();
     args.prepend_arg(cls.into());
     new.call(args, vm)
@@ -822,10 +831,15 @@ pub trait Callable: PyPayload {
     #[inline]
     #[pyslot]
     fn slot_call(zelf: &PyObject, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
-        let Some(zelf) = zelf.downcast_ref() else {
-            let err = vm.new_downcast_type_error(Self::class(&vm.ctx), zelf);
-            return Err(err);
-        };
+        let zelf = zelf.downcast_ref().ok_or_else(|| {
+            let repr = zelf.repr(vm);
+            let help = if let Ok(repr) = repr.as_ref() {
+                repr.as_str().to_owned()
+            } else {
+                zelf.class().name().to_owned()
+            };
+            vm.new_type_error(format!("unexpected payload for __call__ of {help}"))
+        })?;
         let args = args.bind(vm)?;
         Self::call(zelf, args, vm)
     }
@@ -1229,7 +1243,6 @@ pub trait AsNumber: PyPayload {
 #[pyclass]
 pub trait Iterable: PyPayload {
     #[pyslot]
-    #[pymethod(name = "__iter__")]
     fn slot_iter(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
         let zelf = zelf
             .downcast()
@@ -1237,7 +1250,14 @@ pub trait Iterable: PyPayload {
         Self::iter(zelf, vm)
     }
 
+    #[pymethod]
+    fn __iter__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
+        Self::slot_iter(zelf, vm)
+    }
+
     fn iter(zelf: PyRef<Self>, vm: &VirtualMachine) -> PyResult;
+
+    fn extend_slots(_slots: &mut PyTypeSlots) {}
 }
 
 // `Iterator` fits better, but to avoid confusion with rust std::iter::Iterator
@@ -1260,19 +1280,29 @@ pub trait IterNext: PyPayload + Iterable {
     }
 }
 
-pub trait IterNextIterable: PyPayload {}
+pub trait SelfIter: PyPayload {}
 
 impl<T> Iterable for T
 where
-    T: IterNextIterable,
+    T: SelfIter,
 {
-    #[inline]
-    fn slot_iter(zelf: PyObjectRef, _vm: &VirtualMachine) -> PyResult {
-        Ok(zelf)
+    #[cold]
+    fn slot_iter(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
+        let repr = zelf.repr(vm)?;
+        unreachable!("slot must be overriden for {}", repr.as_str());
+    }
+
+    fn __iter__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
+        self_iter(zelf, vm)
     }
 
     #[cold]
     fn iter(_zelf: PyRef<Self>, _vm: &VirtualMachine) -> PyResult {
         unreachable!("slot_iter is implemented");
     }
+
+    fn extend_slots(slots: &mut PyTypeSlots) {
+        let prev = slots.iter.swap(Some(self_iter));
+        debug_assert!(prev.is_some()); // slot_iter would be set
+    }
 }
diff --git a/vm/src/types/zoo.rs b/vm/src/types/zoo.rs
index a2d2a5c797..492b584e29 100644
--- a/vm/src/types/zoo.rs
+++ b/vm/src/types/zoo.rs
@@ -74,6 +74,7 @@ pub struct TypeZoo {
     pub zip_type: &'static Py<PyType>,
     pub function_type: &'static Py<PyType>,
     pub builtin_function_or_method_type: &'static Py<PyType>,
+    pub builtin_method_type: &'static Py<PyType>,
     pub method_descriptor_type: &'static Py<PyType>,
     pub property_type: &'static Py<PyType>,
     pub getset_type: &'static Py<PyType>,
@@ -91,6 +92,9 @@ pub struct TypeZoo {
     pub generic_alias_type: &'static Py<PyType>,
     pub union_type: &'static Py<PyType>,
     pub member_descriptor_type: &'static Py<PyType>,
+
+    // RustPython-original types
+    pub method_def: &'static Py<PyType>,
 }
 
 impl TypeZoo {
@@ -135,7 +139,8 @@ impl TypeZoo {
             async_generator_wrapped_value:
                 asyncgenerator::PyAsyncGenWrappedValue::init_builtin_type(),
             bound_method_type: function::PyBoundMethod::init_builtin_type(),
-            builtin_function_or_method_type: builtin_func::PyBuiltinFunction::init_builtin_type(),
+            builtin_function_or_method_type: builtin_func::PyNativeFunction::init_builtin_type(),
+            builtin_method_type: builtin_func::PyNativeMethod::init_builtin_type(),
             bytearray_iterator_type: bytearray::PyByteArrayIterator::init_builtin_type(),
             bytes_iterator_type: bytes::PyBytesIterator::init_builtin_type(),
             callable_iterator: iter::PyCallableIterator::init_builtin_type(),
@@ -172,12 +177,14 @@ impl TypeZoo {
             traceback_type: traceback::PyTraceback::init_builtin_type(),
             tuple_iterator_type: tuple::PyTupleIterator::init_builtin_type(),
             weakproxy_type: weakproxy::PyWeakProxy::init_builtin_type(),
-            method_descriptor_type: builtin_func::PyBuiltinMethod::init_builtin_type(),
+            method_descriptor_type: descriptor::PyMethodDescriptor::init_builtin_type(),
             none_type: singletons::PyNone::init_builtin_type(),
             not_implemented_type: singletons::PyNotImplemented::init_builtin_type(),
             generic_alias_type: genericalias::PyGenericAlias::init_builtin_type(),
             union_type: union_::PyUnion::init_builtin_type(),
             member_descriptor_type: descriptor::PyMemberDescriptor::init_builtin_type(),
+
+            method_def: crate::function::HeapMethodDef::init_builtin_type(),
         }
     }
 
diff --git a/vm/src/vm/context.rs b/vm/src/vm/context.rs
index c50f2a83e9..f9982c9e9a 100644
--- a/vm/src/vm/context.rs
+++ b/vm/src/vm/context.rs
@@ -1,11 +1,10 @@
 use crate::{
     builtins::{
-        builtin_func::{PyBuiltinFunction, PyBuiltinMethod, PyNativeFuncDef},
         bytes,
         code::{self, PyCode},
         descriptor::{
-            DescrObject, MemberGetter, MemberKind, MemberSetter, MemberSetterFunc, PyMemberDef,
-            PyMemberDescriptor,
+            MemberGetter, MemberKind, MemberSetter, MemberSetterFunc, PyDescriptorOwned,
+            PyMemberDef, PyMemberDescriptor,
         },
         getset::PyGetSet,
         object, pystr,
@@ -17,7 +16,10 @@ use crate::{
     class::{PyClassImpl, StaticType},
     common::rc::PyRc,
     exceptions,
-    function::{IntoPyGetterFunc, IntoPyNativeFunc, IntoPySetterFunc},
+    function::{
+        HeapMethodDef, IntoPyGetterFunc, IntoPyNativeFn, IntoPySetterFunc, PyMethodDef,
+        PyMethodFlags,
+    },
     intern::{InternableString, MaybeInternedString, StringPool},
     object::{Py, PyObjectPayload, PyObjectRef, PyPayload, PyRef},
     types::{PyTypeFlags, PyTypeSlots, TypeZoo},
@@ -45,7 +47,7 @@ pub struct Context {
     pub int_cache_pool: Vec<PyIntRef>,
     // there should only be exact objects of str in here, no non-str objects and no subclasses
     pub(crate) string_pool: StringPool,
-    pub(crate) slot_new_wrapper: PyRef<PyBuiltinFunction>,
+    pub(crate) slot_new_wrapper: PyMethodDef,
     pub names: ConstName,
 }
 
@@ -287,14 +289,16 @@ impl Context {
         let string_pool = StringPool::default();
         let names = unsafe { ConstName::new(&string_pool, &types.str_type.to_owned()) };
 
-        let slot_new_wrapper = create_object(
-            PyNativeFuncDef::new(PyType::__new__.into_func(), names.__new__).into_function(),
-            types.builtin_function_or_method_type,
-        );
+        let slot_new_wrapper = PyMethodDef {
+            name: names.__new__.as_str(),
+            func: PyType::__new__.into_func(),
+            flags: PyMethodFlags::METHOD,
+            doc: None,
+        };
 
         let empty_str = unsafe { string_pool.intern("", types.str_type.to_owned()) };
         let empty_bytes = create_object(PyBytes::from(Vec::new()), types.bytes_type);
-        let context = Context {
+        Context {
             true_value,
             false_value,
             none,
@@ -312,10 +316,7 @@ impl Context {
             string_pool,
             slot_new_wrapper,
             names,
-        };
-        TypeZoo::extend(&context);
-        exceptions::ExceptionZoo::extend(&context);
-        context
+        }
     }
 
     pub fn intern_str<S: InternableString>(&self, s: S) -> &'static PyStrInterned {
@@ -484,12 +485,24 @@ impl Context {
         .unwrap()
     }
 
-    #[inline]
-    pub fn make_func_def<F, FKind>(&self, name: &'static PyStrInterned, f: F) -> PyNativeFuncDef
+    pub fn new_method_def<F, FKind>(
+        &self,
+        name: &'static str,
+        f: F,
+        flags: PyMethodFlags,
+        doc: Option<&'static str>,
+    ) -> PyRef<HeapMethodDef>
     where
-        F: IntoPyNativeFunc<FKind>,
+        F: IntoPyNativeFn<FKind>,
     {
-        PyNativeFuncDef::new(f.into_func(), name)
+        let def = PyMethodDef {
+            name,
+            func: f.into_func(),
+            flags,
+            doc,
+        };
+        let payload = HeapMethodDef::new(def);
+        PyRef::new_ref(payload, self.types.method_def.to_owned(), None)
     }
 
     #[inline]
@@ -509,40 +522,14 @@ impl Context {
             doc: None,
         };
         let member_descriptor = PyMemberDescriptor {
-            common: DescrObject {
+            common: PyDescriptorOwned {
                 typ: class.to_owned(),
                 name: self.intern_str(name),
                 qualname: PyRwLock::new(None),
             },
             member: member_def,
         };
-
-        PyRef::new_ref(
-            member_descriptor,
-            self.types.member_descriptor_type.to_owned(),
-            None,
-        )
-    }
-
-    // #[deprecated]
-    pub fn new_function<F, FKind>(&self, name: &str, f: F) -> PyRef<PyBuiltinFunction>
-    where
-        F: IntoPyNativeFunc<FKind>,
-    {
-        self.make_func_def(self.intern_str(name), f)
-            .build_function(self)
-    }
-
-    pub fn new_method<F, FKind>(
-        &self,
-        name: &'static PyStrInterned,
-        class: &'static Py<PyType>,
-        f: F,
-    ) -> PyRef<PyBuiltinMethod>
-    where
-        F: IntoPyNativeFunc<FKind>,
-    {
-        PyBuiltinMethod::new_ref(name, class, f, self)
+        member_descriptor.into_ref(self)
     }
 
     pub fn new_readonly_getset<F, T>(
diff --git a/vm/src/vm/interpreter.rs b/vm/src/vm/interpreter.rs
index 1a1a6cc97c..de928636a4 100644
--- a/vm/src/vm/interpreter.rs
+++ b/vm/src/vm/interpreter.rs
@@ -46,6 +46,8 @@ impl Interpreter {
         F: FnOnce(&mut VirtualMachine),
     {
         let ctx = Context::genesis();
+        crate::types::TypeZoo::extend(ctx);
+        crate::exceptions::ExceptionZoo::extend(ctx);
         let mut vm = VirtualMachine::new(settings, ctx.clone());
         init(&mut vm);
         vm.initialize();
diff --git a/vm/src/vm/method.rs b/vm/src/vm/method.rs
index 3d09a95793..258b1e9473 100644
--- a/vm/src/vm/method.rs
+++ b/vm/src/vm/method.rs
@@ -33,7 +33,11 @@ impl PyMethod {
         let cls_attr = match interned_name.and_then(|name| cls.get_attr(name)) {
             Some(descr) => {
                 let descr_cls = descr.class();
-                let descr_get = if descr_cls.slots.flags.has_feature(PyTypeFlags::METHOD_DESCR) {
+                let descr_get = if descr_cls
+                    .slots
+                    .flags
+                    .has_feature(PyTypeFlags::METHOD_DESCRIPTOR)
+                {
                     is_method = true;
                     None
                 } else {
@@ -106,7 +110,7 @@ impl PyMethod {
             .class()
             .slots
             .flags
-            .has_feature(PyTypeFlags::METHOD_DESCR)
+            .has_feature(PyTypeFlags::METHOD_DESCRIPTOR)
         {
             Self::Function {
                 target: obj.to_owned(),
diff --git a/vm/src/vm/mod.rs b/vm/src/vm/mod.rs
index 2e546d3d6a..d993cdb938 100644
--- a/vm/src/vm/mod.rs
+++ b/vm/src/vm/mod.rs
@@ -116,17 +116,17 @@ impl VirtualMachine {
 
         // make a new module without access to the vm; doesn't
         // set __spec__, __loader__, etc. attributes
-        let new_module = || {
+        let new_module = |def| {
             PyRef::new_ref(
-                PyModule::new(),
+                PyModule::from_def(def),
                 ctx.types.module_type.to_owned(),
                 Some(ctx.new_dict()),
             )
         };
 
         // Hard-core modules:
-        let builtins = new_module();
-        let sys_module = new_module();
+        let builtins = new_module(stdlib::builtins::__module_def(&ctx));
+        let sys_module = new_module(stdlib::sys::__module_def(&ctx));
 
         let import_func = ctx.none();
         let profile_func = RefCell::new(ctx.none());
@@ -199,11 +199,17 @@ impl VirtualMachine {
         let frozen = frozen::core_frozen_inits().collect();
         PyRc::get_mut(&mut vm.state).unwrap().frozen = frozen;
 
-        vm.builtins
-            .init_module_dict(vm.ctx.intern_str("builtins"), None, &vm);
-        vm.sys_module
-            .init_module_dict(vm.ctx.intern_str("sys"), None, &vm);
-
+        vm.builtins.init_dict(
+            vm.ctx.intern_str("builtins"),
+            Some(vm.ctx.intern_str(stdlib::builtins::DOC.unwrap()).to_owned()),
+            &vm,
+        );
+        vm.sys_module.init_dict(
+            vm.ctx.intern_str("sys"),
+            Some(vm.ctx.intern_str(stdlib::sys::DOC.unwrap()).to_owned()),
+            &vm,
+        );
+        // let name = vm.sys_module.get_attr("__name__", &vm).unwrap();
         vm
     }
 
@@ -251,7 +257,7 @@ impl VirtualMachine {
         self.state_mut().settings.path_list.insert(0, "".to_owned());
 
         stdlib::builtins::init_module(self, &self.builtins);
-        stdlib::sys::init_module(self, self.sys_module.as_ref(), self.builtins.as_ref());
+        stdlib::sys::init_module(self, &self.sys_module, &self.builtins);
 
         let mut essential_init = || -> PyResult {
             #[cfg(not(target_arch = "wasm32"))]
@@ -802,15 +808,13 @@ impl VirtualMachine {
     pub fn __module_set_attr(
         &self,
         module: &Py<PyModule>,
-        attr_name: &str,
+        attr_name: &'static PyStrInterned,
         attr_value: impl Into<PyObjectRef>,
     ) -> PyResult<()> {
         let val = attr_value.into();
-        module.as_object().generic_setattr(
-            self.ctx.intern_str(attr_name),
-            PySetterValue::Assign(val),
-            self,
-        )
+        module
+            .as_object()
+            .generic_setattr(attr_name, PySetterValue::Assign(val), self)
     }
 
     pub fn insert_sys_path(&self, obj: PyObjectRef) -> PyResult<()> {
diff --git a/vm/src/vm/vm_new.rs b/vm/src/vm/vm_new.rs
index 38b37f81f2..301c488f0c 100644
--- a/vm/src/vm/vm_new.rs
+++ b/vm/src/vm/vm_new.rs
@@ -1,9 +1,12 @@
 use crate::{
     builtins::{
+        builtin_func::PyNativeFunction,
+        descriptor::PyMethodDescriptor,
         tuple::{IntoPyTuple, PyTupleRef},
         PyBaseException, PyBaseExceptionRef, PyDictRef, PyModule, PyStrRef, PyType, PyTypeRef,
     },
     convert::ToPyObject,
+    function::{IntoPyNativeFn, PyMethodFlags},
     scope::Scope,
     vm::VirtualMachine,
     AsObject, Py, PyObject, PyObjectRef, PyRef,
@@ -20,17 +23,18 @@ impl VirtualMachine {
         value.into_pytuple(self)
     }
 
-    pub fn new_module(&self, name: &str, dict: PyDictRef, doc: Option<&str>) -> PyRef<PyModule> {
+    pub fn new_module(
+        &self,
+        name: &str,
+        dict: PyDictRef,
+        doc: Option<PyStrRef>,
+    ) -> PyRef<PyModule> {
         let module = PyRef::new_ref(
             PyModule::new(),
             self.ctx.types.module_type.to_owned(),
             Some(dict),
         );
-        module.init_module_dict(
-            self.ctx.intern_str(name),
-            doc.map(|doc| self.ctx.new_str(doc)),
-            self,
-        );
+        module.init_dict(self.ctx.intern_str(name), doc, self);
         module
     }
 
@@ -38,6 +42,31 @@ impl VirtualMachine {
         Scope::with_builtins(None, self.ctx.new_dict(), self)
     }
 
+    pub fn new_function<F, FKind>(&self, name: &'static str, f: F) -> PyRef<PyNativeFunction>
+    where
+        F: IntoPyNativeFn<FKind>,
+    {
+        let def = self
+            .ctx
+            .new_method_def(name, f, PyMethodFlags::empty(), None);
+        def.build_function(self)
+    }
+
+    pub fn new_method<F, FKind>(
+        &self,
+        name: &'static str,
+        class: &'static Py<PyType>,
+        f: F,
+    ) -> PyRef<PyMethodDescriptor>
+    where
+        F: IntoPyNativeFn<FKind>,
+    {
+        let def = self
+            .ctx
+            .new_method_def(name, f, PyMethodFlags::METHOD, None);
+        def.build_method(class, self)
+    }
+
     /// Instantiate an exception with arguments.
     /// This function should only be used with builtin exception types; if a user-defined exception
     /// type is passed in, it may not be fully initialized; try using
diff --git a/wasm/lib/src/convert.rs b/wasm/lib/src/convert.rs
index f91f153f2e..ab2dc6da89 100644
--- a/wasm/lib/src/convert.rs
+++ b/wasm/lib/src/convert.rs
@@ -214,26 +214,25 @@ pub fn js_to_py(vm: &VirtualMachine, js_val: JsValue) -> PyObjectRef {
         }
     } else if js_val.is_function() {
         let func = js_sys::Function::from(js_val);
-        vm.ctx
-            .new_function(
-                String::from(func.name()).as_str(),
-                move |args: FuncArgs, vm: &VirtualMachine| -> PyResult {
-                    let this = Object::new();
-                    for (k, v) in args.kwargs {
-                        Reflect::set(&this, &k.into(), &py_to_js(vm, v))
-                            .expect("property to be settable");
-                    }
-                    let js_args = args
-                        .args
-                        .into_iter()
-                        .map(|v| py_to_js(vm, v))
-                        .collect::<Array>();
-                    func.apply(&this, &js_args)
-                        .map(|val| js_to_py(vm, val))
-                        .map_err(|err| js_err_to_py_err(vm, &err))
-                },
-            )
-            .into()
+        vm.new_function(
+            vm.ctx.intern_str(String::from(func.name())).as_str(),
+            move |args: FuncArgs, vm: &VirtualMachine| -> PyResult {
+                let this = Object::new();
+                for (k, v) in args.kwargs {
+                    Reflect::set(&this, &k.into(), &py_to_js(vm, v))
+                        .expect("property to be settable");
+                }
+                let js_args = args
+                    .args
+                    .into_iter()
+                    .map(|v| py_to_js(vm, v))
+                    .collect::<Array>();
+                func.apply(&this, &js_args)
+                    .map(|val| js_to_py(vm, val))
+                    .map_err(|err| js_err_to_py_err(vm, &err))
+            },
+        )
+        .into()
     } else if let Some(err) = js_val.dyn_ref::<js_sys::Error>() {
         js_err_to_py_err(vm, err).into()
     } else if js_val.is_undefined() {
diff --git a/wasm/lib/src/js_module.rs b/wasm/lib/src/js_module.rs
index e71130c5cc..f0c5378c35 100644
--- a/wasm/lib/src/js_module.rs
+++ b/wasm/lib/src/js_module.rs
@@ -14,7 +14,7 @@ mod _js {
         convert::{IntoObject, ToPyObject},
         function::{ArgCallable, OptionalArg, OptionalOption, PosArgs},
         protocol::PyIterReturn,
-        types::{IterNext, IterNextIterable, Representable},
+        types::{IterNext, Representable, SelfIter},
         Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine,
     };
     use std::{cell, fmt, future};
@@ -423,8 +423,8 @@ mod _js {
                     };
                     let _ = then.call(
                         (
-                            vm.ctx.new_function("resolve", resolve),
-                            vm.ctx.new_function("reject", reject),
+                            vm.new_function("resolve", resolve),
+                            vm.new_function("reject", reject),
                         ),
                         vm,
                     );
@@ -602,7 +602,7 @@ mod _js {
         }
     }
 
-    impl IterNextIterable for AwaitPromise {}
+    impl SelfIter for AwaitPromise {}
     impl IterNext for AwaitPromise {
         fn next(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyIterReturn> {
             zelf.send(None, vm)
diff --git a/wasm/lib/src/vm_class.rs b/wasm/lib/src/vm_class.rs
index 25964efc29..0c62bb32e7 100644
--- a/wasm/lib/src/vm_class.rs
+++ b/wasm/lib/src/vm_class.rs
@@ -316,7 +316,7 @@ impl WASMVirtualMachine {
                 let (key, value) = entry?;
                 let key = Object::from(key).to_string();
                 extend_module!(vm, &py_module, {
-                    &String::from(key) => convert::js_to_py(vm, value),
+                    String::from(key) => convert::js_to_py(vm, value),
                 });
             }
 
diff --git a/wasm/lib/src/wasm_builtins.rs b/wasm/lib/src/wasm_builtins.rs
index 216360c1c9..59d7880af4 100644
--- a/wasm/lib/src/wasm_builtins.rs
+++ b/wasm/lib/src/wasm_builtins.rs
@@ -29,14 +29,14 @@ pub fn make_stdout_object(
         vm.ctx.types.object_type.to_owned(),
         {}
     ));
-    let write_method = ctx.new_method(
-        ctx.intern_str("write"),
+    let write_method = vm.new_method(
+        "write",
         cls,
         move |_self: PyObjectRef, data: PyStrRef, vm: &VirtualMachine| -> PyResult<()> {
             write_f(data.as_str(), vm)
         },
     );
-    let flush_method = ctx.new_method(ctx.intern_str("flush"), cls, |_self: PyObjectRef| {});
+    let flush_method = vm.new_method("flush", cls, |_self: PyObjectRef| {});
     extend_class!(ctx, cls, {
         "write" => write_method,
         "flush" => flush_method,


Test patch:
diff --git a/extra_tests/snippets/builtin_type.py b/extra_tests/snippets/builtin_type.py
index 68ef65efd1..9f30b9b0ed 100644
--- a/extra_tests/snippets/builtin_type.py
+++ b/extra_tests/snippets/builtin_type.py
@@ -1,3 +1,4 @@
+import types
 from testutils import assert_raises
 
 
@@ -256,7 +257,7 @@ class B:
 assert object.__init_subclass__.__qualname__ == 'object.__init_subclass__'
 
 # Dynamic with `#[extend_class]`:
-assert bytearray.maketrans.__qualname__ == 'bytearray.maketrans'
+assert bytearray.maketrans.__qualname__ == 'bytearray.maketrans', bytearray.maketrans.__qualname__
 
 
 # Third-party:
@@ -560,3 +561,7 @@ def my_repr_func():
     pass
 
 assert repr(my_repr_func).startswith('<function my_repr_func at 0x')
+
+
+# https://github.com/RustPython/RustPython/issues/3100
+assert issubclass(types.BuiltinMethodType, types.BuiltinFunctionType)
diff --git a/extra_tests/snippets/syntax_class.py b/extra_tests/snippets/syntax_class.py
index e0dc60723e..28d066a9e9 100644
--- a/extra_tests/snippets/syntax_class.py
+++ b/extra_tests/snippets/syntax_class.py
@@ -269,19 +269,19 @@ class A(type):
     def __new__(mcls):
         assert type(mcls.__new__).__name__ == 'function'
         assert type(mcls.mro).__name__ == 'method_descriptor'
-        assert type(super().__new__).__name__ in ['builtin_function_or_method', 'method']
+        assert type(super().__new__).__name__ in ['builtin_function_or_method', 'builtin_method']
         assert type(super().mro).__name__ == 'method_descriptor'
 
 class B(type):
     def x(self):
-        assert type(self.__new__).__name__ in ['builtin_function_or_method', 'method']
-        assert type(self.mro).__name__ in  ['builtin_function_or_method', 'method']
-        assert type(super().__new__).__name__ in ['builtin_function_or_method', 'method']
-        assert type(super().mro).__name__ in  ['builtin_function_or_method', 'method']
-
-assert type(tuple.__new__).__name__ in ['builtin_function_or_method', 'method']
-assert type(tuple.mro).__name__ in  ['builtin_function_or_method', 'method']
-assert type(type.__new__).__name__ in ['builtin_function_or_method', 'method'], type.__new__
+        assert type(self.__new__).__name__ in ['builtin_function_or_method', 'builtin_method']
+        assert type(self.mro).__name__ in  ['builtin_function_or_method', 'builtin_method']
+        assert type(super().__new__).__name__ in ['builtin_function_or_method', 'builtin_method']
+        assert type(super().mro).__name__ in  ['builtin_function_or_method', 'builtin_method']
+
+assert type(tuple.__new__).__name__ in ['builtin_function_or_method', 'builtin_method']
+assert type(tuple.mro).__name__ in  ['builtin_function_or_method', 'builtin_method']
+assert type(type.__new__).__name__ in ['builtin_function_or_method', 'builtin_method'], type.__new__
 assert type(type.mro).__name__ == 'method_descriptor'
 
 B('x', (int,), {}).x()