diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8755fcb2bf8..fbed2d9774b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - Move Py_DecodeLocale from sysmodule to fileutils. [#1887](https://github.com/PyO3/pyo3/pull/1887)
 - Deprecate `PySys_AddWarnOption`, `PySys_AddWarnOptionUnicode` and `PySys_HasWarnOptions`. [#1887](https://github.com/PyO3/pyo3/pull/1887)
 - Remove function PyTuple_ClearFreeList from python 3.9 above. [#1887](https://github.com/PyO3/pyo3/pull/1887)
+- Deprecate `#[call]` attribute in favor of using `fn __call__`. [#1929](https://github.com/PyO3/pyo3/pull/1929)
 
 ### Fixed
 
diff --git a/benches/bench_pyclass.rs b/benches/bench_pyclass.rs
index 1928a5ce6d5..4cfe737bd42 100644
--- a/benches/bench_pyclass.rs
+++ b/benches/bench_pyclass.rs
@@ -16,8 +16,7 @@ impl MyClass {
         Self { elements }
     }
 
-    #[call]
-    fn call(&mut self, new_element: i32) -> usize {
+    fn __call__(&mut self, new_element: i32) -> usize {
         self.elements.push(new_element);
         self.elements.len()
     }
diff --git a/guide/src/class.md b/guide/src/class.md
index f3455c83f4a..7c75c41985c 100644
--- a/guide/src/class.md
+++ b/guide/src/class.md
@@ -14,7 +14,6 @@ This chapter will discuss the functionality and configuration these attributes o
   - [`#[setter]`](#object-properties-using-getter-and-setter)
   - [`#[staticmethod]`](#static-methods)
   - [`#[classmethod]`](#class-methods)
-  - [`#[call]`](#callable-objects)
   - [`#[classattr]`](#class-attributes)
   - [`#[args]`](#method-arguments)
 - [`#[pyproto]`](class/protocols.html)
@@ -46,7 +45,7 @@ Custom Python classes can then be added to a module using `add_class()`.
 # use pyo3::prelude::*;
 # #[pyclass]
 # struct MyClass {
-#    #[allow(dead_code)] 
+#    #[allow(dead_code)]
 #    num: i32,
 # }
 #[pymodule]
@@ -613,74 +612,6 @@ impl MyClass {
 }
 ```
 
-## Callable objects
-
-To specify a custom `__call__` method for a custom class, the method needs to be annotated with
-the `#[call]` attribute. Arguments of the method are specified as for instance methods.
-
-The following pyclass is a basic decorator - its constructor takes a Python object
-as argument and calls that object when called.
-
-```rust
-# use pyo3::prelude::*;
-# use pyo3::types::{PyDict, PyTuple};
-#
-#[pyclass(name = "counter")]
-struct PyCounter {
-    count: u64,
-    wraps: Py<PyAny>,
-}
-
-#[pymethods]
-impl PyCounter {
-    #[new]
-    fn __new__(wraps: Py<PyAny>) -> Self {
-        PyCounter { count: 0, wraps }
-    }
-
-    #[call]
-    #[args(args = "*", kwargs = "**")]
-    fn __call__(
-        &mut self,
-        py: Python,
-        args: &PyTuple,
-        kwargs: Option<&PyDict>,
-    ) -> PyResult<Py<PyAny>> {
-        self.count += 1;
-        let name = self.wraps.getattr(py, "__name__").unwrap();
-
-        println!("{} has been called {} time(s).", name, self.count);
-        self.wraps.call(py, args, kwargs)
-    }
-}
-```
-
-Python code:
-
-```python
-@counter
-def say_hello():
-    print("hello")
-
-say_hello()
-say_hello()
-say_hello()
-say_hello()
-```
-
-Output:
-
-```text
-say_hello has been called 1 time(s).
-hello
-say_hello has been called 2 time(s).
-hello
-say_hello has been called 3 time(s).
-hello
-say_hello has been called 4 time(s).
-hello
-```
-
 ## Method arguments
 
 By default, PyO3 uses function signatures to determine which arguments are required. Then it scans
@@ -848,11 +779,6 @@ impl pyo3::class::impl_::PyClassImpl for MyClass {
         let collector = PyClassImplCollector::<Self>::new();
         collector.free_impl()
     }
-    fn get_call() -> Option<pyo3::ffi::PyCFunctionWithKeywords> {
-        use pyo3::class::impl_::*;
-        let collector = PyClassImplCollector::<Self>::new();
-        collector.call_impl()
-    }
     fn for_each_proto_slot(visitor: &mut dyn FnMut(&[pyo3::ffi::PyType_Slot])) {
         // Implementation which uses dtolnay specialization to load all slots.
         use pyo3::class::impl_::*;
diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md
index 5ace236c864..5217ba2df90 100644
--- a/guide/src/class/protocols.md
+++ b/guide/src/class/protocols.md
@@ -1,4 +1,4 @@
-## Class customizations
+# Class customizations
 
 PyO3 uses the `#[pyproto]` attribute in combination with special traits to implement certain protocol (aka `__dunder__`) methods of Python classes. The special traits are listed in this chapter of the guide. See also the [documentation for the `pyo3::class` module]({{#PYO3_DOCS_URL}}/pyo3/class/index.html).
 
@@ -10,11 +10,11 @@ All `#[pyproto]` methods can return `T` instead of `PyResult<T>` if the method i
 
 There are many "dunder" methods which are not included in any of PyO3's protocol traits, such as `__dir__`. These methods can be implemented in `#[pymethods]` as already covered in the previous section.
 
-### Basic object customization
+## Basic object customization
 
 The [`PyObjectProtocol`] trait provides several basic customizations.
 
-#### Attribute access
+### Attribute access
 
 To customize object attribute access, define the following methods:
 
@@ -24,14 +24,14 @@ To customize object attribute access, define the following methods:
 
 Each method corresponds to Python's `self.attr`, `self.attr = value` and `del self.attr` code.
 
-#### String Conversions
+### String Conversions
 
   * `fn __repr__(&self) -> PyResult<impl ToPyObject<ObjectType=PyString>>`
   * `fn __str__(&self) -> PyResult<impl ToPyObject<ObjectType=PyString>>`
 
     Possible return types for `__str__` and `__repr__` are `PyResult<String>` or `PyResult<PyString>`.
 
-#### Comparison operators
+### Comparison operators
 
   * `fn __richcmp__(&self, other: impl FromPyObject, op: CompareOp) -> PyResult<impl ToPyObject>`
 
@@ -46,13 +46,78 @@ Each method corresponds to Python's `self.attr`, `self.attr = value` and `del se
     Objects that compare equal must have the same hash value.
     The return type must be `PyResult<T>` where `T` is one of Rust's primitive integer types.
 
-#### Other methods
+### Other methods
 
   * `fn __bool__(&self) -> PyResult<bool>`
 
     Determines the "truthyness" of the object.
 
-### Emulating numeric types
+## Callable objects
+
+Custom classes can be callable if they have a `#[pymethod]` named `__call__`.
+
+The following pyclass is a basic decorator - its constructor takes a Python object
+as argument and calls that object when called.
+
+```rust
+# use pyo3::prelude::*;
+# use pyo3::types::{PyDict, PyTuple};
+#
+#[pyclass(name = "counter")]
+struct PyCounter {
+    count: u64,
+    wraps: Py<PyAny>,
+}
+
+#[pymethods]
+impl PyCounter {
+    #[new]
+    fn __new__(wraps: Py<PyAny>) -> Self {
+        PyCounter { count: 0, wraps }
+    }
+
+    fn __call__(
+        &mut self,
+        py: Python,
+        args: &PyTuple,
+        kwargs: Option<&PyDict>,
+    ) -> PyResult<Py<PyAny>> {
+        self.count += 1;
+        let name = self.wraps.getattr(py, "__name__").unwrap();
+
+        println!("{} has been called {} time(s).", name, self.count);
+        self.wraps.call(py, args, kwargs)
+    }
+}
+```
+
+Python code:
+
+```python
+@counter
+def say_hello():
+    print("hello")
+
+say_hello()
+say_hello()
+say_hello()
+say_hello()
+```
+
+Output:
+
+```text
+say_hello has been called 1 time(s).
+hello
+say_hello has been called 2 time(s).
+hello
+say_hello has been called 3 time(s).
+hello
+say_hello has been called 4 time(s).
+hello
+```
+
+## Emulating numeric types
 
 The [`PyNumberProtocol`] trait can be implemented to emulate [numeric types](https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types).
 
@@ -132,7 +197,7 @@ Other:
 
   * `fn __index__(&'p self) -> PyResult<impl ToPyObject>`
 
-### Emulating sequential containers (such as lists or tuples)
+## Emulating sequential containers (such as lists or tuples)
 
 The [`PySequenceProtocol`] trait can be implemented to emulate
 [sequential container types](https://docs.python.org/3/reference/datamodel.html#emulating-container-types).
@@ -195,7 +260,7 @@ where _N_ is the length of the sequence.
     Used by the `*=` operator, after trying the numeric in place multiplication via
     the `PyNumberProtocol` trait method.
 
-### Emulating mapping containers (such as dictionaries)
+## Emulating mapping containers (such as dictionaries)
 
 The [`PyMappingProtocol`] trait allows to emulate
 [mapping container types](https://docs.python.org/3/reference/datamodel.html#emulating-container-types).
@@ -228,7 +293,7 @@ For a mapping, the keys may be Python objects of arbitrary type.
     The same exceptions should be raised for improper key values as
     for the `__getitem__()` method.
 
-### Garbage Collector Integration
+## Garbage Collector Integration
 
 If your type owns references to other Python objects, you will need to
 integrate with Python's garbage collector so that the GC is aware of
@@ -280,7 +345,7 @@ at compile time:
 struct GCTracked {} // Fails because it does not implement PyGCProtocol
 ```
 
-### Iterator Types
+## Iterator Types
 
 Iterators can be defined using the
 [`PyIterProtocol`]({{#PYO3_DOCS_URL}}/pyo3/class/iter/trait.PyIterProtocol.html) trait.
@@ -365,7 +430,7 @@ impl PyIterProtocol for Container {
 For more details on Python's iteration protocols, check out [the "Iterator Types" section of the library
 documentation](https://docs.python.org/3/library/stdtypes.html#iterator-types).
 
-#### Returning a value from iteration
+### Returning a value from iteration
 
 This guide has so far shown how to use `Option<T>` to implement yielding values during iteration.
 In Python a generator can also return a value. To express this in Rust, PyO3 provides the
diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs
index 0c4e28b9b5f..b178d4e8989 100644
--- a/pyo3-macros-backend/src/deprecations.rs
+++ b/pyo3-macros-backend/src/deprecations.rs
@@ -6,6 +6,7 @@ pub enum Deprecation {
     PyfnNameArgument,
     PyModuleNameArgument,
     TextSignatureAttribute,
+    CallAttribute,
 }
 
 impl Deprecation {
@@ -15,6 +16,7 @@ impl Deprecation {
             Deprecation::PyfnNameArgument => "PYFN_NAME_ARGUMENT",
             Deprecation::PyModuleNameArgument => "PYMODULE_NAME_ARGUMENT",
             Deprecation::TextSignatureAttribute => "TEXT_SIGNATURE_ATTRIBUTE",
+            Deprecation::CallAttribute => "CALL_ATTRIBUTE",
         };
         syn::Ident::new(string, span)
     }
diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs
index 6bfd0692fd3..bf9ec3ad353 100644
--- a/pyo3-macros-backend/src/method.rs
+++ b/pyo3-macros-backend/src/method.rs
@@ -1,6 +1,7 @@
 // Copyright (c) 2017-present PyO3 Project and Contributors
 
 use crate::attributes::TextSignatureAttribute;
+use crate::deprecations::Deprecation;
 use crate::params::{accept_args_kwargs, impl_arg_params};
 use crate::pyfunction::PyFunctionOptions;
 use crate::pyfunction::{PyFunctionArgPyO3Attributes, PyFunctionSignature};
@@ -65,8 +66,6 @@ impl<'a> FnArg<'a> {
 pub enum MethodTypeAttribute {
     /// #[new]
     New,
-    /// #[call]
-    Call,
     /// #[classmethod]
     ClassMethod,
     /// #[classattr]
@@ -84,7 +83,6 @@ pub enum FnType {
     Getter(SelfType),
     Setter(SelfType),
     Fn(SelfType),
-    FnCall(SelfType),
     FnNew,
     FnClass,
     FnStatic,
@@ -99,11 +97,10 @@ impl FnType {
         error_mode: ExtractErrorMode,
     ) -> TokenStream {
         match self {
-            FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) | FnType::FnCall(st) => st
-                .receiver(
-                    cls.expect("no class given for Fn with a \"self\" receiver"),
-                    error_mode,
-                ),
+            FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) => st.receiver(
+                cls.expect("no class given for Fn with a \"self\" receiver"),
+                error_mode,
+            ),
             FnType::FnNew | FnType::FnStatic | FnType::ClassAttribute => {
                 quote!()
             }
@@ -260,31 +257,22 @@ impl<'a> FnSpec<'a> {
         meth_attrs: &mut Vec<syn::Attribute>,
         options: PyFunctionOptions,
     ) -> Result<FnSpec<'a>> {
+        let PyFunctionOptions {
+            text_signature,
+            name,
+            mut deprecations,
+            ..
+        } = options;
+
         let MethodAttributes {
             ty: fn_type_attr,
             args: fn_attrs,
             mut python_name,
-        } = parse_method_attributes(meth_attrs, options.name.map(|name| name.0))?;
-
-        match fn_type_attr {
-            Some(MethodTypeAttribute::New) => {
-                if let Some(name) = &python_name {
-                    bail_spanned!(name.span() => "`name` not allowed with `#[new]`");
-                }
-                python_name = Some(syn::Ident::new("__new__", Span::call_site()))
-            }
-            Some(MethodTypeAttribute::Call) => {
-                if let Some(name) = &python_name {
-                    bail_spanned!(name.span() => "`name` not allowed with `#[call]`");
-                }
-                python_name = Some(syn::Ident::new("__call__", Span::call_site()))
-            }
-            _ => {}
-        }
+        } = parse_method_attributes(meth_attrs, name.map(|name| name.0), &mut deprecations)?;
 
         let (fn_type, skip_first_arg, fixed_convention) =
             Self::parse_fn_type(sig, fn_type_attr, &mut python_name)?;
-        Self::ensure_text_signature_on_valid_method(&fn_type, options.text_signature.as_ref())?;
+        Self::ensure_text_signature_on_valid_method(&fn_type, text_signature.as_ref())?;
 
         let name = &sig.ident;
         let ty = get_return_info(&sig.output);
@@ -292,10 +280,7 @@ impl<'a> FnSpec<'a> {
 
         let doc = utils::get_doc(
             meth_attrs,
-            options
-                .text_signature
-                .as_ref()
-                .map(|attr| (&python_name, attr)),
+            text_signature.as_ref().map(|attr| (&python_name, attr)),
         );
 
         let arguments: Vec<_> = if skip_first_arg {
@@ -323,7 +308,7 @@ impl<'a> FnSpec<'a> {
             args: arguments,
             output: ty,
             doc,
-            deprecations: options.deprecations,
+            deprecations,
         })
     }
 
@@ -342,10 +327,7 @@ impl<'a> FnSpec<'a> {
                     "text_signature not allowed on __new__; if you want to add a signature on \
                      __new__, put it on the struct definition instead"
                 ),
-                FnType::FnCall(_)
-                | FnType::Getter(_)
-                | FnType::Setter(_)
-                | FnType::ClassAttribute => bail_spanned!(
+                FnType::Getter(_) | FnType::Setter(_) | FnType::ClassAttribute => bail_spanned!(
                     text_signature.kw.span() => "text_signature not allowed with this method type"
                 ),
                 _ => {}
@@ -389,14 +371,13 @@ impl<'a> FnSpec<'a> {
                 (FnType::ClassAttribute, false, None)
             }
             Some(MethodTypeAttribute::New) => {
+                if let Some(name) = &python_name {
+                    bail_spanned!(name.span() => "`name` not allowed with `#[new]`");
+                }
+                *python_name = Some(syn::Ident::new("__new__", Span::call_site()));
                 (FnType::FnNew, false, Some(CallingConvention::TpNew))
             }
             Some(MethodTypeAttribute::ClassMethod) => (FnType::FnClass, true, None),
-            Some(MethodTypeAttribute::Call) => (
-                FnType::FnCall(parse_receiver("expected receiver for #[call]")?),
-                true,
-                Some(CallingConvention::Varargs),
-            ),
             Some(MethodTypeAttribute::Getter) => {
                 // Strip off "get_" prefix if needed
                 if python_name.is_none() {
@@ -622,6 +603,7 @@ struct MethodAttributes {
 fn parse_method_attributes(
     attrs: &mut Vec<syn::Attribute>,
     mut python_name: Option<syn::Ident>,
+    deprecations: &mut Deprecations,
 ) -> Result<MethodAttributes> {
     let mut new_attrs = Vec::new();
     let mut args = Vec::new();
@@ -644,7 +626,12 @@ fn parse_method_attributes(
                 } else if name.is_ident("init") || name.is_ident("__init__") {
                     bail_spanned!(name.span() => "#[init] is disabled since PyO3 0.9.0");
                 } else if name.is_ident("call") || name.is_ident("__call__") {
-                    set_ty!(MethodTypeAttribute::Call, name);
+                    deprecations.push(Deprecation::CallAttribute, name.span());
+                    ensure_spanned!(
+                        python_name.is_none(),
+                        python_name.span() => "`name` may not be used with `#[call]`"
+                    );
+                    python_name = Some(syn::Ident::new("__call__", Span::call_site()));
                 } else if name.is_ident("classmethod") {
                     set_ty!(MethodTypeAttribute::ClassMethod, name);
                 } else if name.is_ident("staticmethod") {
@@ -674,7 +661,11 @@ fn parse_method_attributes(
                 } else if path.is_ident("init") {
                     bail_spanned!(path.span() => "#[init] is disabled since PyO3 0.9.0");
                 } else if path.is_ident("call") {
-                    set_ty!(MethodTypeAttribute::Call, path);
+                    ensure_spanned!(
+                        python_name.is_none(),
+                        python_name.span() => "`name` may not be used with `#[call]`"
+                    );
+                    python_name = Some(syn::Ident::new("__call__", Span::call_site()));
                 } else if path.is_ident("setter") || path.is_ident("getter") {
                     if let syn::AttrStyle::Inner(_) = attr.style {
                         bail_spanned!(
diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs
index c68a67c4e23..a75b2584c9d 100644
--- a/pyo3-macros-backend/src/pyclass.rs
+++ b/pyo3-macros-backend/src/pyclass.rs
@@ -346,14 +346,21 @@ fn impl_methods_inventory(cls: &syn::Ident) -> TokenStream {
         #[doc(hidden)]
         pub struct #inventory_cls {
             methods: ::std::vec::Vec<::pyo3::class::PyMethodDefType>,
+            slots: ::std::vec::Vec<::pyo3::ffi::PyType_Slot>,
         }
         impl ::pyo3::class::impl_::PyMethodsInventory for #inventory_cls {
-            fn new(methods: ::std::vec::Vec<::pyo3::class::PyMethodDefType>) -> Self {
-                Self { methods }
+            fn new(
+                methods: ::std::vec::Vec<::pyo3::class::PyMethodDefType>,
+                slots: ::std::vec::Vec<::pyo3::ffi::PyType_Slot>,
+            ) -> Self {
+                Self { methods, slots }
             }
-            fn get(&'static self) -> &'static [::pyo3::class::PyMethodDefType] {
+            fn methods(&'static self) -> &'static [::pyo3::class::PyMethodDefType] {
                 &self.methods
             }
+            fn slots(&'static self) -> &'static [::pyo3::ffi::PyType_Slot] {
+                &self.slots
+            }
         }
 
         impl ::pyo3::class::impl_::HasMethodsInventory for #cls {
@@ -450,15 +457,12 @@ fn impl_class(
     };
 
     let (impl_inventory, for_each_py_method) = match methods_type {
-        PyClassMethodsType::Specialization => (
-            ::std::option::Option::None,
-            quote! { visitor(collector.py_methods()); },
-        ),
+        PyClassMethodsType::Specialization => (None, quote! { visitor(collector.py_methods()); }),
         PyClassMethodsType::Inventory => (
             Some(impl_methods_inventory(cls)),
             quote! {
                 for inventory in ::pyo3::inventory::iter::<<Self as ::pyo3::class::impl_::HasMethodsInventory>::Methods>() {
-                    visitor(::pyo3::class::impl_::PyMethodsInventory::get(inventory));
+                    visitor(::pyo3::class::impl_::PyMethodsInventory::methods(inventory));
                 }
             },
         ),
@@ -466,9 +470,15 @@ fn impl_class(
 
     let methods_protos = match methods_type {
         PyClassMethodsType::Specialization => {
-            Some(quote! { visitor(collector.methods_protocol_slots()); })
+            quote! { visitor(collector.methods_protocol_slots()); }
+        }
+        PyClassMethodsType::Inventory => {
+            quote! {
+                for inventory in ::pyo3::inventory::iter::<<Self as ::pyo3::class::impl_::HasMethodsInventory>::Methods>() {
+                    visitor(::pyo3::class::impl_::PyMethodsInventory::slots(inventory));
+                }
+            }
         }
-        PyClassMethodsType::Inventory => None,
     };
 
     let base = &attr.base;
@@ -579,11 +589,6 @@ fn impl_class(
                 let collector = PyClassImplCollector::<Self>::new();
                 collector.free_impl()
             }
-            fn get_call() -> ::std::option::Option<::pyo3::ffi::PyCFunctionWithKeywords> {
-                use ::pyo3::class::impl_::*;
-                let collector = PyClassImplCollector::<Self>::new();
-                collector.call_impl()
-            }
 
             fn for_each_proto_slot(visitor: &mut dyn ::std::ops::FnMut(&[::pyo3::ffi::PyType_Slot])) {
                 // Implementation which uses dtolnay specialization to load all slots.
diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs
index 62477289c00..885c2540dfd 100644
--- a/pyo3-macros-backend/src/pyimpl.rs
+++ b/pyo3-macros-backend/src/pyimpl.rs
@@ -85,32 +85,29 @@ pub fn impl_methods(
         }
     }
 
-    let methods_registration = match methods_type {
-        PyClassMethodsType::Specialization => impl_py_methods(ty, methods),
-        PyClassMethodsType::Inventory => submit_methods_inventory(ty, methods),
-    };
+    add_shared_proto_slots(ty, &mut proto_impls, implemented_proto_fragments);
 
-    let protos_registration = match methods_type {
+    Ok(match methods_type {
         PyClassMethodsType::Specialization => {
-            Some(impl_protos(ty, proto_impls, implemented_proto_fragments))
+            let methods_registration = impl_py_methods(ty, methods);
+            let protos_registration = impl_protos(ty, proto_impls);
+
+            quote! {
+                #(#trait_impls)*
+
+                #protos_registration
+
+                #methods_registration
+            }
         }
         PyClassMethodsType::Inventory => {
-            if proto_impls.is_empty() {
-                None
-            } else {
-                panic!(
-                    "cannot implement protos in #[pymethods] using `multiple-pymethods` feature"
-                );
+            let inventory = submit_methods_inventory(ty, methods, proto_impls);
+            quote! {
+                #(#trait_impls)*
+
+                #inventory
             }
         }
-    };
-
-    Ok(quote! {
-        #(#trait_impls)*
-
-        #protos_registration
-
-        #methods_registration
     })
 }
 
@@ -147,11 +144,11 @@ fn impl_py_methods(ty: &syn::Type, methods: Vec<TokenStream>) -> TokenStream {
     }
 }
 
-fn impl_protos(
+fn add_shared_proto_slots(
     ty: &syn::Type,
-    mut proto_impls: Vec<TokenStream>,
+    proto_impls: &mut Vec<TokenStream>,
     mut implemented_proto_fragments: HashSet<String>,
-) -> TokenStream {
+) {
     macro_rules! try_add_shared_slot {
         ($first:literal, $second:literal, $slot:ident) => {{
             let first_implemented = implemented_proto_fragments.remove($first);
@@ -184,6 +181,10 @@ fn impl_protos(
     );
     try_add_shared_slot!("__pow__", "__rpow__", generate_pyclass_pow_slot);
 
+    assert!(implemented_proto_fragments.is_empty());
+}
+
+fn impl_protos(ty: &syn::Type, proto_impls: Vec<TokenStream>) -> TokenStream {
     quote! {
         impl ::pyo3::class::impl_::PyMethodsProtocolSlots<#ty>
             for ::pyo3::class::impl_::PyClassImplCollector<#ty>
@@ -195,16 +196,16 @@ fn impl_protos(
     }
 }
 
-fn submit_methods_inventory(ty: &syn::Type, methods: Vec<TokenStream>) -> TokenStream {
-    if methods.is_empty() {
-        return TokenStream::default();
-    }
-
+fn submit_methods_inventory(
+    ty: &syn::Type,
+    methods: Vec<TokenStream>,
+    proto_impls: Vec<TokenStream>,
+) -> TokenStream {
     quote! {
         ::pyo3::inventory::submit! {
             #![crate = ::pyo3] {
                 type Inventory = <#ty as ::pyo3::class::impl_::HasMethodsInventory>::Methods;
-                <Inventory as ::pyo3::class::impl_::PyMethodsInventory>::new(::std::vec![#(#methods),*])
+                <Inventory as ::pyo3::class::impl_::PyMethodsInventory>::new(::std::vec![#(#methods),*], ::std::vec![#(#proto_impls),*])
             }
         }
     }
diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs
index c8c451f0e8e..dfcac58761c 100644
--- a/pyo3-macros-backend/src/pymethod.rs
+++ b/pyo3-macros-backend/src/pymethod.rs
@@ -3,7 +3,7 @@
 use std::borrow::Cow;
 
 use crate::attributes::NameAttribute;
-use crate::method::ExtractErrorMode;
+use crate::method::{CallingConvention, ExtractErrorMode};
 use crate::utils::{ensure_not_async_fn, unwrap_ty_group, PythonDoc};
 use crate::{deprecations::Deprecations, utils};
 use crate::{
@@ -38,6 +38,8 @@ pub fn gen_py_method(
     if let Some(slot_def) = pyproto(&method_name) {
         let slot = slot_def.generate_type_slot(cls, &spec)?;
         return Ok(GeneratedPyMethod::Proto(slot));
+    } else if method_name == "__call__" {
+        return Ok(GeneratedPyMethod::Proto(impl_call_slot(cls, spec)?));
     }
 
     if let Some(slot_fragment_def) = pyproto_fragment(&method_name) {
@@ -60,7 +62,6 @@ pub fn gen_py_method(
         )?),
         // special prototypes
         FnType::FnNew => GeneratedPyMethod::TraitImpl(impl_py_method_def_new(cls, &spec)?),
-        FnType::FnCall(_) => GeneratedPyMethod::TraitImpl(impl_py_method_def_call(cls, &spec)?),
         FnType::ClassAttribute => GeneratedPyMethod::Method(impl_py_class_attribute(cls, &spec)),
         FnType::Getter(self_type) => GeneratedPyMethod::Method(impl_py_getter_def(
             cls,
@@ -136,19 +137,20 @@ fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec) -> Result<TokenStream>
     })
 }
 
-fn impl_py_method_def_call(cls: &syn::Type, spec: &FnSpec) -> Result<TokenStream> {
+fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec) -> Result<TokenStream> {
+    // HACK: __call__ proto slot must always use varargs calling convention, so change the spec.
+    // Probably indicates there's a refactoring opportunity somewhere.
+    spec.convention = CallingConvention::Varargs;
+
     let wrapper_ident = syn::Ident::new("__wrap", Span::call_site());
     let wrapper = spec.get_wrapper_function(&wrapper_ident, Some(cls))?;
-    Ok(quote! {
-        impl ::pyo3::class::impl_::PyClassCallImpl<#cls> for ::pyo3::class::impl_::PyClassImplCollector<#cls> {
-            fn call_impl(self) -> ::std::option::Option<::pyo3::ffi::PyCFunctionWithKeywords> {
-                ::std::option::Option::Some({
-                    #wrapper
-                    #wrapper_ident
-                })
-            }
+    Ok(quote! {{
+        #wrapper
+        ::pyo3::ffi::PyType_Slot {
+            slot: ::pyo3::ffi::Py_tp_call,
+            pfunc: __wrap as ::pyo3::ffi::ternaryfunc as _
         }
-    })
+    }})
 }
 
 fn impl_py_class_attribute(cls: &syn::Type, spec: &FnSpec) -> TokenStream {
diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs
index bd72cd3c9e2..d91553995cc 100644
--- a/pyo3-macros/src/lib.rs
+++ b/pyo3-macros/src/lib.rs
@@ -133,7 +133,6 @@ pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream {
 /// | [`#[getter]`][5] and [`#[setter]`][5] | These define getters and setters, similar to Python's `@property` decorator. This is useful for getters/setters that require computation or side effects; if that is not the case consider using [`#[pyo3(get, set)]`][11] on the struct's field(s).|
 /// | [`#[staticmethod]`][6]| Defines the method as a staticmethod, like Python's `@staticmethod` decorator.|
 /// | [`#[classmethod]`][7]  | Defines the method as a classmethod, like Python's `@classmethod` decorator.|
-/// | [`#[call]`][8]  | Allows Python code to call a class instance as a function, like Python's `__call__` method. |
 /// | [`#[classattr]`][9]  | Defines a class variable. |
 /// | [`#[args]`][10]  | Define a method's default arguments and allows the function to receive `*args` and `**kwargs`.  |
 ///
diff --git a/src/class/impl_.rs b/src/class/impl_.rs
index 125e24a3572..e9fb6492034 100644
--- a/src/class/impl_.rs
+++ b/src/class/impl_.rs
@@ -71,9 +71,6 @@ pub trait PyClassImpl: Sized {
     fn get_new() -> Option<ffi::newfunc> {
         None
     }
-    fn get_call() -> Option<ffi::PyCFunctionWithKeywords> {
-        None
-    }
     fn get_alloc() -> Option<ffi::allocfunc> {
         None
     }
@@ -98,16 +95,6 @@ impl<T> PyClassNewImpl<T> for &'_ PyClassImplCollector<T> {
     }
 }
 
-pub trait PyClassCallImpl<T> {
-    fn call_impl(self) -> Option<ffi::PyCFunctionWithKeywords>;
-}
-
-impl<T> PyClassCallImpl<T> for &'_ PyClassImplCollector<T> {
-    fn call_impl(self) -> Option<ffi::PyCFunctionWithKeywords> {
-        None
-    }
-}
-
 macro_rules! slot_fragment_trait {
     ($trait_name:ident, $($default_method:tt)*) => {
         #[allow(non_camel_case_types)]
@@ -625,10 +612,13 @@ macro_rules! methods_trait {
 #[cfg(all(feature = "macros", feature = "multiple-pymethods"))]
 pub trait PyMethodsInventory: inventory::Collect {
     /// Create a new instance
-    fn new(methods: Vec<PyMethodDefType>) -> Self;
+    fn new(methods: Vec<PyMethodDefType>, slots: Vec<ffi::PyType_Slot>) -> Self;
 
     /// Returns the methods for a single `#[pymethods] impl` block
-    fn get(&'static self) -> &'static [PyMethodDefType];
+    fn methods(&'static self) -> &'static [PyMethodDefType];
+
+    /// Returns the slots for a single `#[pymethods] impl` block
+    fn slots(&'static self) -> &'static [ffi::PyType_Slot];
 }
 
 /// Implemented for `#[pyclass]` in our proc macro code.
@@ -676,6 +666,7 @@ slots_trait!(PyAsyncProtocolSlots, async_protocol_slots);
 slots_trait!(PySequenceProtocolSlots, sequence_protocol_slots);
 slots_trait!(PyBufferProtocolSlots, buffer_protocol_slots);
 
+// Protocol slots from #[pymethods] if not using inventory.
 #[cfg(not(feature = "multiple-pymethods"))]
 slots_trait!(PyMethodsProtocolSlots, methods_protocol_slots);
 
diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs
index 585b59dc88e..1fbabf9eefd 100644
--- a/src/impl_/deprecations.rs
+++ b/src/impl_/deprecations.rs
@@ -23,3 +23,6 @@ pub const PYMODULE_NAME_ARGUMENT: () = ();
     note = "use `#[pyo3(text_signature = \"...\")]` instead of `#[text_signature = \"...\"]`"
 )]
 pub const TEXT_SIGNATURE_ATTRIBUTE: () = ();
+
+#[deprecated(since = "0.15.0", note = "use `fn __call__` instead of `#[call]`")]
+pub const CALL_ATTRIBUTE: () = ();
diff --git a/src/pyclass.rs b/src/pyclass.rs
index 47c9d6f037e..61412a6197f 100644
--- a/src/pyclass.rs
+++ b/src/pyclass.rs
@@ -83,10 +83,6 @@ where
         slots.push(ffi::Py_tp_free, free as _);
     }
 
-    if let Some(call_meth) = T::get_call() {
-        slots.push(ffi::Py_tp_call, call_meth as _);
-    }
-
     if cfg!(Py_3_9) {
         let members = py_class_members::<T>();
         if !members.is_empty() {
diff --git a/tests/hygiene/pymethods.rs b/tests/hygiene/pymethods.rs
index fc2e3547262..37a916f9c6b 100644
--- a/tests/hygiene/pymethods.rs
+++ b/tests/hygiene/pymethods.rs
@@ -363,7 +363,6 @@ impl Dummy {
     fn staticmethod() {}
     #[classmethod]
     fn clsmethod(_: &::pyo3::types::PyType) {}
-    #[call]
     #[args(args = "*", kwds = "**")]
     fn __call__(
         &self,
diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs
index 64649e08229..1faf8492950 100644
--- a/tests/test_arithmetics.rs
+++ b/tests/test_arithmetics.rs
@@ -1,5 +1,3 @@
-#![cfg(not(feature = "multiple-pymethods"))]
-
 use pyo3::class::basic::CompareOp;
 use pyo3::prelude::*;
 use pyo3::py_run;
diff --git a/tests/test_hygiene.rs b/tests/test_hygiene.rs
index b1d405b4b3a..47ffa6212eb 100644
--- a/tests/test_hygiene.rs
+++ b/tests/test_hygiene.rs
@@ -2,8 +2,6 @@ mod hygiene {
     mod misc;
     mod pyclass;
     mod pyfunction;
-    // cannot implement protos in #[pymethods] using `multiple-pymethods` feature
-    #[cfg(not(feature = "multiple-pymethods"))]
     mod pymethods;
     mod pymodule;
     mod pyproto;
diff --git a/tests/test_methods.rs b/tests/test_methods.rs
index 40afb4f3b53..1032acf9a0d 100644
--- a/tests/test_methods.rs
+++ b/tests/test_methods.rs
@@ -868,8 +868,7 @@ impl r#RawIdents {
         self.r#subsubtype = r#subsubtype;
     }
 
-    #[call]
-    pub fn r#call(&mut self, r#type: PyObject) {
+    pub fn r#__call__(&mut self, r#type: PyObject) {
         self.r#type = r#type;
     }
 
diff --git a/tests/test_multiple_pymethods.rs b/tests/test_multiple_pymethods.rs
index ab249ea9eba..a793eec5e32 100644
--- a/tests/test_multiple_pymethods.rs
+++ b/tests/test_multiple_pymethods.rs
@@ -20,8 +20,7 @@ impl PyClassWithMultiplePyMethods {
 
 #[pymethods]
 impl PyClassWithMultiplePyMethods {
-    #[call]
-    fn call(&self) -> &'static str {
+    fn __call__(&self) -> &'static str {
         "call"
     }
 }
diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs
index 1cfafefdedb..f60154fe974 100644
--- a/tests/test_proto_methods.rs
+++ b/tests/test_proto_methods.rs
@@ -1,5 +1,3 @@
-#![cfg(not(feature = "multiple-pymethods"))]
-
 use pyo3::exceptions::PyValueError;
 use pyo3::types::{PySlice, PyType};
 use pyo3::{exceptions::PyAttributeError, prelude::*};
@@ -229,32 +227,57 @@ fn iterator() {
 }
 
 #[pyclass]
-struct Callable {}
+struct Callable;
 
 #[pymethods]
 impl Callable {
-    #[__call__]
     fn __call__(&self, arg: i32) -> i32 {
         arg * 6
     }
 }
 
 #[pyclass]
-struct EmptyClass;
+struct NotCallable;
 
 #[test]
 fn callable() {
     let gil = Python::acquire_gil();
     let py = gil.python();
 
-    let c = Py::new(py, Callable {}).unwrap();
+    let c = Py::new(py, Callable).unwrap();
     py_assert!(py, c, "callable(c)");
     py_assert!(py, c, "c(7) == 42");
 
-    let nc = Py::new(py, EmptyClass).unwrap();
+    let nc = Py::new(py, NotCallable).unwrap();
     py_assert!(py, nc, "not callable(nc)");
 }
 
+#[allow(deprecated)]
+mod deprecated {
+    use super::*;
+
+    #[pyclass]
+    struct Callable;
+
+    #[pymethods]
+    impl Callable {
+        #[__call__]
+        fn __call__(&self, arg: i32) -> i32 {
+            arg * 6
+        }
+    }
+
+    #[test]
+    fn callable() {
+        let gil = Python::acquire_gil();
+        let py = gil.python();
+
+        let c = Py::new(py, Callable).unwrap();
+        py_assert!(py, c, "callable(c)");
+        py_assert!(py, c, "c(7) == 42");
+    }
+}
+
 #[pyclass]
 #[derive(Debug)]
 struct SetItem {
diff --git a/tests/test_dunder.rs b/tests/test_pyproto.rs
similarity index 96%
rename from tests/test_dunder.rs
rename to tests/test_pyproto.rs
index 52c190f40d6..da2dc0dee32 100644
--- a/tests/test_dunder.rs
+++ b/tests/test_pyproto.rs
@@ -210,30 +210,6 @@ fn sequence() {
     py_expect_exception!(py, c, "c['abc']", PyTypeError);
 }
 
-#[pyclass]
-struct Callable {}
-
-#[pymethods]
-impl Callable {
-    #[__call__]
-    fn __call__(&self, arg: i32) -> i32 {
-        arg * 6
-    }
-}
-
-#[test]
-fn callable() {
-    let gil = Python::acquire_gil();
-    let py = gil.python();
-
-    let c = Py::new(py, Callable {}).unwrap();
-    py_assert!(py, c, "callable(c)");
-    py_assert!(py, c, "c(7) == 42");
-
-    let nc = Py::new(py, Comparisons { val: 0 }).unwrap();
-    py_assert!(py, nc, "not callable(nc)");
-}
-
 #[pyclass]
 #[derive(Debug)]
 struct SetItem {
diff --git a/tests/ui/abi3_nativetype_inheritance.stderr b/tests/ui/abi3_nativetype_inheritance.stderr
index 92767f09925..a90d40442f5 100644
--- a/tests/ui/abi3_nativetype_inheritance.stderr
+++ b/tests/ui/abi3_nativetype_inheritance.stderr
@@ -6,15 +6,15 @@ error[E0277]: the trait bound `PyDict: PyClass` is not satisfied
     |
     = note: required because of the requirements on the impl of `PyClassBaseType` for `PyDict`
 note: required by a bound in `PyClassBaseType`
-   --> src/class/impl_.rs:775:1
+   --> src/class/impl_.rs:766:1
     |
-775 | / pub trait PyClassBaseType: Sized {
-776 | |     type Dict;
-777 | |     type WeakRef;
-778 | |     type LayoutAsBase: PyCellLayout<Self>;
+766 | / pub trait PyClassBaseType: Sized {
+767 | |     type Dict;
+768 | |     type WeakRef;
+769 | |     type LayoutAsBase: PyCellLayout<Self>;
 ...   |
-781 | |     type Initializer: PyObjectInit<Self>;
-782 | | }
+772 | |     type Initializer: PyObjectInit<Self>;
+773 | | }
     | |_^ required by this bound in `PyClassBaseType`
     = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info)
 
@@ -26,8 +26,8 @@ error[E0277]: the trait bound `PyDict: PyClass` is not satisfied
     |
     = note: required because of the requirements on the impl of `PyClassBaseType` for `PyDict`
 note: required by a bound in `ThreadCheckerInherited`
-   --> src/class/impl_.rs:762:47
+   --> src/class/impl_.rs:753:47
     |
-762 | pub struct ThreadCheckerInherited<T: Send, U: PyClassBaseType>(PhantomData<T>, U::ThreadChecker);
+753 | pub struct ThreadCheckerInherited<T: Send, U: PyClassBaseType>(PhantomData<T>, U::ThreadChecker);
     |                                               ^^^^^^^^^^^^^^^ required by this bound in `ThreadCheckerInherited`
     = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info)
diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs
index ca4805a32bb..50236b39a82 100644
--- a/tests/ui/deprecations.rs
+++ b/tests/ui/deprecations.rs
@@ -24,6 +24,15 @@ impl TestClass {
     fn deprecated_name_staticmethod() {}
 }
 
+#[pyclass]
+struct DeprecatedCall;
+
+#[pymethods]
+impl DeprecatedCall {
+    #[call]
+    fn deprecated_call(&self) {}
+}
+
 #[pyfunction]
 #[name = "foo"]
 #[text_signature = "()"]
diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr
index 0b78dcaf0d8..d0131a9c5d3 100644
--- a/tests/ui/deprecations.stderr
+++ b/tests/ui/deprecations.stderr
@@ -34,34 +34,40 @@ error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATT
 23 |     #[text_signature = "()"]
    |     ^
 
+error: use of deprecated constant `pyo3::impl_::deprecations::CALL_ATTRIBUTE`: use `fn __call__` instead of `#[call]`
+  --> tests/ui/deprecations.rs:32:7
+   |
+32 |     #[call]
+   |       ^^^^
+
 error: use of deprecated constant `pyo3::impl_::deprecations::NAME_ATTRIBUTE`: use `#[pyo3(name = "...")]` instead of `#[name = "..."]`
-  --> tests/ui/deprecations.rs:28:1
+  --> tests/ui/deprecations.rs:37:1
    |
-28 | #[name = "foo"]
+37 | #[name = "foo"]
    | ^
 
 error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]`
-  --> tests/ui/deprecations.rs:29:1
+  --> tests/ui/deprecations.rs:38:1
    |
-29 | #[text_signature = "()"]
+38 | #[text_signature = "()"]
    | ^
 
 error: use of deprecated constant `pyo3::impl_::deprecations::PYFN_NAME_ARGUMENT`: use `#[pyfn(m)] #[pyo3(name = "...")]` instead of `#[pyfn(m, "...")]`
-  --> tests/ui/deprecations.rs:34:15
+  --> tests/ui/deprecations.rs:43:15
    |
-34 |     #[pyfn(m, "some_name")]
+43 |     #[pyfn(m, "some_name")]
    |               ^^^^^^^^^^^
 
 error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]`
-  --> tests/ui/deprecations.rs:35:5
+  --> tests/ui/deprecations.rs:44:5
    |
-35 |     #[text_signature = "()"]
+44 |     #[text_signature = "()"]
    |     ^
 
 error: use of deprecated constant `pyo3::impl_::deprecations::PYMODULE_NAME_ARGUMENT`: use `#[pymodule] #[pyo3(name = "...")]` instead of `#[pymodule(...)]`
-  --> tests/ui/deprecations.rs:32:12
+  --> tests/ui/deprecations.rs:41:12
    |
-32 | #[pymodule(deprecated_module_name)]
+41 | #[pymodule(deprecated_module_name)]
    |            ^^^^^^^^^^^^^^^^^^^^^^
 
 error: use of deprecated constant `pyo3::impl_::deprecations::TEXT_SIGNATURE_ATTRIBUTE`: use `#[pyo3(text_signature = "...")]` instead of `#[text_signature = "..."]`
diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs
index 528af3197c1..fa9ee2bafd8 100644
--- a/tests/ui/invalid_pymethods.rs
+++ b/tests/ui/invalid_pymethods.rs
@@ -52,12 +52,12 @@ impl MyClass {
     fn text_signature_on_new() {}
 }
 
-#[pymethods]
-impl MyClass {
-    #[call]
-    #[pyo3(text_signature = "()")]
-    fn text_signature_on_call(&self) {}
-}
+// FIXME: this doesn't fail - should refuse text signature on protocol methods in general?
+// #[pymethods]
+// impl MyClass {
+//     #[pyo3(text_signature = "()")]
+//     fn __call__(&self) {}
+// }
 
 #[pymethods]
 impl MyClass {
diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr
index 6265c080ecc..4621a4492dc 100644
--- a/tests/ui/invalid_pymethods.stderr
+++ b/tests/ui/invalid_pymethods.stderr
@@ -40,12 +40,6 @@ error: text_signature not allowed on __new__; if you want to add a signature on
 51 |     #[pyo3(text_signature = "()")]
    |            ^^^^^^^^^^^^^^
 
-error: text_signature not allowed with this method type
-  --> tests/ui/invalid_pymethods.rs:58:12
-   |
-58 |     #[pyo3(text_signature = "()")]
-   |            ^^^^^^^^^^^^^^
-
 error: text_signature not allowed with this method type
   --> tests/ui/invalid_pymethods.rs:65:12
    |
diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr
index fd5779e61ef..56217f1e916 100644
--- a/tests/ui/pyclass_send.stderr
+++ b/tests/ui/pyclass_send.stderr
@@ -11,8 +11,8 @@ note: required because it appears within the type `NotThreadSafe`
 5   | struct NotThreadSafe {
     |        ^^^^^^^^^^^^^
 note: required by a bound in `ThreadCheckerStub`
-   --> src/class/impl_.rs:719:33
+   --> src/class/impl_.rs:710:33
     |
-719 | pub struct ThreadCheckerStub<T: Send>(PhantomData<T>);
+710 | pub struct ThreadCheckerStub<T: Send>(PhantomData<T>);
     |                                 ^^^^ required by this bound in `ThreadCheckerStub`
     = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info)