Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0755274
Add PyCallbackOutput for () and finalizefunc trampoline
messense Feb 28, 2026
9a20579
Map __del__ to Py_tp_finalize slot in macro backend
messense Feb 28, 2026
6a8f38a
Call tp_finalize from PyO3's tp_dealloc implementations
messense Feb 28, 2026
13eba3f
Add tests for __del__ support
messense Feb 28, 2026
61a4006
Update documentation and changelog for __del__ support
messense Feb 28, 2026
722e038
Guard dealloc-dependent __del__ tests with cfg(not(Py_LIMITED_API))
messense Feb 28, 2026
1b21c3c
Add tests for cyclic GC finalization and object resurrection
messense Feb 28, 2026
b126fee
Fix panic safety in finalizefunc trampoline and add tests
messense Feb 28, 2026
6d18154
Fix cargo fmt
messense Feb 28, 2026
67c8e0c
Gate __del__ tests using UnraisableCapture on Py_3_8
messense Feb 28, 2026
dbe0b68
Fix test_del_via_cyclic_gc on free-threaded Python
messense Feb 28, 2026
f5fd8d2
Fix __del__ test failures on Python 3.7 and wasm32
messense Mar 1, 2026
7c7a534
Skip PyObject_CallFinalizerFromDealloc on GraalPy
messense Mar 15, 2026
66271a8
Fix non-exhaustive match for PyMethodProtoKind::Del
messense Mar 15, 2026
5ed877c
clarify `__del__` behavior
messense Mar 15, 2026
fd5cc16
Address review feedback for __del__ support
messense Mar 18, 2026
9289d59
Use SlotDef for __del__ codegen, add safety comments
messense Mar 18, 2026
61ce137
Add __del__ inheritance tests in test_inheritance
messense Mar 18, 2026
c4c160b
Validate __del__ receiver in slot calling convention
messense Mar 18, 2026
1518da8
Fix abi3 tp_finalize slot lookup
messense Mar 18, 2026
084ae7f
fix
messense Mar 18, 2026
7e55cb7
workaround compile error tests
messense Mar 18, 2026
c5a07a0
fix abi3 __del__: add refcount dance in tp_dealloc for limited API
messense Mar 18, 2026
c1d5d27
fix: inline refcount reset in call_finalize_from_dealloc
messense Mar 19, 2026
2cc1571
try to fix windows python 3.9 abi3 tests
messense Mar 19, 2026
9561ae9
Document why call_finalize_from_dealloc does not guard against panics
messense Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion guide/src/class/protocols.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Because of the double-underscores surrounding their name, these are also known a
PyO3 makes it possible for every magic method to be implemented in `#[pymethods]` just as they would be done in a regular Python class, with a few notable differences:

