Repository: PyO3/pyo3 (Rust)

Issue:
Generated __new__ method does not check args to enforce signature
### Bug Description

Firstly, thank you for creating this amazing crate!

I noticed today that when using `#[new]` to generate a `__new__` method with an empty signature, the generated code doesn't enforce that _args and _kwargs must be empty. As a result, it's possible to call the constructor from Python and provide unused arguments without raising an exception. This could be confusing, for example the developer might expect the kwargs to set field values. I think this is a bug.

Here's part of the code generated by the macros, from `cargo expand`:

```
    impl Foo {
        unsafe extern "C" fn __pymethod___new____(
            subtype: *mut _pyo3::ffi::PyTypeObject,
            _args: *mut _pyo3::ffi::PyObject,
            _kwargs: *mut _pyo3::ffi::PyObject,
        ) -> *mut _pyo3::ffi::PyObject {
            use _pyo3::{callback::IntoPyCallbackOutput, pyclass_init::PyObjectInit};
            let gil = _pyo3::GILPool::new();
            let _py = gil.python();
            _pyo3::callback::panic_result_into_callback_output(
                _py,
                ::std::panic::catch_unwind(move || -> _pyo3::PyResult<_> {
                    let result = Foo::new();
                    let initializer: _pyo3::PyClassInitializer<Foo> = result.convert(_py)?;
                    initializer.into_new_object(_py, subtype)
                }),
            )
        }
    }
```

### Steps to Reproduce

```
#![allow(unused_variables)]
use pyo3::prelude::*;
use pyo3::types::IntoPyDict;

#[pyclass]
struct Foo {}

#[pymethods]
impl Foo {
    #[new]
    pub fn new() -> Self {
        Foo {}
    }
}

pub fn main() -> PyResult<()> {
    pyo3::prepare_freethreaded_python();
    Python::with_gil(|py| {
    	let locals = [("Foo", py.get_type::<Foo>())].into_py_dict(py);
    	py.run("print(Foo()); print(Foo(123)); print(Foo(bar=123))", Some(locals), None)
    })
}
```

This code prints three objects, without raising any exceptions. The expected behavior would be to print the first and raise an exception on the second (and third of course).

### Backtrace

_No response_

### Your operating system and version

WSL2

### Your Python version (`python --version`)

3.11.0

### Your Rust version (`rustc --version`)

rustc 1.60.0-nightly (22e491ac7 2022-01-13)

### Your PyO3 version

0.17.3

### How did you install python? Did you use a virtualenv?

venv

### Additional Info

_No response_


