Function signatures
The #[pyfunction]
attribute also accepts parameters to control how the generated Python function accepts arguments. Just like in Python, arguments can be positional-only, keyword-only, or accept either. *args
lists and **kwargs
dicts can also be accepted. These parameters also work for #[pymethods]
which will be introduced in the Python Classes section of the guide.
Like Python, by default PyO3 accepts all arguments as either positional or keyword arguments. Most arguments are required by default, except for trailing Option<_>
arguments, which are implicitly given a default of None
. This behaviour can be configured by the #[pyo3(signature = (...))]
option which allows writing a signature in Python syntax.
This section of the guide goes into detail about use of the #[pyo3(signature = (...))]
option and its related option #[pyo3(text_signature = "...")]
Using #[pyo3(signature = (...))]
For example, below is a function that accepts arbitrary keyword arguments (**kwargs
in Python syntax) and returns the number that was passed:
use pyo3::prelude::*;
use pyo3::types::PyDict;
#[pyfunction]
#[pyo3(signature = (**kwds))]
fn num_kwds(kwds: Option<&Bound<'_, PyDict>>) -> usize {
kwds.map_or(0, |dict| dict.len())
}
#[pymodule]
fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(num_kwds, m)?)
}
Just like in Python, the following constructs can be part of the signature::
/
: positional-only arguments separator, each parameter defined before/
is a positional-only parameter.*
: var arguments separator, each parameter defined after*
is a keyword-only parameter.*args
: "args" is var args. Type of theargs
parameter has to be&Bound<'_, PyTuple>
.**kwargs
: "kwargs" receives keyword arguments. The type of thekwargs
parameter has to beOption<&Bound<'_, PyDict>>
.arg=Value
: arguments with default value. If thearg
argument is defined after var arguments, it is treated as a keyword-only argument. Note thatValue
has to be valid rust code, PyO3 just inserts it into the generated code unmodified.
Example:
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};
#[pyclass]
struct MyClass {
num: i32,
}
#[pymethods]
impl MyClass {
#[new]
#[pyo3(signature = (num=-1))]
fn new(num: i32) -> Self {
MyClass { num }
}
#[pyo3(signature = (num=10, *py_args, name="Hello", **py_kwargs))]
fn method(
&mut self,
num: i32,
py_args: &Bound<'_, PyTuple>,
name: &str,
py_kwargs: Option<&Bound<'_, PyDict>>,
) -> String {
let num_before = self.num;
self.num = num;
format!(
"num={} (was previously={}), py_args={:?}, name={}, py_kwargs={:?} ",
num, num_before, py_args, name, py_kwargs,
)
}
fn make_change(&mut self, num: i32) -> PyResult<String> {
self.num = num;
Ok(format!("num={}", self.num))
}
}
Arguments of type Python
must not be part of the signature:
#![allow(dead_code)]
use pyo3::prelude::*;
#[pyfunction]
#[pyo3(signature = (lambda))]
pub fn simple_python_bound_function(py: Python<'_>, lambda: PyObject) -> PyResult<()> {
Ok(())
}
N.B. the position of the /
and *
arguments (if included) control the system of handling positional and keyword arguments. In Python:
import mymodule
mc = mymodule.MyClass()
print(mc.method(44, False, "World", 666, x=44, y=55))
print(mc.method(num=-1, name="World"))
print(mc.make_change(44, False))
Produces output:
py_args=('World', 666), py_kwargs=Some({'x': 44, 'y': 55}), name=Hello, num=44
py_args=(), py_kwargs=None, name=World, num=-1
num=44
num=-1
Note: to use keywords like
struct
as a function argument, use "raw identifier" syntaxr#struct
in both the signature and the function definition:#![allow(dead_code)] use pyo3::prelude::*; #[pyfunction(signature = (r#struct = "foo"))] fn function_with_keyword(r#struct: &str) { let _ = r#struct; /* ... */ }
Trailing optional arguments
⚠️ Warning: This behaviour is being phased out 🛠️
The special casing of trailing optional arguments is deprecated. In a future pyo3
version, arguments of type Option<..>
will share the same behaviour as other arguments, they are required unless a default is set using #[pyo3(signature = (...))]
.
This is done to better align the Python and Rust definition of such functions and make it more intuitive to rewrite them from Python in Rust. Specifically def some_fn(a: int, b: Optional[int]): ...
will not automatically default b
to none
, but requires an explicit default if desired, where as in current pyo3
it is handled the other way around.
During the migration window a #[pyo3(signature = (...))]
will be required to silence the deprecation warning. After support for trailing optional arguments is fully removed, the signature attribute can be removed if all arguments should be required.
As a convenience, functions without a #[pyo3(signature = (...))]
option will treat trailing Option<T>
arguments as having a default of None
. In the example below, PyO3 will create increment
with a signature of increment(x, amount=None)
.
#![allow(deprecated)]
use pyo3::prelude::*;
/// Returns a copy of `x` increased by `amount`.
///
/// If `amount` is unspecified or `None`, equivalent to `x + 1`.
#[pyfunction]
fn increment(x: u64, amount: Option<u64>) -> u64 {
x + amount.unwrap_or(1)
}
fn main() -> PyResult<()> {
Python::with_gil(|py| {
let fun = pyo3::wrap_pyfunction_bound!(increment, py)?;
let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?;
let sig: String = inspect
.call1((fun,))?
.call_method0("__str__")?
.extract()?;
#[cfg(Py_3_8)] // on 3.7 the signature doesn't render b, upstream bug?
assert_eq!(sig, "(x, amount=None)");
Ok(())
})
}
To make trailing Option<T>
arguments required, but still accept None
, add a #[pyo3(signature = (...))]
annotation. For the example above, this would be #[pyo3(signature = (x, amount))]
:
use pyo3::prelude::*;
#[pyfunction]
#[pyo3(signature = (x, amount))]
fn increment(x: u64, amount: Option<u64>) -> u64 {
x + amount.unwrap_or(1)
}
fn main() -> PyResult<()> {
Python::with_gil(|py| {
let fun = pyo3::wrap_pyfunction_bound!(increment, py)?;
let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?;
let sig: String = inspect
.call1((fun,))?
.call_method0("__str__")?
.extract()?;
#[cfg(Py_3_8)] // on 3.7 the signature doesn't render b, upstream bug?
assert_eq!(sig, "(x, amount)");
Ok(())
})
}
To help avoid confusion, PyO3 requires #[pyo3(signature = (...))]
when an Option<T>
argument is surrounded by arguments which aren't Option<T>
.
Making the function signature available to Python
The function signature is exposed to Python via the __text_signature__
attribute. PyO3 automatically generates this for every #[pyfunction]
and all #[pymethods]
directly from the Rust function, taking into account any override done with the #[pyo3(signature = (...))]
option.
This automatic generation can only display the value of default arguments for strings, integers, boolean types, and None
. Any other default arguments will be displayed as ...
. (.pyi
type stub files commonly also use ...
for default arguments in the same way.)
In cases where the automatically-generated signature needs adjusting, it can be overridden using the #[pyo3(text_signature)]
option.)
The example below creates a function add
which accepts two positional-only arguments a
and b
, where b
has a default value of zero.
use pyo3::prelude::*;
/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[pyo3(signature = (a, b=0, /))]
fn add(a: u64, b: u64) -> u64 {
a + b
}
fn main() -> PyResult<()> {
Python::with_gil(|py| {
let fun = pyo3::wrap_pyfunction_bound!(add, py)?;
let doc: String = fun.getattr("__doc__")?.extract()?;
assert_eq!(doc, "This function adds two unsigned 64-bit integers.");
let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?;
let sig: String = inspect
.call1((fun,))?
.call_method0("__str__")?
.extract()?;
#[cfg(Py_3_8)] // on 3.7 the signature doesn't render b, upstream bug?
assert_eq!(sig, "(a, b=0, /)");
Ok(())
})
}
The following IPython output demonstrates how this generated signature will be seen from Python tooling:
>>> pyo3_test.add.__text_signature__
'(a, b=..., /)'
>>> pyo3_test.add?
Signature: pyo3_test.add(a, b=0, /)
Docstring: This function adds two unsigned 64-bit integers.
Type: builtin_function_or_method
Overriding the generated signature
The #[pyo3(text_signature = "(<some signature>)")]
attribute can be used to override the default generated signature.
In the snippet below, the text signature attribute is used to include the default value of 0
for the argument b
, instead of the automatically-generated default value of ...
:
use pyo3::prelude::*;
/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[pyo3(signature = (a, b=0, /), text_signature = "(a, b=0, /)")]
fn add(a: u64, b: u64) -> u64 {
a + b
}
fn main() -> PyResult<()> {
Python::with_gil(|py| {
let fun = pyo3::wrap_pyfunction_bound!(add, py)?;
let doc: String = fun.getattr("__doc__")?.extract()?;
assert_eq!(doc, "This function adds two unsigned 64-bit integers.");
let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?;
let sig: String = inspect
.call1((fun,))?
.call_method0("__str__")?
.extract()?;
assert_eq!(sig, "(a, b=0, /)");
Ok(())
})
}
PyO3 will include the contents of the annotation unmodified as the __text_signature__
. Below shows how IPython will now present this (see the default value of 0 for b):
>>> pyo3_test.add.__text_signature__
'(a, b=0, /)'
>>> pyo3_test.add?
Signature: pyo3_test.add(a, b=0, /)
Docstring: This function adds two unsigned 64-bit integers.
Type: builtin_function_or_method
If no signature is wanted at all, #[pyo3(text_signature = None)]
will disable the built-in signature. The snippet below demonstrates use of this:
use pyo3::prelude::*;
/// This function adds two unsigned 64-bit integers.
#[pyfunction]
#[pyo3(signature = (a, b=0, /), text_signature = None)]
fn add(a: u64, b: u64) -> u64 {
a + b
}
fn main() -> PyResult<()> {
Python::with_gil(|py| {
let fun = pyo3::wrap_pyfunction_bound!(add, py)?;
let doc: String = fun.getattr("__doc__")?.extract()?;
assert_eq!(doc, "This function adds two unsigned 64-bit integers.");
assert!(fun.getattr("__text_signature__")?.is_none());
Ok(())
})
}
Now the function's __text_signature__
will be set to None
, and IPython will not display any signature in the help:
>>> pyo3_test.add.__text_signature__ == None
True
>>> pyo3_test.add?
Docstring: This function adds two unsigned 64-bit integers.
Type: builtin_function_or_method