- `__new__` is replaced by the [`#[new]` attribute](../class.md#constructor).
- `__del__` is not yet supported, but may be in the future.
- `__buffer__` and `__release_buffer__` are currently not supported and instead PyO3 supports [`__getbuffer__` and `__releasebuffer__`](#buffer-objects) methods (these predate [PEP 688](https://peps.python.org/pep-0688/#python-level-buffer-protocol)), again this may change in the future.
- PyO3 adds [`__traverse__` and `__clear__`](#garbage-collector-integration) methods for controlling garbage collection.
- The Python C-API which PyO3 is implemented upon requires many magic methods to have a specific function signature in C and be placed into special "slots" on the class type object.
Expand Down Expand Up @@ -173,6 +172,13 @@ The given signatures should be interpreted as follows:
- `__call__(<self>, ...) -> object` - here, any argument list can be defined
as for normal `pymethods`

- `__del__(<self>) -> ()`

Called when the instance is about to be destroyed (the Python finalizer).
Exceptions raised in `__del__` are ignored and written to `sys.unraisablehook`.

> Note: on `abi3` builds, `__del__` may not be called during normal deallocation due to `PyObject_CallFinalizerFromDealloc` not being part of the stable ABI. > It will still be invoked by the cyclic garbage collector for GC-tracked types, and can be called explicitly via Python code.

### Iterable objects

Iterators can be defined using these methods:
Expand Down
1 change: 1 addition & 0 deletions newsfragments/2484.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added support for `__del__` magic method in `#[pymethods]`, mapped to `tp_finalize`.
119 changes: 93 additions & 26 deletions pyo3-macros-backend/src/pymethod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ impl PyMethodKind {
"__ior__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__IOR__)),
"__getbuffer__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__GETBUFFER__)),
"__releasebuffer__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__RELEASEBUFFER__)),
"__del__" => PyMethodKind::Proto(PyMethodProtoKind::Slot(&__DEL__)),
// Protocols implemented through traits
"__getattribute__" => {
PyMethodKind::Proto(PyMethodProtoKind::SlotFragment(&__GETATTRIBUTE__))
Expand Down Expand Up @@ -1062,6 +1063,9 @@ const __GETBUFFER__: SlotDef = SlotDef::new("Py_bf_getbuffer", "getbufferproc").
const __RELEASEBUFFER__: SlotDef =
SlotDef::new("Py_bf_releasebuffer", "releasebufferproc").require_unsafe();
const __CLEAR__: SlotDef = SlotDef::new("Py_tp_clear", "inquiry");
const __DEL__: SlotDef = SlotDef::new("Py_tp_finalize", "finalizefunc")
.ffi_cast_ty_override("destructor")
.require_instance_method();

#[derive(Clone, Copy)]
enum Ty {
Expand Down Expand Up @@ -1260,73 +1264,123 @@ impl ReturnMode {

pub struct SlotDef {
slot: StaticIdent,
func_ty: StaticIdent,
trampoline_ty: StaticIdent,
/// When the FFI type name differs from the trampoline module name, this
/// overrides the type used in the `as ffi::<type>` cast.
ffi_cast_ty: Option<StaticIdent>,
calling_convention: SlotCallingConvention,
ret_ty: Ty,
extract_error_mode: ExtractErrorMode,
return_mode: Option<ReturnMode>,
require_unsafe: bool,
}

#[derive(Clone, Copy)]
enum SlotReceiverKind {
Instance,
}

#[derive(Clone, Copy)]
enum SlotCallingConvention {
/// Specific set of arguments for the slot function
FixedArguments(&'static [Ty]),
FixedArguments {
arguments: &'static [Ty],
receiver_kind: Option<SlotReceiverKind>,
},
/// Arbitrary arguments for `__new__` from the signature (extracted from args / kwargs)
TpNew,
TpInit,
}

impl SlotCallingConvention {
const fn fixed_arguments(arguments: &'static [Ty]) -> Self {
Self::FixedArguments {
arguments,
receiver_kind: None,
}
}

const fn require_instance_method(self) -> Self {
match self {
Self::FixedArguments { arguments, .. } => Self::FixedArguments {
arguments,
receiver_kind: Some(SlotReceiverKind::Instance),
},
Self::TpNew | Self::TpInit => {
panic!("require_instance_method only supported for fixed-argument slots")
}
}
}

fn ensure_receiver_kind(&self, spec: &FnSpec<'_>, method_name: &str) -> Result<()> {
match self {
Self::FixedArguments {
receiver_kind: Some(SlotReceiverKind::Instance),
..
} => match spec.tp {
FnType::Fn(_) => Ok(()),
_ => bail_spanned!(
spec.name.span() => format!("expected instance method for `{}` function", method_name)
),
},
Self::FixedArguments { .. } | Self::TpNew | Self::TpInit => Ok(()),
}
}
}

impl SlotDef {
const fn new(slot: &'static str, func_ty: &'static str) -> Self {
// The FFI function pointer type determines the arguments and return type
let (calling_convention, ret_ty) = match func_ty.as_bytes() {
const fn new(slot: &'static str, trampoline_ty: &'static str) -> Self {
// The trampoline function type determines the arguments and return type.
let (calling_convention, ret_ty) = match trampoline_ty.as_bytes() {
b"newfunc" => (SlotCallingConvention::TpNew, Ty::Object),
b"initproc" => (SlotCallingConvention::TpInit, Ty::Int),
b"reprfunc" => (SlotCallingConvention::FixedArguments(&[]), Ty::Object),
b"hashfunc" => (SlotCallingConvention::FixedArguments(&[]), Ty::PyHashT),
b"reprfunc" => (SlotCallingConvention::fixed_arguments(&[]), Ty::Object),
b"hashfunc" => (SlotCallingConvention::fixed_arguments(&[]), Ty::PyHashT),
b"richcmpfunc" => (
SlotCallingConvention::FixedArguments(&[Ty::Object, Ty::CompareOp]),
SlotCallingConvention::fixed_arguments(&[Ty::Object, Ty::CompareOp]),
Ty::Object,
),
b"descrgetfunc" => (
SlotCallingConvention::FixedArguments(&[Ty::MaybeNullObject, Ty::MaybeNullObject]),
SlotCallingConvention::fixed_arguments(&[Ty::MaybeNullObject, Ty::MaybeNullObject]),
Ty::Object,
),
b"getiterfunc" => (SlotCallingConvention::FixedArguments(&[]), Ty::Object),
b"iternextfunc" => (SlotCallingConvention::FixedArguments(&[]), Ty::Object),
b"unaryfunc" => (SlotCallingConvention::FixedArguments(&[]), Ty::Object),
b"lenfunc" => (SlotCallingConvention::FixedArguments(&[]), Ty::PySsizeT),
b"getiterfunc" => (SlotCallingConvention::fixed_arguments(&[]), Ty::Object),
b"iternextfunc" => (SlotCallingConvention::fixed_arguments(&[]), Ty::Object),
b"unaryfunc" => (SlotCallingConvention::fixed_arguments(&[]), Ty::Object),
b"lenfunc" => (SlotCallingConvention::fixed_arguments(&[]), Ty::PySsizeT),
b"objobjproc" => (
SlotCallingConvention::FixedArguments(&[Ty::Object]),
SlotCallingConvention::fixed_arguments(&[Ty::Object]),
Ty::Int,
),
b"binaryfunc" => (
SlotCallingConvention::FixedArguments(&[Ty::Object]),
SlotCallingConvention::fixed_arguments(&[Ty::Object]),
Ty::Object,
),
b"inquiry" => (SlotCallingConvention::FixedArguments(&[]), Ty::Int),
b"inquiry" => (SlotCallingConvention::fixed_arguments(&[]), Ty::Int),
b"ssizeargfunc" => (
SlotCallingConvention::FixedArguments(&[Ty::PySsizeT]),
SlotCallingConvention::fixed_arguments(&[Ty::PySsizeT]),
Ty::Object,
),
b"getbufferproc" => (
SlotCallingConvention::FixedArguments(&[Ty::PyBuffer, Ty::Int]),
SlotCallingConvention::fixed_arguments(&[Ty::PyBuffer, Ty::Int]),
Ty::Int,
),
b"releasebufferproc" => (
SlotCallingConvention::FixedArguments(&[Ty::PyBuffer]),
SlotCallingConvention::fixed_arguments(&[Ty::PyBuffer]),
Ty::Void,
),
b"finalizefunc" => (SlotCallingConvention::fixed_arguments(&[]), Ty::Void),
b"ipowfunc" => (
SlotCallingConvention::FixedArguments(&[Ty::Object, Ty::IPowModulo]),
SlotCallingConvention::fixed_arguments(&[Ty::Object, Ty::IPowModulo]),
Ty::Object,
),
_ => panic!("don't know calling convention for func_ty"),
_ => panic!("don't know calling convention for trampoline_ty"),
};

SlotDef {
slot: StaticIdent::new(slot),
func_ty: StaticIdent::new(func_ty),
trampoline_ty: StaticIdent::new(trampoline_ty),
ffi_cast_ty: None,
calling_convention,
ret_ty,
extract_error_mode: ExtractErrorMode::Raise,
Expand All @@ -1342,6 +1396,11 @@ impl SlotDef {
.return_self()
}

const fn ffi_cast_ty_override(mut self, ffi_cast_ty: &'static str) -> Self {
self.ffi_cast_ty = Some(StaticIdent::new(ffi_cast_ty));
self
}

const fn return_conversion(mut self, return_conversion: TokenGenerator) -> Self {
self.return_mode = Some(ReturnMode::Conversion(return_conversion));
self
Expand Down Expand Up @@ -1371,6 +1430,11 @@ impl SlotDef {
self
}

const fn require_instance_method(mut self) -> Self {
self.calling_convention = self.calling_convention.require_instance_method();
self
}

pub fn generate_type_slot(
&self,
cls: &syn::Type,
Expand All @@ -1381,19 +1445,22 @@ impl SlotDef {
let Ctx { pyo3_path, .. } = ctx;
let SlotDef {
slot,
func_ty,
trampoline_ty,
ffi_cast_ty,
calling_convention,
extract_error_mode,
ret_ty,
return_mode,
require_unsafe,
} = self;
let ffi_cast_ty = ffi_cast_ty.unwrap_or(*trampoline_ty);
if *require_unsafe {
ensure_spanned!(
spec.unsafety.is_some(),
spec.name.span() => format!("`{}` must be `unsafe fn`", method_name)
);
}
calling_convention.ensure_receiver_kind(spec, method_name)?;
let wrapper_ident = format_ident!("__pymethod_{}__", method_name);
let ret_ty = ret_ty.ffi_type(ctx);
let mut holders = Holders::new();
Expand Down Expand Up @@ -1426,7 +1493,7 @@ impl SlotDef {
let slot_def = quote! {
#pyo3_path::ffi::PyType_Slot {
slot: #pyo3_path::ffi::#slot,
pfunc: #pyo3_path::impl_::trampoline::get_trampoline_function!(#func_ty, #cls::#wrapper_ident) as #pyo3_path::ffi::#func_ty as _
pfunc: #pyo3_path::impl_::trampoline::get_trampoline_function!(#trampoline_ty, #cls::#wrapper_ident) as #pyo3_path::ffi::#ffi_cast_ty as _
}
};
Ok(MethodAndSlotDef {
Expand Down Expand Up @@ -1534,7 +1601,7 @@ fn generate_method_body(
};
(arg_idents, arg_types, body)
}
SlotCallingConvention::FixedArguments(arguments) => {
SlotCallingConvention::FixedArguments { arguments, .. } => {
let arg_idents: Vec<_> = std::iter::once(format_ident!("_slf"))
.chain((0..arguments.len()).map(|i| format_ident!("arg{}", i)))
.collect();
Expand Down Expand Up @@ -1630,7 +1697,7 @@ impl SlotFragmentDef {
} = generate_method_body(
cls,
spec,
&SlotCallingConvention::FixedArguments(arguments),
&SlotCallingConvention::fixed_arguments(arguments),
*extract_error_mode,
&mut holders,
None,
Expand Down
5 changes: 5 additions & 0 deletions src/impl_/callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod py_callback_output {
impl Sealed for *mut PyObject {}
impl Sealed for c_int {}
impl Sealed for Py_ssize_t {}
impl Sealed for () {}
}

impl PyCallbackOutput for *mut ffi::PyObject {
Expand All @@ -39,6 +40,10 @@ impl PyCallbackOutput for ffi::Py_ssize_t {
const ERR_VALUE: Self = -1;
}

impl PyCallbackOutput for () {
const ERR_VALUE: Self = ();
}

/// Convert the result of callback function into the appropriate return value.
pub trait IntoPyCallbackOutput<'py, Target>: into_py_callback_output::Sealed<'py, Target> {
fn convert(self, py: Python<'py>) -> PyResult<Target>;
Expand Down
Loading
Loading