Discussion:
Indeed, this is due to [this check](https://github.com/PyO3/pyo3/blob/main/pyo3-macros-backend/src/params.rs#L35) which for empty signatures simply doesn't generate any code, even if it should check for absence of given args.



Patch:
diff --git a/guide/src/class.md b/guide/src/class.md
index 5e976817b20..1a19ceb969d 100644
--- a/guide/src/class.md
+++ b/guide/src/class.md
@@ -233,7 +233,7 @@ Consult the table below to determine which type your constructor should return:
 
 ## Inheritance
 
-By default, `PyAny` is used as the base class. To override this default,
+By default, `object`, i.e. `PyAny` is used as the base class. To override this default,
 use the `extends` parameter for `pyclass` with the full path to the base class.
 
 For convenience, `(T, U)` implements `Into<PyClassInitializer<T>>` where `U` is the
@@ -310,7 +310,8 @@ impl SubSubClass {
 ```
 
 You can also inherit native types such as `PyDict`, if they implement
-[`PySizedLayout`]({{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PySizedLayout.html). However, this is not supported when building for the Python limited API (aka the `abi3` feature of PyO3).
+[`PySizedLayout`]({{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PySizedLayout.html).
+This is not supported when building for the Python limited API (aka the `abi3` feature of PyO3).
 
 However, because of some technical problems, we don't currently provide safe upcasting methods for types
 that inherit native types. Even in such cases, you can unsafely get a base class by raw pointer conversion.
@@ -334,6 +335,7 @@ impl DictWithCounter {
     fn new() -> Self {
         Self::default()
     }
+
     fn set(mut self_: PyRefMut<'_, Self>, key: String, value: &PyAny) -> PyResult<()> {
         self_.counter.entry(key.clone()).or_insert(0);
         let py = self_.py();
@@ -371,6 +373,42 @@ impl SubClass {
 }
 ```
 
+The `__new__` constructor of a native base class is called implicitly when
+creating a new instance from Python.  Be sure to accept arguments in the
+`#[new]` method that you want the base class to get, even if they are not used
+in that `fn`:
+
+```rust
+# #[allow(dead_code)]
+# #[cfg(not(Py_LIMITED_API))] {
+# use pyo3::prelude::*;
+use pyo3::types::PyDict;
+
+#[pyclass(extends=PyDict)]
+struct MyDict {
+    private: i32,
+}
+
+#[pymethods]
+impl MyDict {
+    #[new]
+    #[pyo3(signature = (*args, **kwargs))]
+    fn new(args: &PyAny, kwargs: Option<&PyAny>) -> Self {
+        Self { private: 0 }
+    }
+
+    // some custom methods that use `private` here...
+}
+# Python::with_gil(|py| {
+#     let cls = py.get_type::<MyDict>();
+#     pyo3::py_run!(py, cls, "cls(a=1, b=2)")
+# });
+# }
+```
+
+Here, the `args` and `kwargs` allow creating instances of the subclass passing
+initial items, such as `MyDict(item_sequence)` or `MyDict(a=1, b=2)`.
+
 ## Object properties
 
 PyO3 supports two ways to add properties to your `#[pyclass]`:
diff --git a/newsfragments/2749.fixed.md b/newsfragments/2749.fixed.md
new file mode 100644
index 00000000000..e37742a1715
--- /dev/null
+++ b/newsfragments/2749.fixed.md
@@ -0,0 +1,2 @@
+Fix a bug that allowed `#[new]` pymethods with no arguments to be called from
+Python with any argument list.
diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs
index ab784591f46..a069d5596a0 100644
--- a/pyo3-macros-backend/src/params.rs
+++ b/pyo3-macros-backend/src/params.rs
@@ -32,10 +32,6 @@ pub fn impl_arg_params(
     py: &syn::Ident,
     fastcall: bool,
 ) -> Result<(TokenStream, Vec<TokenStream>)> {
-    if spec.signature.arguments.is_empty() {
-        return Ok((TokenStream::new(), vec![]));
-    }
-
     let args_array = syn::Ident::new("output", Span::call_site());
 
     if !fastcall && is_forwarded_args(&spec.signature) {


Test patch:
diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs
index 427557922e6..b9b0d152086 100644
--- a/tests/test_class_new.rs
+++ b/tests/test_class_new.rs
@@ -2,6 +2,7 @@
 
 use pyo3::exceptions::PyValueError;
 use pyo3::prelude::*;
+use pyo3::types::IntoPyDict;
 
 #[pyclass]
 struct EmptyClassWithNew {}
@@ -23,6 +24,12 @@ fn empty_class_with_new() {
             .unwrap()
             .downcast::<PyCell<EmptyClassWithNew>>()
             .is_ok());
+
+        // Calling with arbitrary args or kwargs is not ok
+        assert!(typeobj.call(("some", "args"), None).is_err());
+        assert!(typeobj
+            .call((), Some([("some", "kwarg")].into_py_dict(py)))
+            .is_err());
     });
 }
 
diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs
index be4c75bf024..8ef76f87a30 100644
--- a/tests/test_inheritance.rs
+++ b/tests/test_inheritance.rs
@@ -259,7 +259,7 @@ mod inheriting_native_type {
     #[pymethods]
     impl CustomException {
         #[new]
-        fn new() -> Self {
+        fn new(_exc_arg: &PyAny) -> Self {
             CustomException {
                 context: "Hello :)",
             }