diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 25c8014f762..d2f74401dd6 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -24,6 +24,7 @@ jobs:
   build:
     continue-on-error: ${{ endsWith(inputs.python-version, '-dev') || contains(fromJSON('["3.7", "pypy3.7"]'), inputs.python-version) || inputs.rust == 'beta' || inputs.rust == 'nightly' }}
     runs-on: ${{ inputs.os }}
+    if: ${{ !(startsWith(inputs.python-version, 'graalpy') && startsWith(inputs.os, 'windows')) }}
     steps:
       - uses: actions/checkout@v4
 
@@ -65,6 +66,7 @@ jobs:
         run: nox -s docs
 
       - name: Build (no features)
+        if: ${{ !startsWith(inputs.python-version, 'graalpy') }}
         run: cargo build --lib --tests --no-default-features
 
       # --no-default-features when used with `cargo build/test -p` doesn't seem to work!
@@ -74,7 +76,7 @@ jobs:
           cargo build --no-default-features
 
       # Run tests (except on PyPy, because no embedding API).
-      - if: ${{ !startsWith(inputs.python-version, 'pypy') }}
+      - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }}
         name: Test (no features)
         run: cargo test --no-default-features --lib --tests
 
@@ -85,6 +87,7 @@ jobs:
           cargo test --no-default-features
 
       - name: Build (all additive features)
+        if: ${{ !startsWith(inputs.python-version, 'graalpy') }}
         run: cargo build --lib --tests --no-default-features --features "full ${{ inputs.extra-features }}"
 
       - if: ${{ startsWith(inputs.python-version, 'pypy') }}
@@ -92,17 +95,17 @@ jobs:
         run: cargo build --lib --tests --no-default-features --features "abi3-py37 full ${{ inputs.extra-features }}"
 
       # Run tests (except on PyPy, because no embedding API).
-      - if: ${{ !startsWith(inputs.python-version, 'pypy') }}
+      - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }}
         name: Test
         run: cargo test --no-default-features --features "full ${{ inputs.extra-features }}"
 
       # Run tests again, but in abi3 mode
-      - if: ${{ !startsWith(inputs.python-version, 'pypy') }}
+      - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }}
         name: Test (abi3)
         run: cargo test --no-default-features --features "abi3 full ${{ inputs.extra-features }}"
 
       # Run tests again, for abi3-py37 (the minimal Python version)
-      - if: ${{ (!startsWith(inputs.python-version, 'pypy')) && (inputs.python-version != '3.7') }}
+      - if: ${{ (!startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy')) && (inputs.python-version != '3.7') }}
         name: Test (abi3-py37)
         run: cargo test --no-default-features --features "abi3-py37 full ${{ inputs.extra-features }}"
 
@@ -120,7 +123,7 @@ jobs:
 
       - uses: dorny/paths-filter@v3
         # pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here
-        if: ${{ inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' }}
+        if: ${{ inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !startsWith(inputs.python-version, 'graalpy') }}
         id: ffi-changes
         with:
           base: ${{ github.event.pull_request.base.ref || github.event.merge_group.base_ref }}
@@ -135,7 +138,7 @@ jobs:
       - name: Run pyo3-ffi-check
         # pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here, nor
         # is pypy 3.9 on windows
-        if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }}
+        if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !startsWith(inputs.python-version, 'graalpy') && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }}
         run: nox -s ffi-check
 
     env:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5ee0776ebed..63f6c31a599 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -228,6 +228,7 @@ jobs:
           "pypy3.8",
           "pypy3.9",
           "pypy3.10",
+          "graalpy24.0",
         ]
         platform:
           [
diff --git a/README.md b/README.md
index c11754c3ca1..461e56612ad 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
 ## Usage
 
 PyO3 supports the following software versions:
-  - Python 3.7 and up (CPython and PyPy)
+  - Python 3.7 and up (CPython, PyPy, and GraalPy)
   - Rust 1.56 and up
 
 You can use PyO3 to write a native Python module in Rust, or to embed Python in a Rust binary. The following sections explain each of these in turn.
diff --git a/newsfragments/3247.added.md b/newsfragments/3247.added.md
new file mode 100644
index 00000000000..dd2641c3194
--- /dev/null
+++ b/newsfragments/3247.added.md
@@ -0,0 +1 @@
+Added support for running PyO3 extensions on GraalPy.
diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs
index 7f02f744fc2..d5373db9655 100644
--- a/pyo3-build-config/src/impl_.rs
+++ b/pyo3-build-config/src/impl_.rs
@@ -32,6 +32,12 @@ use crate::{
 /// Minimum Python version PyO3 supports.
 const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 };
 
+/// GraalPy may implement the same CPython version over multiple releases.
+const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion {
+    major: 24,
+    minor: 0,
+};
+
 /// Maximum Python version that can be used as minimum required Python version with abi3.
 const ABI3_MAX_MINOR: u8 = 12;
 
@@ -173,6 +179,11 @@ impl InterpreterConfig {
                     See https://foss.heptapod.net/pypy/pypy/-/issues/3397 for more information."
                 ));
             }
+        } else if self.implementation.is_graalpy() {
+            println!("cargo:rustc-cfg=GraalPy");
+            if self.abi3 {
+                warn!("GraalPy does not support abi3 so the build artifacts will be version-specific.");
+            }
         } else if self.abi3 {
             out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned());
         }
@@ -197,6 +208,12 @@ import sys
 from sysconfig import get_config_var, get_platform
 
 PYPY = platform.python_implementation() == "PyPy"
+GRAALPY = platform.python_implementation() == "GraalVM"
+
+if GRAALPY:
+    graalpy_ver = map(int, __graalpython__.get_graalvm_version().split('.'));
+    print("graalpy_major", next(graalpy_ver))
+    print("graalpy_minor", next(graalpy_ver))
 
 # sys.base_prefix is missing on Python versions older than 3.3; this allows the script to continue
 # so that the version mismatch can be reported in a nicer way later.
@@ -226,7 +243,7 @@ SHARED = bool(get_config_var("Py_ENABLE_SHARED"))
 print("implementation", platform.python_implementation())
 print("version_major", sys.version_info[0])
 print("version_minor", sys.version_info[1])
-print("shared", PYPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED)
+print("shared", PYPY or GRAALPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED)
 print_if_set("ld_version", get_config_var("LDVERSION"))
 print_if_set("libdir", get_config_var("LIBDIR"))
 print_if_set("base_prefix", base_prefix)
@@ -244,6 +261,23 @@ print("ext_suffix", get_config_var("EXT_SUFFIX"))
             interpreter.as_ref().display()
         );
 
+        if let Some(value) = map.get("graalpy_major") {
+            let graalpy_version = PythonVersion {
+                major: value
+                    .parse()
+                    .context("failed to parse GraalPy major version")?,
+                minor: map["graalpy_minor"]
+                    .parse()
+                    .context("failed to parse GraalPy minor version")?,
+            };
+            ensure!(
+                graalpy_version >= MINIMUM_SUPPORTED_VERSION_GRAALPY,
+                "At least GraalPy version {} needed, got {}",
+                MINIMUM_SUPPORTED_VERSION_GRAALPY,
+                graalpy_version
+            );
+        };
+
         let shared = map["shared"].as_str() == "True";
 
         let version = PythonVersion {
@@ -588,7 +622,7 @@ print("ext_suffix", get_config_var("EXT_SUFFIX"))
     /// Lowers the configured version to the abi3 version, if set.
     fn fixup_for_abi3_version(&mut self, abi3_version: Option<PythonVersion>) -> Result<()> {
         // PyPy doesn't support abi3; don't adjust the version
-        if self.implementation.is_pypy() {
+        if self.implementation.is_pypy() || self.implementation.is_graalpy() {
             return Ok(());
         }
 
@@ -647,6 +681,7 @@ impl FromStr for PythonVersion {
 pub enum PythonImplementation {
     CPython,
     PyPy,
+    GraalPy,
 }
 
 impl PythonImplementation {
@@ -655,12 +690,19 @@ impl PythonImplementation {
         self == PythonImplementation::PyPy
     }
 
+    #[doc(hidden)]
+    pub fn is_graalpy(self) -> bool {
+        self == PythonImplementation::GraalPy
+    }
+
     #[doc(hidden)]
     pub fn from_soabi(soabi: &str) -> Result<Self> {
         if soabi.starts_with("pypy") {
             Ok(PythonImplementation::PyPy)
         } else if soabi.starts_with("cpython") {
             Ok(PythonImplementation::CPython)
+        } else if soabi.starts_with("graalpy") {
+            Ok(PythonImplementation::GraalPy)
         } else {
             bail!("unsupported Python interpreter");
         }
@@ -672,6 +714,7 @@ impl Display for PythonImplementation {
         match self {
             PythonImplementation::CPython => write!(f, "CPython"),
             PythonImplementation::PyPy => write!(f, "PyPy"),
+            PythonImplementation::GraalPy => write!(f, "GraalVM"),
         }
     }
 }
@@ -682,6 +725,7 @@ impl FromStr for PythonImplementation {
         match s {
             "CPython" => Ok(PythonImplementation::CPython),
             "PyPy" => Ok(PythonImplementation::PyPy),
+            "GraalVM" => Ok(PythonImplementation::GraalPy),
             _ => bail!("unknown interpreter: {}", s),
         }
     }
@@ -760,7 +804,7 @@ pub struct CrossCompileConfig {
     /// The version of the Python library to link against.
     version: Option<PythonVersion>,
 
-    /// The target Python implementation hint (CPython or PyPy)
+    /// The target Python implementation hint (CPython, PyPy, GraalPy, ...)
     implementation: Option<PythonImplementation>,
 
     /// The compile target triple (e.g. aarch64-unknown-linux-gnu)
@@ -1264,6 +1308,15 @@ fn is_pypy_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool {
     path == "lib_pypy" || path.starts_with(&pypy_version_pat)
 }
 
+fn is_graalpy_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool {
+    let graalpy_version_pat = if let Some(v) = v {
+        format!("graalpy{}", v)
+    } else {
+        "graalpy2".into()
+    };
+    path == "lib_graalpython" || path.starts_with(&graalpy_version_pat)
+}
+
 fn is_cpython_lib_dir(path: &str, v: &Option<PythonVersion>) -> bool {
     let cpython_version_pat = if let Some(v) = v {
         format!("python{}", v)
@@ -1297,6 +1350,7 @@ fn search_lib_dir(path: impl AsRef<Path>, cross: &CrossCompileConfig) -> Vec<Pat
                     search_lib_dir(f.path(), cross)
                 } else if is_cpython_lib_dir(&file_name, &cross.version)
                     || is_pypy_lib_dir(&file_name, &cross.version)
+                    || is_graalpy_lib_dir(&file_name, &cross.version)
                 {
                     search_lib_dir(f.path(), cross)
                 } else {
@@ -1418,7 +1472,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result<In
 ///
 /// Must be called from a PyO3 crate build script.
 fn default_abi3_config(host: &Triple, version: PythonVersion) -> InterpreterConfig {
-    // FIXME: PyPy does not support the Stable ABI yet.
+    // FIXME: PyPy & GraalPy do not support the Stable ABI.
     let implementation = PythonImplementation::CPython;
     let abi3 = true;
 
@@ -1524,7 +1578,7 @@ fn default_lib_name_windows(
         // CPython bug: linking against python3_d.dll raises error
         // https://github.com/python/cpython/issues/101614
         format!("python{}{}_d", version.major, version.minor)
-    } else if abi3 && !implementation.is_pypy() {
+    } else if abi3 && !(implementation.is_pypy() || implementation.is_graalpy()) {
         WINDOWS_ABI3_LIB_NAME.to_owned()
     } else if mingw {
         // https://packages.msys2.org/base/mingw-w64-python
@@ -1562,6 +1616,7 @@ fn default_lib_name_unix(
                 format!("pypy{}-c", version.major)
             }
         }
+        PythonImplementation::GraalPy => "python-native".to_string(),
     }
 }
 
@@ -1662,7 +1717,9 @@ pub fn find_interpreter() -> Result<PathBuf> {
             .find(|bin| {
                 if let Ok(out) = Command::new(bin).arg("--version").output() {
                     // begin with `Python 3.X.X :: additional info`
-                    out.stdout.starts_with(b"Python 3") || out.stderr.starts_with(b"Python 3")
+                    out.stdout.starts_with(b"Python 3")
+                        || out.stderr.starts_with(b"Python 3")
+                        || out.stdout.starts_with(b"GraalPy 3")
                 } else {
                     false
                 }
diff --git a/pyo3-build-config/src/import_lib.rs b/pyo3-build-config/src/import_lib.rs
index dc21638c3db..0925a861b5b 100644
--- a/pyo3-build-config/src/import_lib.rs
+++ b/pyo3-build-config/src/import_lib.rs
@@ -7,7 +7,7 @@ use python3_dll_a::ImportLibraryGenerator;
 use target_lexicon::{Architecture, OperatingSystem, Triple};
 
 use super::{PythonImplementation, PythonVersion};
-use crate::errors::{Context, Result};
+use crate::errors::{Context, Error, Result};
 
 /// Generates the `python3.dll` or `pythonXY.dll` import library for Windows targets.
 ///
@@ -42,6 +42,9 @@ pub(super) fn generate_import_lib(
     let implementation = match py_impl {
         PythonImplementation::CPython => python3_dll_a::PythonImplementation::CPython,
         PythonImplementation::PyPy => python3_dll_a::PythonImplementation::PyPy,
+        PythonImplementation::GraalPy => {
+            return Err(Error::from("No support for GraalPy on Windows"))
+        }
     };
 
     ImportLibraryGenerator::new(&arch, &env)
diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs
index e7e59d3b79d..eab7376d0ea 100644
--- a/pyo3-build-config/src/lib.rs
+++ b/pyo3-build-config/src/lib.rs
@@ -38,6 +38,7 @@ use target_lexicon::OperatingSystem;
 /// | `#[cfg(Py_3_7)]`, `#[cfg(Py_3_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]` | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_7)]` marks code which can run on Python 3.7 **and newer**. |
 /// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. |
 /// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. |
+/// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. |
 ///
 /// For examples of how to use these attributes, [see PyO3's guide](https://pyo3.rs/latest/building-and-distribution/multiple_python_versions.html).
 #[cfg(feature = "resolve-config")]
diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs
index 286767d8f25..a5ab352b6a7 100644
--- a/pyo3-ffi/build.rs
+++ b/pyo3-ffi/build.rs
@@ -29,6 +29,17 @@ const SUPPORTED_VERSIONS_PYPY: SupportedVersions = SupportedVersions {
     },
 };
 
+const SUPPORTED_VERSIONS_GRAALPY: SupportedVersions = SupportedVersions {
+    min: PythonVersion {
+        major: 3,
+        minor: 10,
+    },
+    max: PythonVersion {
+        major: 3,
+        minor: 11,
+    },
+};
+
 fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
     // This is an undocumented env var which is only really intended to be used in CI / for testing
     // and development.
@@ -73,6 +84,24 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> {
                 std::env::var("CARGO_PKG_VERSION").unwrap()
             );
         }
+        PythonImplementation::GraalPy => {
+            let versions = SUPPORTED_VERSIONS_GRAALPY;
+            ensure!(
+                interpreter_config.version >= versions.min,
+                "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})",
+                interpreter_config.version,
+                versions.min,
+            );
+            // GraalPy does not support abi3, so we cannot offer forward compatibility
+            ensure!(
+                interpreter_config.version <= versions.max,
+                "the configured GraalPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\
+                 = help: please check if an updated version of PyO3 is available. Current version: {}",
+                interpreter_config.version,
+                versions.max,
+                std::env::var("CARGO_PKG_VERSION").unwrap()
+            );
+        }
     }
 
     Ok(())
diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs
index 0b3b7dbb3c2..b5bf9cc3d35 100644
--- a/pyo3-ffi/src/abstract_.rs
+++ b/pyo3-ffi/src/abstract_.rs
@@ -23,6 +23,7 @@ pub unsafe fn PyObject_DelAttr(o: *mut PyObject, attr_name: *mut PyObject) -> c_
 extern "C" {
     #[cfg(all(
         not(PyPy),
+        not(GraalPy),
         any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // Added to python in 3.9 but to limited API in 3.10
     ))]
     #[cfg_attr(PyPy, link_name = "PyPyObject_CallNoArgs")]
diff --git a/pyo3-ffi/src/boolobject.rs b/pyo3-ffi/src/boolobject.rs
index 78972ff0835..10b5969fa4f 100644
--- a/pyo3-ffi/src/boolobject.rs
+++ b/pyo3-ffi/src/boolobject.rs
@@ -1,3 +1,4 @@
+#[cfg(not(GraalPy))]
 use crate::longobject::PyLongObject;
 use crate::object::*;
 use std::os::raw::{c_int, c_long};
@@ -16,20 +17,33 @@ pub unsafe fn PyBool_Check(op: *mut PyObject) -> c_int {
 
 #[cfg_attr(windows, link(name = "pythonXY"))]
 extern "C" {
+    #[cfg(not(GraalPy))]
     #[cfg_attr(PyPy, link_name = "_PyPy_FalseStruct")]
     static mut _Py_FalseStruct: PyLongObject;
+    #[cfg(not(GraalPy))]
     #[cfg_attr(PyPy, link_name = "_PyPy_TrueStruct")]
     static mut _Py_TrueStruct: PyLongObject;
+
+    #[cfg(GraalPy)]
+    static mut _Py_FalseStructReference: *mut PyObject;
+    #[cfg(GraalPy)]
+    static mut _Py_TrueStructReference: *mut PyObject;
 }
 
 #[inline]
 pub unsafe fn Py_False() -> *mut PyObject {
-    addr_of_mut!(_Py_FalseStruct) as *mut PyObject
+    #[cfg(not(GraalPy))]
+    return addr_of_mut!(_Py_FalseStruct) as *mut PyObject;
+    #[cfg(GraalPy)]
+    return _Py_FalseStructReference;
 }
 
 #[inline]
 pub unsafe fn Py_True() -> *mut PyObject {
-    addr_of_mut!(_Py_TrueStruct) as *mut PyObject
+    #[cfg(not(GraalPy))]
+    return addr_of_mut!(_Py_TrueStruct) as *mut PyObject;
+    #[cfg(GraalPy)]
+    return _Py_TrueStructReference;
 }
 
 #[inline]
diff --git a/pyo3-ffi/src/bytearrayobject.rs b/pyo3-ffi/src/bytearrayobject.rs
index a37deb410f7..c09eac5b22c 100644
--- a/pyo3-ffi/src/bytearrayobject.rs
+++ b/pyo3-ffi/src/bytearrayobject.rs
@@ -3,7 +3,7 @@ use crate::pyport::Py_ssize_t;
 use std::os::raw::{c_char, c_int};
 use std::ptr::addr_of_mut;
 
-#[cfg(not(any(PyPy, Py_LIMITED_API)))]
+#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct PyByteArrayObject {
@@ -17,7 +17,7 @@ pub struct PyByteArrayObject {
     pub ob_exports: c_int,
 }
 
-#[cfg(any(PyPy, Py_LIMITED_API))]
+#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))]
 opaque_struct!(PyByteArrayObject);
 
 #[cfg_attr(windows, link(name = "pythonXY"))]
diff --git a/pyo3-ffi/src/complexobject.rs b/pyo3-ffi/src/complexobject.rs
index 339f5d8c81a..a03d9b00932 100644
--- a/pyo3-ffi/src/complexobject.rs
+++ b/pyo3-ffi/src/complexobject.rs
@@ -30,6 +30,7 @@ extern "C" {
 // non-limited
 pub struct PyComplexObject {
     pub ob_base: PyObject,
+    #[cfg(not(GraalPy))]
     pub cval: Py_complex,
 }
 
diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs
index d2e3ca9d67a..cf95f6711d4 100644
--- a/pyo3-ffi/src/cpython/abstract_.rs
+++ b/pyo3-ffi/src/cpython/abstract_.rs
@@ -6,7 +6,7 @@ use crate::Py_buffer;
 
 #[cfg(Py_3_8)]
 use crate::pyport::PY_SSIZE_T_MAX;
-#[cfg(all(Py_3_8, not(PyPy)))]
+#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
 use crate::{
     vectorcallfunc, PyCallable_Check, PyThreadState, PyThreadState_GET, PyTuple_Check,
     PyType_HasFeature, Py_TPFLAGS_HAVE_VECTORCALL,
@@ -15,15 +15,15 @@ use crate::{
 use libc::size_t;
 
 extern "C" {
-    #[cfg(all(Py_3_8, not(PyPy)))]
+    #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
     pub fn _PyStack_AsDict(values: *const *mut PyObject, kwnames: *mut PyObject) -> *mut PyObject;
 }
 
-#[cfg(all(Py_3_8, not(PyPy)))]
+#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
 const _PY_FASTCALL_SMALL_STACK: size_t = 5;
 
 extern "C" {
-    #[cfg(all(Py_3_8, not(PyPy)))]
+    #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
     pub fn _Py_CheckFunctionResult(
         tstate: *mut PyThreadState,
         callable: *mut PyObject,
@@ -31,7 +31,7 @@ extern "C" {
         where_: *const c_char,
     ) -> *mut PyObject;
 
-    #[cfg(all(Py_3_8, not(PyPy)))]
+    #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
     pub fn _PyObject_MakeTpCall(
         tstate: *mut PyThreadState,
         callable: *mut PyObject,
@@ -52,7 +52,7 @@ pub unsafe fn PyVectorcall_NARGS(n: size_t) -> Py_ssize_t {
     (n as Py_ssize_t) & !PY_VECTORCALL_ARGUMENTS_OFFSET
 }
 
-#[cfg(all(Py_3_8, not(PyPy)))]
+#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
 #[inline(always)]
 pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option<vectorcallfunc> {
     assert!(!callable.is_null());
@@ -67,7 +67,7 @@ pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option<vectorcal
     *ptr
 }
 
-#[cfg(all(Py_3_8, not(PyPy)))]
+#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
 #[inline(always)]
 pub unsafe fn _PyObject_VectorcallTstate(
     tstate: *mut PyThreadState,
@@ -91,7 +91,7 @@ pub unsafe fn _PyObject_VectorcallTstate(
     }
 }
 
-#[cfg(all(Py_3_8, not(PyPy)))]
+#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
 #[inline(always)]
 pub unsafe fn PyObject_Vectorcall(
     callable: *mut PyObject,
@@ -114,7 +114,10 @@ extern "C" {
     ) -> *mut PyObject;
 
     #[cfg(Py_3_8)]
-    #[cfg_attr(all(not(PyPy), not(Py_3_9)), link_name = "_PyObject_VectorcallDict")]
+    #[cfg_attr(
+        all(not(any(PyPy, GraalPy)), not(Py_3_9)),
+        link_name = "_PyObject_VectorcallDict"
+    )]
     #[cfg_attr(all(PyPy, not(Py_3_9)), link_name = "_PyPyObject_VectorcallDict")]
     #[cfg_attr(all(PyPy, Py_3_9), link_name = "PyPyObject_VectorcallDict")]
     pub fn PyObject_VectorcallDict(
@@ -134,7 +137,7 @@ extern "C" {
     ) -> *mut PyObject;
 }
 
-#[cfg(all(Py_3_8, not(PyPy)))]
+#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
 #[inline(always)]
 pub unsafe fn _PyObject_FastCallTstate(
     tstate: *mut PyThreadState,
@@ -145,7 +148,7 @@ pub unsafe fn _PyObject_FastCallTstate(
     _PyObject_VectorcallTstate(tstate, func, args, nargs as size_t, std::ptr::null_mut())
 }
 
-#[cfg(all(Py_3_8, not(PyPy)))]
+#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
 #[inline(always)]
 pub unsafe fn _PyObject_FastCall(
     func: *mut PyObject,
@@ -155,7 +158,7 @@ pub unsafe fn _PyObject_FastCall(
     _PyObject_FastCallTstate(PyThreadState_GET(), func, args, nargs)
 }
 
-#[cfg(all(Py_3_8, not(PyPy)))]
+#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
 #[inline(always)]
 pub unsafe fn _PyObject_CallNoArg(func: *mut PyObject) -> *mut PyObject {
     _PyObject_VectorcallTstate(
@@ -173,7 +176,7 @@ extern "C" {
     pub fn _PyObject_CallNoArg(func: *mut PyObject) -> *mut PyObject;
 }
 
-#[cfg(all(Py_3_8, not(PyPy)))]
+#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))]
 #[inline(always)]
 pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *mut PyObject {
     assert!(!arg.is_null());
@@ -185,7 +188,7 @@ pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *m
 }
 
 extern "C" {
-    #[cfg(all(Py_3_9, not(PyPy)))]
+    #[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))]
     pub fn PyObject_VectorcallMethod(
         name: *mut PyObject,
         args: *const *mut PyObject,
@@ -194,7 +197,7 @@ extern "C" {
     ) -> *mut PyObject;
 }
 
-#[cfg(all(Py_3_9, not(PyPy)))]
+#[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))]
 #[inline(always)]
 pub unsafe fn PyObject_CallMethodNoArgs(
     self_: *mut PyObject,
@@ -208,7 +211,7 @@ pub unsafe fn PyObject_CallMethodNoArgs(
     )
 }
 
-#[cfg(all(Py_3_9, not(PyPy)))]
+#[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))]
 #[inline(always)]
 pub unsafe fn PyObject_CallMethodOneArg(
     self_: *mut PyObject,
@@ -236,7 +239,7 @@ extern "C" {
     pub fn PyObject_LengthHint(o: *mut PyObject, arg1: Py_ssize_t) -> Py_ssize_t;
 
     #[cfg(not(Py_3_11))] // moved to src/buffer.rs from 3.11
-    #[cfg(all(Py_3_9, not(PyPy)))]
+    #[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))]
     pub fn PyObject_CheckBuffer(obj: *mut PyObject) -> c_int;
 }
 
@@ -308,7 +311,7 @@ pub const PY_ITERSEARCH_INDEX: c_int = 2;
 pub const PY_ITERSEARCH_CONTAINS: c_int = 3;
 
 extern "C" {
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn _PySequence_IterSearch(
         seq: *mut PyObject,
         obj: *mut PyObject,
diff --git a/pyo3-ffi/src/cpython/bytesobject.rs b/pyo3-ffi/src/cpython/bytesobject.rs
index 912fc0ac427..fb0b38cf1d8 100644
--- a/pyo3-ffi/src/cpython/bytesobject.rs
+++ b/pyo3-ffi/src/cpython/bytesobject.rs
@@ -1,10 +1,10 @@
 use crate::object::*;
 use crate::Py_ssize_t;
-#[cfg(not(any(PyPy, Py_LIMITED_API)))]
+#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))]
 use std::os::raw::c_char;
 use std::os::raw::c_int;
 
-#[cfg(not(any(PyPy, Py_LIMITED_API)))]
+#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct PyBytesObject {
@@ -13,7 +13,7 @@ pub struct PyBytesObject {
     pub ob_sval: [c_char; 1],
 }
 
-#[cfg(any(PyPy, Py_LIMITED_API))]
+#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))]
 opaque_struct!(PyBytesObject);
 
 extern "C" {
diff --git a/pyo3-ffi/src/cpython/code.rs b/pyo3-ffi/src/cpython/code.rs
index 498eab59cce..74586eac595 100644
--- a/pyo3-ffi/src/cpython/code.rs
+++ b/pyo3-ffi/src/cpython/code.rs
@@ -3,10 +3,10 @@ use crate::pyport::Py_ssize_t;
 
 #[allow(unused_imports)]
 use std::os::raw::{c_char, c_int, c_short, c_uchar, c_void};
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 use std::ptr::addr_of_mut;
 
-#[cfg(all(Py_3_8, not(PyPy), not(Py_3_11)))]
+#[cfg(all(Py_3_8, not(any(PyPy, GraalPy)), not(Py_3_11)))]
 opaque_struct!(_PyOpcache);
 
 #[cfg(Py_3_12)]
@@ -73,10 +73,10 @@ pub struct _PyCoMonitoringData {
     pub per_instruction_tools: *mut u8,
 }
 
-#[cfg(all(not(PyPy), not(Py_3_7)))]
+#[cfg(all(not(any(PyPy, GraalPy)), not(Py_3_7)))]
 opaque_struct!(PyCodeObject);
 
-#[cfg(all(not(PyPy), Py_3_7, not(Py_3_8)))]
+#[cfg(all(not(any(PyPy, GraalPy)), Py_3_7, not(Py_3_8)))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct PyCodeObject {
@@ -102,7 +102,7 @@ pub struct PyCodeObject {
     pub co_extra: *mut c_void,
 }
 
-#[cfg(all(not(PyPy), Py_3_8, not(Py_3_11)))]
+#[cfg(all(not(any(PyPy, GraalPy)), Py_3_8, not(Py_3_11)))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct PyCodeObject {
@@ -136,7 +136,7 @@ pub struct PyCodeObject {
     pub co_opcache_size: c_uchar,
 }
 
-#[cfg(all(not(PyPy), Py_3_11))]
+#[cfg(all(not(any(PyPy, GraalPy)), Py_3_11))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct PyCodeObject {
@@ -230,26 +230,26 @@ pub const CO_FUTURE_GENERATOR_STOP: c_int = 0x8_0000;
 
 pub const CO_MAXBLOCKS: usize = 20;
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[cfg_attr(windows, link(name = "pythonXY"))]
 extern "C" {
     pub static mut PyCode_Type: PyTypeObject;
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub unsafe fn PyCode_Check(op: *mut PyObject) -> c_int {
     (Py_TYPE(op) == addr_of_mut!(PyCode_Type)) as c_int
 }
 
 #[inline]
-#[cfg(all(not(PyPy), Py_3_10, not(Py_3_11)))]
+#[cfg(all(not(any(PyPy, GraalPy)), Py_3_10, not(Py_3_11)))]
 pub unsafe fn PyCode_GetNumFree(op: *mut PyCodeObject) -> Py_ssize_t {
     crate::PyTuple_GET_SIZE((*op).co_freevars)
 }
 
 #[inline]
-#[cfg(all(not(Py_3_10), Py_3_11, not(PyPy)))]
+#[cfg(all(not(Py_3_10), Py_3_11, not(any(PyPy, GraalPy))))]
 pub unsafe fn PyCode_GetNumFree(op: *mut PyCodeObject) -> c_int {
     (*op).co_nfreevars
 }
@@ -265,6 +265,7 @@ extern "C" {
 }
 
 extern "C" {
+    #[cfg(not(GraalPy))]
     #[cfg_attr(PyPy, link_name = "PyPyCode_New")]
     pub fn PyCode_New(
         argcount: c_int,
@@ -283,6 +284,7 @@ extern "C" {
         firstlineno: c_int,
         lnotab: *mut PyObject,
     ) -> *mut PyCodeObject;
+    #[cfg(not(GraalPy))]
     #[cfg(Py_3_8)]
     pub fn PyCode_NewWithPosOnlyArgs(
         argcount: c_int,
@@ -302,12 +304,14 @@ extern "C" {
         firstlineno: c_int,
         lnotab: *mut PyObject,
     ) -> *mut PyCodeObject;
+    #[cfg(not(GraalPy))]
     #[cfg_attr(PyPy, link_name = "PyPyCode_NewEmpty")]
     pub fn PyCode_NewEmpty(
         filename: *const c_char,
         funcname: *const c_char,
         firstlineno: c_int,
     ) -> *mut PyCodeObject;
+    #[cfg(not(GraalPy))]
     pub fn PyCode_Addr2Line(arg1: *mut PyCodeObject, arg2: c_int) -> c_int;
     // skipped PyCodeAddressRange "for internal use only"
     // skipped _PyCode_CheckLineNumber
diff --git a/pyo3-ffi/src/cpython/compile.rs b/pyo3-ffi/src/cpython/compile.rs
index 71af81e83e5..8bce9dacb3b 100644
--- a/pyo3-ffi/src/cpython/compile.rs
+++ b/pyo3-ffi/src/cpython/compile.rs
@@ -30,7 +30,7 @@ pub struct PyCompilerFlags {
 
 // skipped non-limited _PyCompilerFlags_INIT
 
-#[cfg(all(Py_3_12, not(PyPy)))]
+#[cfg(all(Py_3_12, not(any(PyPy, GraalPy))))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct _PyCompilerSrcLocation {
@@ -42,7 +42,7 @@ pub struct _PyCompilerSrcLocation {
 
 // skipped SRC_LOCATION_FROM_AST
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct PyFutureFeatures {
diff --git a/pyo3-ffi/src/cpython/dictobject.rs b/pyo3-ffi/src/cpython/dictobject.rs
index 4af990a2d9a..74b970ebac2 100644
--- a/pyo3-ffi/src/cpython/dictobject.rs
+++ b/pyo3-ffi/src/cpython/dictobject.rs
@@ -7,6 +7,7 @@ opaque_struct!(PyDictKeysObject);
 #[cfg(Py_3_11)]
 opaque_struct!(PyDictValues);
 
+#[cfg(not(GraalPy))]
 #[repr(C)]
 #[derive(Debug)]
 pub struct PyDictObject {
diff --git a/pyo3-ffi/src/cpython/floatobject.rs b/pyo3-ffi/src/cpython/floatobject.rs
index e33da0b91b9..8c7ee88543d 100644
--- a/pyo3-ffi/src/cpython/floatobject.rs
+++ b/pyo3-ffi/src/cpython/floatobject.rs
@@ -1,9 +1,12 @@
+#[cfg(GraalPy)]
+use crate::PyFloat_AsDouble;
 use crate::{PyFloat_Check, PyObject};
 use std::os::raw::c_double;
 
 #[repr(C)]
 pub struct PyFloatObject {
     pub ob_base: PyObject,
+    #[cfg(not(GraalPy))]
     pub ob_fval: c_double,
 }
 
@@ -15,7 +18,10 @@ pub unsafe fn _PyFloat_CAST(op: *mut PyObject) -> *mut PyFloatObject {
 
 #[inline]
 pub unsafe fn PyFloat_AS_DOUBLE(op: *mut PyObject) -> c_double {
-    (*_PyFloat_CAST(op)).ob_fval
+    #[cfg(not(GraalPy))]
+    return (*_PyFloat_CAST(op)).ob_fval;
+    #[cfg(GraalPy)]
+    return PyFloat_AsDouble(op);
 }
 
 // skipped PyFloat_Pack2
diff --git a/pyo3-ffi/src/cpython/frameobject.rs b/pyo3-ffi/src/cpython/frameobject.rs
index 7410000ef45..a85818ace0a 100644
--- a/pyo3-ffi/src/cpython/frameobject.rs
+++ b/pyo3-ffi/src/cpython/frameobject.rs
@@ -1,17 +1,19 @@
+#[cfg(not(GraalPy))]
 use crate::cpython::code::PyCodeObject;
 use crate::object::*;
+#[cfg(not(GraalPy))]
 use crate::pystate::PyThreadState;
-#[cfg(not(any(PyPy, Py_3_11)))]
+#[cfg(not(any(PyPy, GraalPy, Py_3_11)))]
 use std::os::raw::c_char;
 use std::os::raw::c_int;
 use std::ptr::addr_of_mut;
 
-#[cfg(not(any(PyPy, Py_3_11)))]
+#[cfg(not(any(PyPy, GraalPy, Py_3_11)))]
 pub type PyFrameState = c_char;
 
 #[repr(C)]
 #[derive(Copy, Clone)]
-#[cfg(not(any(PyPy, Py_3_11)))]
+#[cfg(not(any(PyPy, GraalPy, Py_3_11)))]
 pub struct PyTryBlock {
     pub b_type: c_int,
     pub b_handler: c_int,
@@ -20,7 +22,7 @@ pub struct PyTryBlock {
 
 #[repr(C)]
 #[derive(Copy, Clone)]
-#[cfg(not(any(PyPy, Py_3_11)))]
+#[cfg(not(any(PyPy, GraalPy, Py_3_11)))]
 pub struct PyFrameObject {
     pub ob_base: PyVarObject,
     pub f_back: *mut PyFrameObject,
@@ -51,7 +53,7 @@ pub struct PyFrameObject {
     pub f_localsplus: [*mut PyObject; 1],
 }
 
-#[cfg(any(PyPy, Py_3_11))]
+#[cfg(any(PyPy, GraalPy, Py_3_11))]
 opaque_struct!(PyFrameObject);
 
 // skipped _PyFrame_IsRunnable
@@ -69,6 +71,7 @@ pub unsafe fn PyFrame_Check(op: *mut PyObject) -> c_int {
 }
 
 extern "C" {
+    #[cfg(not(GraalPy))]
     #[cfg_attr(PyPy, link_name = "PyPyFrame_New")]
     pub fn PyFrame_New(
         tstate: *mut PyThreadState,
@@ -79,7 +82,7 @@ extern "C" {
     // skipped _PyFrame_New_NoTrack
 
     pub fn PyFrame_BlockSetup(f: *mut PyFrameObject, _type: c_int, handler: c_int, level: c_int);
-    #[cfg(not(any(PyPy, Py_3_11)))]
+    #[cfg(not(any(PyPy, GraalPy, Py_3_11)))]
     pub fn PyFrame_BlockPop(f: *mut PyFrameObject) -> *mut PyTryBlock;
 
     pub fn PyFrame_LocalsToFast(f: *mut PyFrameObject, clear: c_int);
diff --git a/pyo3-ffi/src/cpython/funcobject.rs b/pyo3-ffi/src/cpython/funcobject.rs
index 1e9ee0cc18c..25de30d57f7 100644
--- a/pyo3-ffi/src/cpython/funcobject.rs
+++ b/pyo3-ffi/src/cpython/funcobject.rs
@@ -4,7 +4,7 @@ use std::ptr::addr_of_mut;
 
 use crate::PyObject;
 
-#[cfg(all(not(PyPy), not(Py_3_10)))]
+#[cfg(all(not(any(PyPy, GraalPy)), not(Py_3_10)))]
 #[repr(C)]
 pub struct PyFunctionObject {
     pub ob_base: PyObject,
@@ -24,7 +24,7 @@ pub struct PyFunctionObject {
     pub vectorcall: Option<crate::vectorcallfunc>,
 }
 
-#[cfg(all(not(PyPy), Py_3_10))]
+#[cfg(all(not(any(PyPy, GraalPy)), Py_3_10))]
 #[repr(C)]
 pub struct PyFunctionObject {
     pub ob_base: PyObject,
@@ -55,6 +55,11 @@ pub struct PyFunctionObject {
     pub func_name: *mut PyObject,
 }
 
+#[cfg(GraalPy)]
+pub struct PyFunctionObject {
+    pub ob_base: PyObject,
+}
+
 #[cfg_attr(windows, link(name = "pythonXY"))]
 extern "C" {
     #[cfg(not(all(PyPy, not(Py_3_8))))]
diff --git a/pyo3-ffi/src/cpython/genobject.rs b/pyo3-ffi/src/cpython/genobject.rs
index aaa03f82eef..73ebdb491ff 100644
--- a/pyo3-ffi/src/cpython/genobject.rs
+++ b/pyo3-ffi/src/cpython/genobject.rs
@@ -1,13 +1,13 @@
 use crate::object::*;
 use crate::PyFrameObject;
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 use crate::_PyErr_StackItem;
 #[cfg(Py_3_11)]
 use std::os::raw::c_char;
 use std::os::raw::c_int;
 use std::ptr::addr_of_mut;
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct PyGenObject {
diff --git a/pyo3-ffi/src/cpython/listobject.rs b/pyo3-ffi/src/cpython/listobject.rs
index 7fb2228fadd..ea15cfc1ff5 100644
--- a/pyo3-ffi/src/cpython/listobject.rs
+++ b/pyo3-ffi/src/cpython/listobject.rs
@@ -2,7 +2,7 @@ use crate::object::*;
 #[cfg(not(PyPy))]
 use crate::pyport::Py_ssize_t;
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct PyListObject {
@@ -11,7 +11,7 @@ pub struct PyListObject {
     pub allocated: Py_ssize_t,
 }
 
-#[cfg(PyPy)]
+#[cfg(any(PyPy, GraalPy))]
 pub struct PyListObject {
     pub ob_base: PyObject,
 }
@@ -22,14 +22,14 @@ pub struct PyListObject {
 
 /// Macro, trading safety for speed
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub unsafe fn PyList_GET_ITEM(op: *mut PyObject, i: Py_ssize_t) -> *mut PyObject {
     *(*(op as *mut PyListObject)).ob_item.offset(i)
 }
 
 /// Macro, *only* to be used to fill in brand new lists
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub unsafe fn PyList_SET_ITEM(op: *mut PyObject, i: Py_ssize_t, v: *mut PyObject) {
     *(*(op as *mut PyListObject)).ob_item.offset(i) = v;
 }
diff --git a/pyo3-ffi/src/cpython/methodobject.rs b/pyo3-ffi/src/cpython/methodobject.rs
index 7d9659785ba..97ad9ce35f0 100644
--- a/pyo3-ffi/src/cpython/methodobject.rs
+++ b/pyo3-ffi/src/cpython/methodobject.rs
@@ -1,8 +1,10 @@
 use crate::object::*;
+#[cfg(not(GraalPy))]
 use crate::{PyCFunctionObject, PyMethodDefPointer, METH_METHOD, METH_STATIC};
 use std::os::raw::c_int;
 use std::ptr::addr_of_mut;
 
+#[cfg(not(GraalPy))]
 pub struct PyCMethodObject {
     pub func: PyCFunctionObject,
     pub mm_class: *mut PyTypeObject,
@@ -23,6 +25,7 @@ pub unsafe fn PyCMethod_Check(op: *mut PyObject) -> c_int {
     PyObject_TypeCheck(op, addr_of_mut!(PyCMethod_Type))
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyCFunction_GET_FUNCTION(func: *mut PyObject) -> PyMethodDefPointer {
     debug_assert_eq!(PyCMethod_Check(func), 1);
@@ -31,6 +34,7 @@ pub unsafe fn PyCFunction_GET_FUNCTION(func: *mut PyObject) -> PyMethodDefPointe
     (*(*func).m_ml).ml_meth
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyCFunction_GET_SELF(func: *mut PyObject) -> *mut PyObject {
     debug_assert_eq!(PyCMethod_Check(func), 1);
@@ -43,6 +47,7 @@ pub unsafe fn PyCFunction_GET_SELF(func: *mut PyObject) -> *mut PyObject {
     }
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyCFunction_GET_FLAGS(func: *mut PyObject) -> c_int {
     debug_assert_eq!(PyCMethod_Check(func), 1);
@@ -51,6 +56,7 @@ pub unsafe fn PyCFunction_GET_FLAGS(func: *mut PyObject) -> c_int {
     (*(*func).m_ml).ml_flags
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyCFunction_GET_CLASS(func: *mut PyObject) -> *mut PyTypeObject {
     debug_assert_eq!(PyCMethod_Check(func), 1);
diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs
index 738ba37652e..1ab0e3c893f 100644
--- a/pyo3-ffi/src/cpython/mod.rs
+++ b/pyo3-ffi/src/cpython/mod.rs
@@ -68,5 +68,5 @@ pub use self::pystate::*;
 pub use self::pythonrun::*;
 pub use self::tupleobject::*;
 pub use self::unicodeobject::*;
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub use self::weakrefobject::*;
diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs
index d0c1634082b..0f1778f6a3d 100644
--- a/pyo3-ffi/src/cpython/object.rs
+++ b/pyo3-ffi/src/cpython/object.rs
@@ -217,6 +217,8 @@ pub struct PyTypeObject {
     pub ob_size: Py_ssize_t,
     #[cfg(not(all(PyPy, not(Py_3_9))))]
     pub ob_base: object::PyVarObject,
+    #[cfg(GraalPy)]
+    pub ob_size: Py_ssize_t,
     pub tp_name: *const c_char,
     pub tp_basicsize: Py_ssize_t,
     pub tp_itemsize: Py_ssize_t,
diff --git a/pyo3-ffi/src/cpython/objimpl.rs b/pyo3-ffi/src/cpython/objimpl.rs
index 36a4380d122..3e0270ddc8f 100644
--- a/pyo3-ffi/src/cpython/objimpl.rs
+++ b/pyo3-ffi/src/cpython/objimpl.rs
@@ -1,7 +1,7 @@
 use libc::size_t;
 use std::os::raw::c_int;
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 use std::os::raw::c_void;
 
 use crate::object::*;
@@ -14,7 +14,7 @@ extern "C" {
     pub fn _Py_GetAllocatedBlocks() -> crate::Py_ssize_t;
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct PyObjectArenaAllocator {
@@ -23,7 +23,7 @@ pub struct PyObjectArenaAllocator {
     pub free: Option<extern "C" fn(ctx: *mut c_void, ptr: *mut c_void, size: size_t)>,
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 impl Default for PyObjectArenaAllocator {
     #[inline]
     fn default() -> Self {
@@ -32,9 +32,9 @@ impl Default for PyObjectArenaAllocator {
 }
 
 extern "C" {
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyObject_GetArenaAllocator(allocator: *mut PyObjectArenaAllocator);
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyObject_SetArenaAllocator(allocator: *mut PyObjectArenaAllocator);
 
     #[cfg(Py_3_9)]
diff --git a/pyo3-ffi/src/cpython/pyerrors.rs b/pyo3-ffi/src/cpython/pyerrors.rs
index fe7b4d4b045..6d17ebc8124 100644
--- a/pyo3-ffi/src/cpython/pyerrors.rs
+++ b/pyo3-ffi/src/cpython/pyerrors.rs
@@ -1,28 +1,28 @@
 use crate::PyObject;
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 use crate::Py_ssize_t;
 
 #[repr(C)]
 #[derive(Debug)]
 pub struct PyBaseExceptionObject {
     pub ob_base: PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub dict: *mut PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub args: *mut PyObject,
-    #[cfg(all(Py_3_11, not(PyPy)))]
+    #[cfg(all(Py_3_11, not(any(PyPy, GraalPy))))]
     pub notes: *mut PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub traceback: *mut PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub context: *mut PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub cause: *mut PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub suppress_context: char,
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Debug)]
 pub struct PySyntaxErrorObject {
@@ -48,7 +48,7 @@ pub struct PySyntaxErrorObject {
     pub print_file_and_line: *mut PyObject,
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Debug)]
 pub struct PyImportErrorObject {
@@ -69,7 +69,7 @@ pub struct PyImportErrorObject {
     pub name_from: *mut PyObject,
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Debug)]
 pub struct PyUnicodeErrorObject {
@@ -90,7 +90,7 @@ pub struct PyUnicodeErrorObject {
     pub reason: *mut PyObject,
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Debug)]
 pub struct PySystemExitObject {
@@ -107,7 +107,7 @@ pub struct PySystemExitObject {
     pub code: *mut PyObject,
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Debug)]
 pub struct PyOSErrorObject {
@@ -134,26 +134,26 @@ pub struct PyOSErrorObject {
 #[derive(Debug)]
 pub struct PyStopIterationObject {
     pub ob_base: PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub dict: *mut PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub args: *mut PyObject,
-    #[cfg(all(Py_3_11, not(PyPy)))]
+    #[cfg(all(Py_3_11, not(any(PyPy, GraalPy))))]
     pub notes: *mut PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub traceback: *mut PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub context: *mut PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub cause: *mut PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub suppress_context: char,
 
     pub value: *mut PyObject,
 }
 
 extern "C" {
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn _PyErr_ChainExceptions(typ: *mut PyObject, val: *mut PyObject, tb: *mut PyObject);
 }
 
diff --git a/pyo3-ffi/src/cpython/pymem.rs b/pyo3-ffi/src/cpython/pymem.rs
index 2dfb3f3bcfa..c400fa30b05 100644
--- a/pyo3-ffi/src/cpython/pymem.rs
+++ b/pyo3-ffi/src/cpython/pymem.rs
@@ -26,7 +26,7 @@ pub enum PyMemAllocatorDomain {
 }
 
 // skipped PyMemAllocatorName
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct PyMemAllocatorEx {
@@ -40,10 +40,10 @@ pub struct PyMemAllocatorEx {
 }
 
 extern "C" {
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyMem_GetAllocator(domain: PyMemAllocatorDomain, allocator: *mut PyMemAllocatorEx);
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyMem_SetAllocator(domain: PyMemAllocatorDomain, allocator: *mut PyMemAllocatorEx);
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyMem_SetupDebugHooks();
 }
diff --git a/pyo3-ffi/src/cpython/pythonrun.rs b/pyo3-ffi/src/cpython/pythonrun.rs
index a92528b7fdc..94863166e11 100644
--- a/pyo3-ffi/src/cpython/pythonrun.rs
+++ b/pyo3-ffi/src/cpython/pythonrun.rs
@@ -1,8 +1,8 @@
 use crate::object::*;
-#[cfg(not(any(PyPy, Py_LIMITED_API, Py_3_10)))]
+#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API, Py_3_10)))]
 use crate::pyarena::PyArena;
 use crate::PyCompilerFlags;
-#[cfg(not(any(PyPy, Py_3_10)))]
+#[cfg(not(any(PyPy, GraalPy, Py_3_10)))]
 use crate::{_mod, _node};
 use libc::FILE;
 use std::os::raw::{c_char, c_int};
@@ -54,7 +54,7 @@ extern "C" {
         flags: *mut PyCompilerFlags,
     ) -> c_int;
 
-    #[cfg(not(any(PyPy, Py_3_10)))]
+    #[cfg(not(any(PyPy, GraalPy, Py_3_10)))]
     pub fn PyParser_ASTFromString(
         s: *const c_char,
         filename: *const c_char,
@@ -62,7 +62,7 @@ extern "C" {
         flags: *mut PyCompilerFlags,
         arena: *mut PyArena,
     ) -> *mut _mod;
-    #[cfg(not(any(PyPy, Py_3_10)))]
+    #[cfg(not(any(PyPy, GraalPy, Py_3_10)))]
     pub fn PyParser_ASTFromStringObject(
         s: *const c_char,
         filename: *mut PyObject,
@@ -70,7 +70,7 @@ extern "C" {
         flags: *mut PyCompilerFlags,
         arena: *mut PyArena,
     ) -> *mut _mod;
-    #[cfg(not(any(PyPy, Py_3_10)))]
+    #[cfg(not(any(PyPy, GraalPy, Py_3_10)))]
     pub fn PyParser_ASTFromFile(
         fp: *mut FILE,
         filename: *const c_char,
@@ -82,7 +82,7 @@ extern "C" {
         errcode: *mut c_int,
         arena: *mut PyArena,
     ) -> *mut _mod;
-    #[cfg(not(any(PyPy, Py_3_10)))]
+    #[cfg(not(any(PyPy, GraalPy, Py_3_10)))]
     pub fn PyParser_ASTFromFileObject(
         fp: *mut FILE,
         filename: *mut PyObject,
@@ -105,7 +105,7 @@ extern "C" {
         arg4: *mut PyObject,
         arg5: *mut PyCompilerFlags,
     ) -> *mut PyObject;
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyRun_FileExFlags(
         fp: *mut FILE,
         filename: *const c_char,
@@ -116,7 +116,7 @@ extern "C" {
         flags: *mut PyCompilerFlags,
     ) -> *mut PyObject;
 
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn Py_CompileStringExFlags(
         str: *const c_char,
         filename: *const c_char,
@@ -135,6 +135,7 @@ extern "C" {
 }
 
 #[inline]
+#[cfg(not(GraalPy))]
 pub unsafe fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int) -> *mut PyObject {
     #[cfg(not(PyPy))]
     return Py_CompileStringExFlags(string, p, s, std::ptr::null_mut(), -1);
@@ -144,7 +145,7 @@ pub unsafe fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub unsafe fn Py_CompileStringFlags(
     string: *const c_char,
     p: *const c_char,
@@ -164,11 +165,11 @@ extern "C" {
         g: *mut PyObject,
         l: *mut PyObject,
     ) -> *mut PyObject;
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyRun_AnyFile(fp: *mut FILE, name: *const c_char) -> c_int;
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyRun_AnyFileEx(fp: *mut FILE, name: *const c_char, closeit: c_int) -> c_int;
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyRun_AnyFileFlags(
         arg1: *mut FILE,
         arg2: *const c_char,
@@ -176,13 +177,13 @@ extern "C" {
     ) -> c_int;
     #[cfg_attr(PyPy, link_name = "PyPyRun_SimpleString")]
     pub fn PyRun_SimpleString(s: *const c_char) -> c_int;
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyRun_SimpleFile(f: *mut FILE, p: *const c_char) -> c_int;
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyRun_SimpleFileEx(f: *mut FILE, p: *const c_char, c: c_int) -> c_int;
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyRun_InteractiveOne(f: *mut FILE, p: *const c_char) -> c_int;
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyRun_InteractiveLoop(f: *mut FILE, p: *const c_char) -> c_int;
     #[cfg_attr(PyPy, link_name = "PyPyRun_File")]
     pub fn PyRun_File(
@@ -192,7 +193,7 @@ extern "C" {
         g: *mut PyObject,
         l: *mut PyObject,
     ) -> *mut PyObject;
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyRun_FileEx(
         fp: *mut FILE,
         p: *const c_char,
@@ -201,7 +202,7 @@ extern "C" {
         l: *mut PyObject,
         c: c_int,
     ) -> *mut PyObject;
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn PyRun_FileFlags(
         fp: *mut FILE,
         p: *const c_char,
@@ -218,14 +219,14 @@ extern "C" {
 // skipped macro PyRun_AnyFileFlags
 
 extern "C" {
-    #[cfg(not(any(PyPy, Py_3_10)))]
+    #[cfg(not(any(PyPy, GraalPy, Py_3_10)))]
     #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))]
     pub fn PyParser_SimpleParseStringFlags(
         arg1: *const c_char,
         arg2: c_int,
         arg3: c_int,
     ) -> *mut _node;
-    #[cfg(not(any(PyPy, Py_3_10)))]
+    #[cfg(not(any(PyPy, GraalPy, Py_3_10)))]
     #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))]
     pub fn PyParser_SimpleParseStringFlagsFilename(
         arg1: *const c_char,
@@ -233,7 +234,7 @@ extern "C" {
         arg3: c_int,
         arg4: c_int,
     ) -> *mut _node;
-    #[cfg(not(any(PyPy, Py_3_10)))]
+    #[cfg(not(any(PyPy, GraalPy, Py_3_10)))]
     #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))]
     pub fn PyParser_SimpleParseFileFlags(
         arg1: *mut FILE,
diff --git a/pyo3-ffi/src/cpython/tupleobject.rs b/pyo3-ffi/src/cpython/tupleobject.rs
index 24dde268526..4ed8520daf3 100644
--- a/pyo3-ffi/src/cpython/tupleobject.rs
+++ b/pyo3-ffi/src/cpython/tupleobject.rs
@@ -5,6 +5,7 @@ use crate::pyport::Py_ssize_t;
 #[repr(C)]
 pub struct PyTupleObject {
     pub ob_base: PyVarObject,
+    #[cfg(not(GraalPy))]
     pub ob_item: [*mut PyObject; 1],
 }
 
@@ -22,14 +23,14 @@ pub unsafe fn PyTuple_GET_SIZE(op: *mut PyObject) -> Py_ssize_t {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub unsafe fn PyTuple_GET_ITEM(op: *mut PyObject, i: Py_ssize_t) -> *mut PyObject {
     *(*(op as *mut PyTupleObject)).ob_item.as_ptr().offset(i)
 }
 
 /// Macro, *only* to be used to fill in brand new tuples
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub unsafe fn PyTuple_SET_ITEM(op: *mut PyObject, i: Py_ssize_t, v: *mut PyObject) {
     *(*(op as *mut PyTupleObject)).ob_item.as_mut_ptr().offset(i) = v;
 }
diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs
index f618ecf0a84..9ab523a2d7f 100644
--- a/pyo3-ffi/src/cpython/unicodeobject.rs
+++ b/pyo3-ffi/src/cpython/unicodeobject.rs
@@ -1,7 +1,7 @@
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 use crate::Py_hash_t;
 use crate::{PyObject, Py_UCS1, Py_UCS2, Py_UCS4, Py_UNICODE, Py_ssize_t};
-#[cfg(not(Py_3_12))]
+#[cfg(not(any(Py_3_12, GraalPy)))]
 use libc::wchar_t;
 use std::os::raw::{c_char, c_int, c_uint, c_void};
 
@@ -44,6 +44,7 @@ impl<Storage> BitfieldUnit<Storage> {
     }
 }
 
+#[cfg(not(GraalPy))]
 impl<Storage> BitfieldUnit<Storage>
 where
     Storage: AsRef<[u8]> + AsMut<[u8]>,
@@ -117,23 +118,31 @@ where
     }
 }
 
+#[cfg(not(GraalPy))]
 const STATE_INTERNED_INDEX: usize = 0;
+#[cfg(not(GraalPy))]
 const STATE_INTERNED_WIDTH: u8 = 2;
 
+#[cfg(not(GraalPy))]
 const STATE_KIND_INDEX: usize = STATE_INTERNED_WIDTH as usize;
+#[cfg(not(GraalPy))]
 const STATE_KIND_WIDTH: u8 = 3;
 
+#[cfg(not(GraalPy))]
 const STATE_COMPACT_INDEX: usize = (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH) as usize;
+#[cfg(not(GraalPy))]
 const STATE_COMPACT_WIDTH: u8 = 1;
 
+#[cfg(not(GraalPy))]
 const STATE_ASCII_INDEX: usize =
     (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH + STATE_COMPACT_WIDTH) as usize;
+#[cfg(not(GraalPy))]
 const STATE_ASCII_WIDTH: u8 = 1;
 
-#[cfg(not(Py_3_12))]
+#[cfg(not(any(Py_3_12, GraalPy)))]
 const STATE_READY_INDEX: usize =
     (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH + STATE_COMPACT_WIDTH + STATE_ASCII_WIDTH) as usize;
-#[cfg(not(Py_3_12))]
+#[cfg(not(any(Py_3_12, GraalPy)))]
 const STATE_READY_WIDTH: u8 = 1;
 
 // generated by bindgen v0.63.0 (with small adaptations)
@@ -153,6 +162,7 @@ struct PyASCIIObjectState {
 }
 
 // c_uint and u32 are not necessarily the same type on all targets / architectures
+#[cfg(not(GraalPy))]
 #[allow(clippy::useless_transmute)]
 impl PyASCIIObjectState {
     #[inline]
@@ -241,8 +251,9 @@ impl From<PyASCIIObjectState> for u32 {
 #[repr(C)]
 pub struct PyASCIIObject {
     pub ob_base: PyObject,
+    #[cfg(not(GraalPy))]
     pub length: Py_ssize_t,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub hash: Py_hash_t,
     /// A bit field with various properties.
     ///
@@ -255,12 +266,14 @@ pub struct PyASCIIObject {
     /// unsigned int ascii:1;
     /// unsigned int ready:1;
     /// unsigned int :24;
+    #[cfg(not(GraalPy))]
     pub state: u32,
-    #[cfg(not(Py_3_12))]
+    #[cfg(not(any(Py_3_12, GraalPy)))]
     pub wstr: *mut wchar_t,
 }
 
 /// Interacting with the bitfield is not actually well-defined, so we mark these APIs unsafe.
+#[cfg(not(GraalPy))]
 impl PyASCIIObject {
     #[cfg_attr(not(Py_3_12), allow(rustdoc::broken_intra_doc_links))] // SSTATE_INTERNED_IMMORTAL_STATIC requires 3.12
     /// Get the `interned` field of the [`PyASCIIObject`] state bitfield.
@@ -367,9 +380,11 @@ impl PyASCIIObject {
 #[repr(C)]
 pub struct PyCompactUnicodeObject {
     pub _base: PyASCIIObject,
+    #[cfg(not(GraalPy))]
     pub utf8_length: Py_ssize_t,
+    #[cfg(not(GraalPy))]
     pub utf8: *mut c_char,
-    #[cfg(not(Py_3_12))]
+    #[cfg(not(any(Py_3_12, GraalPy)))]
     pub wstr_length: Py_ssize_t,
 }
 
@@ -384,11 +399,12 @@ pub union PyUnicodeObjectData {
 #[repr(C)]
 pub struct PyUnicodeObject {
     pub _base: PyCompactUnicodeObject,
+    #[cfg(not(GraalPy))]
     pub data: PyUnicodeObjectData,
 }
 
 extern "C" {
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn _PyUnicode_CheckConsistency(op: *mut PyObject, check_content: c_int) -> c_int;
 }
 
@@ -403,6 +419,7 @@ pub const SSTATE_INTERNED_IMMORTAL: c_uint = 2;
 #[cfg(Py_3_12)]
 pub const SSTATE_INTERNED_IMMORTAL_STATIC: c_uint = 3;
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_uint {
     debug_assert!(crate::PyUnicode_Check(op) != 0);
@@ -412,11 +429,13 @@ pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_uint {
     (*(op as *mut PyASCIIObject)).ascii()
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyUnicode_IS_COMPACT(op: *mut PyObject) -> c_uint {
     (*(op as *mut PyASCIIObject)).compact()
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyUnicode_IS_COMPACT_ASCII(op: *mut PyObject) -> c_uint {
     ((*(op as *mut PyASCIIObject)).ascii() != 0 && PyUnicode_IS_COMPACT(op) != 0).into()
@@ -430,21 +449,25 @@ pub const PyUnicode_1BYTE_KIND: c_uint = 1;
 pub const PyUnicode_2BYTE_KIND: c_uint = 2;
 pub const PyUnicode_4BYTE_KIND: c_uint = 4;
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyUnicode_1BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS1 {
     PyUnicode_DATA(op) as *mut Py_UCS1
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyUnicode_2BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS2 {
     PyUnicode_DATA(op) as *mut Py_UCS2
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyUnicode_4BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS4 {
     PyUnicode_DATA(op) as *mut Py_UCS4
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_uint {
     debug_assert!(crate::PyUnicode_Check(op) != 0);
@@ -454,6 +477,7 @@ pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_uint {
     (*(op as *mut PyASCIIObject)).kind()
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn _PyUnicode_COMPACT_DATA(op: *mut PyObject) -> *mut c_void {
     if PyUnicode_IS_ASCII(op) != 0 {
@@ -463,6 +487,7 @@ pub unsafe fn _PyUnicode_COMPACT_DATA(op: *mut PyObject) -> *mut c_void {
     }
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn _PyUnicode_NONCOMPACT_DATA(op: *mut PyObject) -> *mut c_void {
     debug_assert!(!(*(op as *mut PyUnicodeObject)).data.any.is_null());
@@ -470,6 +495,7 @@ pub unsafe fn _PyUnicode_NONCOMPACT_DATA(op: *mut PyObject) -> *mut c_void {
     (*(op as *mut PyUnicodeObject)).data.any
 }
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyUnicode_DATA(op: *mut PyObject) -> *mut c_void {
     debug_assert!(crate::PyUnicode_Check(op) != 0);
@@ -485,6 +511,7 @@ pub unsafe fn PyUnicode_DATA(op: *mut PyObject) -> *mut c_void {
 // skipped PyUnicode_READ
 // skipped PyUnicode_READ_CHAR
 
+#[cfg(not(GraalPy))]
 #[inline]
 pub unsafe fn PyUnicode_GET_LENGTH(op: *mut PyObject) -> Py_ssize_t {
     debug_assert!(crate::PyUnicode_Check(op) != 0);
@@ -494,26 +521,26 @@ pub unsafe fn PyUnicode_GET_LENGTH(op: *mut PyObject) -> Py_ssize_t {
     (*(op as *mut PyASCIIObject)).length
 }
 
-#[cfg(Py_3_12)]
+#[cfg(any(Py_3_12, GraalPy))]
 #[inline]
 pub unsafe fn PyUnicode_IS_READY(_op: *mut PyObject) -> c_uint {
     // kept in CPython for backwards compatibility
     1
 }
 
-#[cfg(not(Py_3_12))]
+#[cfg(not(any(GraalPy, Py_3_12)))]
 #[inline]
 pub unsafe fn PyUnicode_IS_READY(op: *mut PyObject) -> c_uint {
     (*(op as *mut PyASCIIObject)).ready()
 }
 
-#[cfg(Py_3_12)]
+#[cfg(any(Py_3_12, GraalPy))]
 #[inline]
 pub unsafe fn PyUnicode_READY(_op: *mut PyObject) -> c_int {
     0
 }
 
-#[cfg(not(Py_3_12))]
+#[cfg(not(any(Py_3_12, GraalPy)))]
 #[inline]
 pub unsafe fn PyUnicode_READY(op: *mut PyObject) -> c_int {
     debug_assert!(crate::PyUnicode_Check(op) != 0);
diff --git a/pyo3-ffi/src/cpython/weakrefobject.rs b/pyo3-ffi/src/cpython/weakrefobject.rs
index 5a5f85c5f0c..3a232c7ed38 100644
--- a/pyo3-ffi/src/cpython/weakrefobject.rs
+++ b/pyo3-ffi/src/cpython/weakrefobject.rs
@@ -1,4 +1,4 @@
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub struct _PyWeakReference {
     pub ob_base: crate::PyObject,
     pub wr_object: *mut crate::PyObject,
diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs
index 7e5a250990f..a20b76aa91d 100644
--- a/pyo3-ffi/src/datetime.rs
+++ b/pyo3-ffi/src/datetime.rs
@@ -9,17 +9,16 @@
 //! Support for `PyDateTime_CAPI` is limited as of PyPy 7.0.0.
 //! `DateTime_FromTimestamp` and `Date_FromTimestamp` are currently not supported.
 
+#[cfg(GraalPy)]
+use crate::{PyLong_AsLong, PyLong_Check, PyObject_GetAttrString, Py_DecRef};
 use crate::{PyObject, PyObject_TypeCheck, PyTypeObject, Py_TYPE};
 use std::cell::UnsafeCell;
 use std::os::raw::{c_char, c_int};
 use std::ptr;
 #[cfg(not(PyPy))]
-use {
-    crate::{PyCapsule_Import, Py_hash_t},
-    std::ffi::CString,
-    std::os::raw::c_uchar,
-};
-
+use {crate::PyCapsule_Import, std::ffi::CString};
+#[cfg(not(any(PyPy, GraalPy)))]
+use {crate::Py_hash_t, std::os::raw::c_uchar};
 // Type struct wrappers
 const _PyDateTime_DATE_DATASIZE: usize = 4;
 const _PyDateTime_TIME_DATASIZE: usize = 6;
@@ -30,26 +29,27 @@ const _PyDateTime_DATETIME_DATASIZE: usize = 10;
 /// Structure representing a `datetime.timedelta`.
 pub struct PyDateTime_Delta {
     pub ob_base: PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub hashcode: Py_hash_t,
+    #[cfg(not(GraalPy))]
     pub days: c_int,
+    #[cfg(not(GraalPy))]
     pub seconds: c_int,
+    #[cfg(not(GraalPy))]
     pub microseconds: c_int,
 }
 
 // skipped non-limited PyDateTime_TZInfo
 // skipped non-limited _PyDateTime_BaseTZInfo
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Debug, Copy, Clone)]
 /// Structure representing a `datetime.time` without a `tzinfo` member.
 pub struct _PyDateTime_BaseTime {
     pub ob_base: PyObject,
-    #[cfg(not(PyPy))]
     pub hashcode: Py_hash_t,
     pub hastzinfo: c_char,
-    #[cfg(not(PyPy))]
     pub data: [c_uchar; _PyDateTime_TIME_DATASIZE],
 }
 
@@ -58,17 +58,19 @@ pub struct _PyDateTime_BaseTime {
 /// Structure representing a `datetime.time`.
 pub struct PyDateTime_Time {
     pub ob_base: PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub hashcode: Py_hash_t,
+    #[cfg(not(GraalPy))]
     pub hastzinfo: c_char,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub data: [c_uchar; _PyDateTime_TIME_DATASIZE],
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fold: c_uchar,
     /// # Safety
     ///
     /// Care should be taken when reading this field. If the time does not have a
     /// tzinfo then CPython may allocate as a `_PyDateTime_BaseTime` without this field.
+    #[cfg(not(GraalPy))]
     pub tzinfo: *mut PyObject,
 }
 
@@ -77,24 +79,22 @@ pub struct PyDateTime_Time {
 /// Structure representing a `datetime.date`
 pub struct PyDateTime_Date {
     pub ob_base: PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub hashcode: Py_hash_t,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub hastzinfo: c_char,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub data: [c_uchar; _PyDateTime_DATE_DATASIZE],
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Debug, Copy, Clone)]
 /// Structure representing a `datetime.datetime` without a `tzinfo` member.
 pub struct _PyDateTime_BaseDateTime {
     pub ob_base: PyObject,
-    #[cfg(not(PyPy))]
     pub hashcode: Py_hash_t,
     pub hastzinfo: c_char,
-    #[cfg(not(PyPy))]
     pub data: [c_uchar; _PyDateTime_DATETIME_DATASIZE],
 }
 
@@ -103,17 +103,19 @@ pub struct _PyDateTime_BaseDateTime {
 /// Structure representing a `datetime.datetime`.
 pub struct PyDateTime_DateTime {
     pub ob_base: PyObject,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub hashcode: Py_hash_t,
+    #[cfg(not(GraalPy))]
     pub hastzinfo: c_char,
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub data: [c_uchar; _PyDateTime_DATETIME_DATASIZE],
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fold: c_uchar,
     /// # Safety
     ///
     /// Care should be taken when reading this field. If the time does not have a
     /// tzinfo then CPython may allocate as a `_PyDateTime_BaseDateTime` without this field.
+    #[cfg(not(GraalPy))]
     pub tzinfo: *mut PyObject,
 }
 
@@ -121,7 +123,7 @@ pub struct PyDateTime_DateTime {
 
 // Accessor functions for PyDateTime_Date and PyDateTime_DateTime
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the year component of a `PyDateTime_Date` or `PyDateTime_DateTime`.
 /// Returns a signed integer greater than 0.
 pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int {
@@ -131,7 +133,7 @@ pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the month component of a `PyDateTime_Date` or `PyDateTime_DateTime`.
 /// Returns a signed integer in the range `[1, 12]`.
 pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int {
@@ -140,7 +142,7 @@ pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the day component of a `PyDateTime_Date` or `PyDateTime_DateTime`.
 /// Returns a signed integer in the interval `[1, 31]`.
 pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int {
@@ -149,28 +151,28 @@ pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int {
 }
 
 // Accessor macros for times
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 macro_rules! _PyDateTime_GET_HOUR {
     ($o: expr, $offset:expr) => {
         c_int::from((*$o).data[$offset + 0])
     };
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 macro_rules! _PyDateTime_GET_MINUTE {
     ($o: expr, $offset:expr) => {
         c_int::from((*$o).data[$offset + 1])
     };
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 macro_rules! _PyDateTime_GET_SECOND {
     ($o: expr, $offset:expr) => {
         c_int::from((*$o).data[$offset + 2])
     };
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 macro_rules! _PyDateTime_GET_MICROSECOND {
     ($o: expr, $offset:expr) => {
         (c_int::from((*$o).data[$offset + 3]) << 16)
@@ -179,14 +181,14 @@ macro_rules! _PyDateTime_GET_MICROSECOND {
     };
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 macro_rules! _PyDateTime_GET_FOLD {
     ($o: expr) => {
         (*$o).fold
     };
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 macro_rules! _PyDateTime_GET_TZINFO {
     ($o: expr) => {
         if (*$o).hastzinfo != 0 {
@@ -199,7 +201,7 @@ macro_rules! _PyDateTime_GET_TZINFO {
 
 // Accessor functions for DateTime
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the hour component of a `PyDateTime_DateTime`.
 /// Returns a signed integer in the interval `[0, 23]`
 pub unsafe fn PyDateTime_DATE_GET_HOUR(o: *mut PyObject) -> c_int {
@@ -207,7 +209,7 @@ pub unsafe fn PyDateTime_DATE_GET_HOUR(o: *mut PyObject) -> c_int {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the minute component of a `PyDateTime_DateTime`.
 /// Returns a signed integer in the interval `[0, 59]`
 pub unsafe fn PyDateTime_DATE_GET_MINUTE(o: *mut PyObject) -> c_int {
@@ -215,7 +217,7 @@ pub unsafe fn PyDateTime_DATE_GET_MINUTE(o: *mut PyObject) -> c_int {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the second component of a `PyDateTime_DateTime`.
 /// Returns a signed integer in the interval `[0, 59]`
 pub unsafe fn PyDateTime_DATE_GET_SECOND(o: *mut PyObject) -> c_int {
@@ -223,7 +225,7 @@ pub unsafe fn PyDateTime_DATE_GET_SECOND(o: *mut PyObject) -> c_int {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the microsecond component of a `PyDateTime_DateTime`.
 /// Returns a signed integer in the interval `[0, 999999]`
 pub unsafe fn PyDateTime_DATE_GET_MICROSECOND(o: *mut PyObject) -> c_int {
@@ -231,7 +233,7 @@ pub unsafe fn PyDateTime_DATE_GET_MICROSECOND(o: *mut PyObject) -> c_int {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the fold component of a `PyDateTime_DateTime`.
 /// Returns a signed integer in the interval `[0, 1]`
 pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_uchar {
@@ -239,7 +241,7 @@ pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_uchar {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the tzinfo component of a `PyDateTime_DateTime`.
 /// Returns a pointer to a `PyObject` that should be either NULL or an instance
 /// of a `datetime.tzinfo` subclass.
@@ -249,7 +251,7 @@ pub unsafe fn PyDateTime_DATE_GET_TZINFO(o: *mut PyObject) -> *mut PyObject {
 
 // Accessor functions for Time
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the hour component of a `PyDateTime_Time`.
 /// Returns a signed integer in the interval `[0, 23]`
 pub unsafe fn PyDateTime_TIME_GET_HOUR(o: *mut PyObject) -> c_int {
@@ -257,7 +259,7 @@ pub unsafe fn PyDateTime_TIME_GET_HOUR(o: *mut PyObject) -> c_int {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the minute component of a `PyDateTime_Time`.
 /// Returns a signed integer in the interval `[0, 59]`
 pub unsafe fn PyDateTime_TIME_GET_MINUTE(o: *mut PyObject) -> c_int {
@@ -265,7 +267,7 @@ pub unsafe fn PyDateTime_TIME_GET_MINUTE(o: *mut PyObject) -> c_int {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the second component of a `PyDateTime_DateTime`.
 /// Returns a signed integer in the interval `[0, 59]`
 pub unsafe fn PyDateTime_TIME_GET_SECOND(o: *mut PyObject) -> c_int {
@@ -273,14 +275,14 @@ pub unsafe fn PyDateTime_TIME_GET_SECOND(o: *mut PyObject) -> c_int {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the microsecond component of a `PyDateTime_DateTime`.
 /// Returns a signed integer in the interval `[0, 999999]`
 pub unsafe fn PyDateTime_TIME_GET_MICROSECOND(o: *mut PyObject) -> c_int {
     _PyDateTime_GET_MICROSECOND!((o as *mut PyDateTime_Time), 0)
 }
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[inline]
 /// Retrieve the fold component of a `PyDateTime_Time`.
 /// Returns a signed integer in the interval `[0, 1]`
@@ -289,7 +291,7 @@ pub unsafe fn PyDateTime_TIME_GET_FOLD(o: *mut PyObject) -> c_uchar {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the tzinfo component of a `PyDateTime_Time`.
 /// Returns a pointer to a `PyObject` that should be either NULL or an instance
 /// of a `datetime.tzinfo` subclass.
@@ -298,7 +300,7 @@ pub unsafe fn PyDateTime_TIME_GET_TZINFO(o: *mut PyObject) -> *mut PyObject {
 }
 
 // Accessor functions
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 macro_rules! _access_field {
     ($obj:expr, $type: ident, $field:ident) => {
         (*($obj as *mut $type)).$field
@@ -306,7 +308,7 @@ macro_rules! _access_field {
 }
 
 // Accessor functions for PyDateTime_Delta
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 macro_rules! _access_delta_field {
     ($obj:expr, $field:ident) => {
         _access_field!($obj, PyDateTime_Delta, $field)
@@ -314,7 +316,7 @@ macro_rules! _access_delta_field {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the days component of a `PyDateTime_Delta`.
 ///
 /// Returns a signed integer in the interval [-999999999, 999999999].
@@ -326,7 +328,7 @@ pub unsafe fn PyDateTime_DELTA_GET_DAYS(o: *mut PyObject) -> c_int {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the seconds component of a `PyDateTime_Delta`.
 ///
 /// Returns a signed integer in the interval [0, 86399].
@@ -338,7 +340,7 @@ pub unsafe fn PyDateTime_DELTA_GET_SECONDS(o: *mut PyObject) -> c_int {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 /// Retrieve the seconds component of a `PyDateTime_Delta`.
 ///
 /// Returns a signed integer in the interval [0, 999999].
@@ -349,6 +351,132 @@ pub unsafe fn PyDateTime_DELTA_GET_MICROSECONDS(o: *mut PyObject) -> c_int {
     _access_delta_field!(o, microseconds)
 }
 
+// Accessor functions for GraalPy. The macros on GraalPy work differently,
+// but copying them seems suboptimal
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn _get_attr(obj: *mut PyObject, field: &str) -> c_int {
+    let result = PyObject_GetAttrString(obj, field.as_ptr() as *const c_char);
+    Py_DecRef(result); // the original macros are borrowing
+    if PyLong_Check(result) == 1 {
+        PyLong_AsLong(result) as c_int
+    } else {
+        0
+    }
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int {
+    _get_attr(o, "year\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int {
+    _get_attr(o, "month\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int {
+    _get_attr(o, "day\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_DATE_GET_HOUR(o: *mut PyObject) -> c_int {
+    _get_attr(o, "hour\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_DATE_GET_MINUTE(o: *mut PyObject) -> c_int {
+    _get_attr(o, "minute\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_DATE_GET_SECOND(o: *mut PyObject) -> c_int {
+    _get_attr(o, "second\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_DATE_GET_MICROSECOND(o: *mut PyObject) -> c_int {
+    _get_attr(o, "microsecond\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_int {
+    _get_attr(o, "fold\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_DATE_GET_TZINFO(o: *mut PyObject) -> *mut PyObject {
+    let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr() as *const c_char);
+    Py_DecRef(res); // the original macros are borrowing
+    res
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_TIME_GET_HOUR(o: *mut PyObject) -> c_int {
+    _get_attr(o, "hour\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_TIME_GET_MINUTE(o: *mut PyObject) -> c_int {
+    _get_attr(o, "minute\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_TIME_GET_SECOND(o: *mut PyObject) -> c_int {
+    _get_attr(o, "second\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_TIME_GET_MICROSECOND(o: *mut PyObject) -> c_int {
+    _get_attr(o, "microsecond\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_TIME_GET_FOLD(o: *mut PyObject) -> c_int {
+    _get_attr(o, "fold\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_TIME_GET_TZINFO(o: *mut PyObject) -> *mut PyObject {
+    let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr() as *const c_char);
+    Py_DecRef(res); // the original macros are borrowing
+    res
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_DELTA_GET_DAYS(o: *mut PyObject) -> c_int {
+    _get_attr(o, "days\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_DELTA_GET_SECONDS(o: *mut PyObject) -> c_int {
+    _get_attr(o, "seconds\0")
+}
+
+#[inline]
+#[cfg(GraalPy)]
+pub unsafe fn PyDateTime_DELTA_GET_MICROSECONDS(o: *mut PyObject) -> c_int {
+    _get_attr(o, "microseconds\0")
+}
+
 #[cfg(PyPy)]
 extern "C" {
     // skipped _PyDateTime_HAS_TZINFO (not in PyPy)
diff --git a/pyo3-ffi/src/dictobject.rs b/pyo3-ffi/src/dictobject.rs
index 8d522df97e2..99fc56b246b 100644
--- a/pyo3-ffi/src/dictobject.rs
+++ b/pyo3-ffi/src/dictobject.rs
@@ -109,6 +109,6 @@ extern "C" {
     pub static mut PyDictRevIterItem_Type: PyTypeObject;
 }
 
-#[cfg(any(PyPy, Py_LIMITED_API))]
+#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))]
 // TODO: remove (see https://github.com/PyO3/pyo3/pull/1341#issuecomment-751515985)
 opaque_struct!(PyDictObject);
diff --git a/pyo3-ffi/src/listobject.rs b/pyo3-ffi/src/listobject.rs
index 32c6a2dc26a..a6f47eadf83 100644
--- a/pyo3-ffi/src/listobject.rs
+++ b/pyo3-ffi/src/listobject.rs
@@ -54,14 +54,16 @@ extern "C" {
     #[cfg_attr(PyPy, link_name = "PyPyList_AsTuple")]
     pub fn PyList_AsTuple(arg1: *mut PyObject) -> *mut PyObject;
 
-    // CPython macros exported as functions on PyPy
-    #[cfg(PyPy)]
+    // CPython macros exported as functions on PyPy or GraalPy
+    #[cfg(any(PyPy, GraalPy))]
     #[cfg_attr(PyPy, link_name = "PyPyList_GET_ITEM")]
+    #[cfg_attr(GraalPy, link_name = "PyList_GetItem")]
     pub fn PyList_GET_ITEM(arg1: *mut PyObject, arg2: Py_ssize_t) -> *mut PyObject;
     #[cfg(PyPy)]
     #[cfg_attr(PyPy, link_name = "PyPyList_GET_SIZE")]
     pub fn PyList_GET_SIZE(arg1: *mut PyObject) -> Py_ssize_t;
-    #[cfg(PyPy)]
+    #[cfg(any(PyPy, GraalPy))]
     #[cfg_attr(PyPy, link_name = "PyPyList_SET_ITEM")]
+    #[cfg_attr(GraalPy, link_name = "_PyList_SET_ITEM")]
     pub fn PyList_SET_ITEM(arg1: *mut PyObject, arg2: Py_ssize_t, arg3: *mut PyObject);
 }
diff --git a/pyo3-ffi/src/methodobject.rs b/pyo3-ffi/src/methodobject.rs
index e80d5668e78..3ed6b770e54 100644
--- a/pyo3-ffi/src/methodobject.rs
+++ b/pyo3-ffi/src/methodobject.rs
@@ -4,7 +4,7 @@ use crate::PyObject_TypeCheck;
 use std::os::raw::{c_char, c_int, c_void};
 use std::{mem, ptr};
 
-#[cfg(all(Py_3_9, not(Py_LIMITED_API)))]
+#[cfg(all(Py_3_9, not(Py_LIMITED_API), not(GraalPy)))]
 pub struct PyCFunctionObject {
     pub ob_base: PyObject,
     pub m_ml: *mut PyMethodDef,
diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs
index 0e0243cd764..b33ee558a37 100644
--- a/pyo3-ffi/src/object.rs
+++ b/pyo3-ffi/src/object.rs
@@ -79,6 +79,7 @@ pub struct PyObject {
 #[derive(Debug, Copy, Clone)]
 pub struct PyVarObject {
     pub ob_base: PyObject,
+    #[cfg(not(GraalPy))]
     pub ob_size: Py_ssize_t,
 }
 
@@ -98,12 +99,18 @@ pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t {
 #[inline]
 #[cfg(not(Py_3_12))]
 pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t {
-    (*ob).ob_refcnt
+    #[cfg(not(GraalPy))]
+    return (*ob).ob_refcnt;
+    #[cfg(GraalPy)]
+    return _Py_REFCNT(ob);
 }
 
 #[inline]
 pub unsafe fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject {
-    (*ob).ob_type
+    #[cfg(not(GraalPy))]
+    return (*ob).ob_type;
+    #[cfg(GraalPy)]
+    return _Py_TYPE(ob);
 }
 
 // PyLong_Type defined in longobject.rs
@@ -111,9 +118,14 @@ pub unsafe fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject {
 
 #[inline]
 pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t {
-    debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyLong_Type));
-    debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyBool_Type));
-    (*ob.cast::<PyVarObject>()).ob_size
+    #[cfg(not(GraalPy))]
+    {
+        debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyLong_Type));
+        debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyBool_Type));
+        (*ob.cast::<PyVarObject>()).ob_size
+    }
+    #[cfg(GraalPy)]
+    _Py_SIZE(ob)
 }
 
 #[inline]
@@ -464,8 +476,10 @@ extern "C" {
     pub fn _Py_Dealloc(arg1: *mut PyObject);
 
     #[cfg_attr(PyPy, link_name = "PyPy_IncRef")]
+    #[cfg_attr(GraalPy, link_name = "_Py_IncRef")]
     pub fn Py_IncRef(o: *mut PyObject);
     #[cfg_attr(PyPy, link_name = "PyPy_DecRef")]
+    #[cfg_attr(GraalPy, link_name = "_Py_DecRef")]
     pub fn Py_DecRef(o: *mut PyObject);
 
     #[cfg(Py_3_10)]
@@ -474,11 +488,21 @@ extern "C" {
     #[cfg(Py_3_10)]
     #[cfg_attr(PyPy, link_name = "_PyPy_DecRef")]
     pub fn _Py_DecRef(o: *mut PyObject);
+
+    #[cfg(GraalPy)]
+    pub fn _Py_REFCNT(arg1: *const PyObject) -> Py_ssize_t;
+
+    #[cfg(GraalPy)]
+    pub fn _Py_TYPE(arg1: *const PyObject) -> *mut PyTypeObject;
+
+    #[cfg(GraalPy)]
+    pub fn _Py_SIZE(arg1: *const PyObject) -> Py_ssize_t;
 }
 
 #[inline(always)]
 pub unsafe fn Py_INCREF(op: *mut PyObject) {
     #[cfg(any(
+        GraalPy,
         all(Py_LIMITED_API, Py_3_12),
         all(
             py_sys_config = "Py_REF_DEBUG",
@@ -499,6 +523,7 @@ pub unsafe fn Py_INCREF(op: *mut PyObject) {
         all(Py_LIMITED_API, not(Py_3_12)),
         all(
             not(Py_LIMITED_API),
+            not(GraalPy),
             any(
                 not(py_sys_config = "Py_REF_DEBUG"),
                 all(py_sys_config = "Py_REF_DEBUG", Py_3_12),
@@ -544,6 +569,7 @@ pub unsafe fn Py_INCREF(op: *mut PyObject) {
 )]
 pub unsafe fn Py_DECREF(op: *mut PyObject) {
     #[cfg(any(
+        GraalPy,
         all(Py_LIMITED_API, Py_3_12),
         all(
             py_sys_config = "Py_REF_DEBUG",
@@ -564,6 +590,7 @@ pub unsafe fn Py_DECREF(op: *mut PyObject) {
         all(Py_LIMITED_API, not(Py_3_12)),
         all(
             not(Py_LIMITED_API),
+            not(GraalPy),
             any(
                 not(py_sys_config = "Py_REF_DEBUG"),
                 all(py_sys_config = "Py_REF_DEBUG", Py_3_12),
@@ -669,13 +696,20 @@ pub unsafe fn Py_XNewRef(obj: *mut PyObject) -> *mut PyObject {
 
 #[cfg_attr(windows, link(name = "pythonXY"))]
 extern "C" {
+    #[cfg(not(GraalPy))]
     #[cfg_attr(PyPy, link_name = "_PyPy_NoneStruct")]
     static mut _Py_NoneStruct: PyObject;
+
+    #[cfg(GraalPy)]
+    static mut _Py_NoneStructReference: *mut PyObject;
 }
 
 #[inline]
 pub unsafe fn Py_None() -> *mut PyObject {
-    ptr::addr_of_mut!(_Py_NoneStruct)
+    #[cfg(not(GraalPy))]
+    return ptr::addr_of_mut!(_Py_NoneStruct);
+    #[cfg(GraalPy)]
+    return _Py_NoneStructReference;
 }
 
 #[inline]
@@ -687,13 +721,20 @@ pub unsafe fn Py_IsNone(x: *mut PyObject) -> c_int {
 
 #[cfg_attr(windows, link(name = "pythonXY"))]
 extern "C" {
+    #[cfg(not(GraalPy))]
     #[cfg_attr(PyPy, link_name = "_PyPy_NotImplementedStruct")]
     static mut _Py_NotImplementedStruct: PyObject;
+
+    #[cfg(GraalPy)]
+    static mut _Py_NotImplementedStructReference: *mut PyObject;
 }
 
 #[inline]
 pub unsafe fn Py_NotImplemented() -> *mut PyObject {
-    ptr::addr_of_mut!(_Py_NotImplementedStruct)
+    #[cfg(not(GraalPy))]
+    return ptr::addr_of_mut!(_Py_NotImplementedStruct);
+    #[cfg(GraalPy)]
+    return _Py_NotImplementedStructReference;
 }
 
 // skipped Py_RETURN_NOTIMPLEMENTED
diff --git a/pyo3-ffi/src/pyframe.rs b/pyo3-ffi/src/pyframe.rs
index 43a9d1f6777..4dd3d2b31a5 100644
--- a/pyo3-ffi/src/pyframe.rs
+++ b/pyo3-ffi/src/pyframe.rs
@@ -1,3 +1,4 @@
+#[cfg(not(GraalPy))]
 #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))]
 use crate::PyCodeObject;
 #[cfg(not(Py_LIMITED_API))]
@@ -9,6 +10,7 @@ opaque_struct!(PyFrameObject);
 
 extern "C" {
     pub fn PyFrame_GetLineNumber(f: *mut PyFrameObject) -> c_int;
+    #[cfg(not(GraalPy))]
     #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))]
     pub fn PyFrame_GetCode(f: *mut PyFrameObject) -> *mut PyCodeObject;
 }
diff --git a/pyo3-ffi/src/pyhash.rs b/pyo3-ffi/src/pyhash.rs
index f8072d85108..f42f9730f1b 100644
--- a/pyo3-ffi/src/pyhash.rs
+++ b/pyo3-ffi/src/pyhash.rs
@@ -1,6 +1,6 @@
-#[cfg(not(any(Py_LIMITED_API, PyPy)))]
+#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 use crate::pyport::{Py_hash_t, Py_ssize_t};
-#[cfg(not(any(Py_LIMITED_API, PyPy)))]
+#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 use std::os::raw::{c_char, c_void};
 
 use std::os::raw::{c_int, c_ulong};
@@ -10,7 +10,7 @@ extern "C" {
     // skipped non-limited _Py_HashPointer
     // skipped non-limited _Py_HashPointerRaw
 
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     pub fn _Py_HashBytes(src: *const c_void, len: Py_ssize_t) -> Py_hash_t;
 }
 
@@ -20,7 +20,7 @@ pub const _PyHASH_MULTIPLIER: c_ulong = 1000003;
 
 // skipped non-limited _Py_HashSecret_t
 
-#[cfg(not(any(Py_LIMITED_API, PyPy)))]
+#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Copy, Clone)]
 pub struct PyHash_FuncDef {
@@ -30,7 +30,7 @@ pub struct PyHash_FuncDef {
     pub seed_bits: c_int,
 }
 
-#[cfg(not(any(Py_LIMITED_API, PyPy)))]
+#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 impl Default for PyHash_FuncDef {
     #[inline]
     fn default() -> Self {
@@ -39,7 +39,7 @@ impl Default for PyHash_FuncDef {
 }
 
 extern "C" {
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     pub fn PyHash_GetFuncDef() -> *mut PyHash_FuncDef;
 }
 
diff --git a/pyo3-ffi/src/pythonrun.rs b/pyo3-ffi/src/pythonrun.rs
index e5f20de0058..10985b6068c 100644
--- a/pyo3-ffi/src/pythonrun.rs
+++ b/pyo3-ffi/src/pythonrun.rs
@@ -1,12 +1,12 @@
 use crate::object::*;
 #[cfg(not(any(PyPy, Py_LIMITED_API, Py_3_10)))]
 use libc::FILE;
-#[cfg(all(not(PyPy), any(Py_LIMITED_API, not(Py_3_10))))]
+#[cfg(all(not(PyPy), any(Py_LIMITED_API, not(Py_3_10), GraalPy)))]
 use std::os::raw::c_char;
 use std::os::raw::c_int;
 
 extern "C" {
-    #[cfg(all(Py_LIMITED_API, not(PyPy)))]
+    #[cfg(any(all(Py_LIMITED_API, not(PyPy)), GraalPy))]
     pub fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int) -> *mut PyObject;
 
     #[cfg_attr(PyPy, link_name = "PyPyErr_Print")]
diff --git a/pyo3-ffi/src/setobject.rs b/pyo3-ffi/src/setobject.rs
index 84a368a7f27..9d5351fc798 100644
--- a/pyo3-ffi/src/setobject.rs
+++ b/pyo3-ffi/src/setobject.rs
@@ -1,5 +1,5 @@
 use crate::object::*;
-#[cfg(not(any(Py_LIMITED_API, PyPy)))]
+#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 use crate::pyport::Py_hash_t;
 use crate::pyport::Py_ssize_t;
 use std::os::raw::c_int;
@@ -7,7 +7,7 @@ use std::ptr::addr_of_mut;
 
 pub const PySet_MINSIZE: usize = 8;
 
-#[cfg(not(any(Py_LIMITED_API, PyPy)))]
+#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Debug)]
 pub struct setentry {
@@ -15,7 +15,7 @@ pub struct setentry {
     pub hash: Py_hash_t,
 }
 
-#[cfg(not(any(Py_LIMITED_API, PyPy)))]
+#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 #[repr(C)]
 #[derive(Debug)]
 pub struct PySetObject {
@@ -32,7 +32,7 @@ pub struct PySetObject {
 
 // skipped
 #[inline]
-#[cfg(all(not(PyPy), not(Py_LIMITED_API)))]
+#[cfg(all(not(any(PyPy, GraalPy)), not(Py_LIMITED_API)))]
 pub unsafe fn PySet_GET_SIZE(so: *mut PyObject) -> Py_ssize_t {
     debug_assert_eq!(PyAnySet_Check(so), 1);
     let so = so.cast::<PySetObject>();
@@ -92,7 +92,7 @@ extern "C" {
 }
 
 #[inline]
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub unsafe fn PyFrozenSet_CheckExact(ob: *mut PyObject) -> c_int {
     (Py_TYPE(ob) == addr_of_mut!(PyFrozenSet_Type)) as c_int
 }
diff --git a/pyo3-ffi/src/sliceobject.rs b/pyo3-ffi/src/sliceobject.rs
index 6f09906fcc4..a3ea153987c 100644
--- a/pyo3-ffi/src/sliceobject.rs
+++ b/pyo3-ffi/src/sliceobject.rs
@@ -5,21 +5,31 @@ use std::ptr::addr_of_mut;
 
 #[cfg_attr(windows, link(name = "pythonXY"))]
 extern "C" {
+    #[cfg(not(GraalPy))]
     #[cfg_attr(PyPy, link_name = "_PyPy_EllipsisObject")]
     static mut _Py_EllipsisObject: PyObject;
+
+    #[cfg(GraalPy)]
+    static mut _Py_EllipsisObjectReference: *mut PyObject;
 }
 
 #[inline]
 pub unsafe fn Py_Ellipsis() -> *mut PyObject {
-    addr_of_mut!(_Py_EllipsisObject)
+    #[cfg(not(GraalPy))]
+    return addr_of_mut!(_Py_EllipsisObject);
+    #[cfg(GraalPy)]
+    return _Py_EllipsisObjectReference;
 }
 
 #[cfg(not(Py_LIMITED_API))]
 #[repr(C)]
 pub struct PySliceObject {
     pub ob_base: PyObject,
+    #[cfg(not(GraalPy))]
     pub start: *mut PyObject,
+    #[cfg(not(GraalPy))]
     pub stop: *mut PyObject,
+    #[cfg(not(GraalPy))]
     pub step: *mut PyObject,
 }
 
diff --git a/pyo3-ffi/src/structseq.rs b/pyo3-ffi/src/structseq.rs
index 6dfc1daf158..f8566787b51 100644
--- a/pyo3-ffi/src/structseq.rs
+++ b/pyo3-ffi/src/structseq.rs
@@ -42,13 +42,13 @@ extern "C" {
 #[cfg(not(Py_LIMITED_API))]
 pub type PyStructSequence = crate::PyTupleObject;
 
-#[cfg(not(any(Py_LIMITED_API, PyPy)))]
+#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 #[inline]
 pub unsafe fn PyStructSequence_SET_ITEM(op: *mut PyObject, i: Py_ssize_t, v: *mut PyObject) {
     crate::PyTuple_SET_ITEM(op, i, v)
 }
 
-#[cfg(not(any(Py_LIMITED_API, PyPy)))]
+#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 #[inline]
 pub unsafe fn PyStructSequence_GET_ITEM(op: *mut PyObject, i: Py_ssize_t) -> *mut PyObject {
     crate::PyTuple_GET_ITEM(op, i)
diff --git a/pyo3-ffi/src/weakrefobject.rs b/pyo3-ffi/src/weakrefobject.rs
index d065ae23e0f..7e11a9012e7 100644
--- a/pyo3-ffi/src/weakrefobject.rs
+++ b/pyo3-ffi/src/weakrefobject.rs
@@ -3,10 +3,10 @@ use std::os::raw::c_int;
 #[cfg(not(PyPy))]
 use std::ptr::addr_of_mut;
 
-#[cfg(all(not(PyPy), Py_LIMITED_API))]
+#[cfg(all(not(PyPy), Py_LIMITED_API, not(GraalPy)))]
 opaque_struct!(PyWeakReference);
 
-#[cfg(all(not(PyPy), not(Py_LIMITED_API)))]
+#[cfg(all(not(PyPy), not(Py_LIMITED_API), not(GraalPy)))]
 pub use crate::_PyWeakReference as PyWeakReference;
 
 #[cfg_attr(windows, link(name = "pythonXY"))]
diff --git a/pytests/tests/test_awaitable.py b/pytests/tests/test_awaitable.py
index 2bada317517..40f1e1cc01d 100644
--- a/pytests/tests/test_awaitable.py
+++ b/pytests/tests/test_awaitable.py
@@ -1,13 +1,22 @@
 import pytest
+import sys
 
 from pyo3_pytests.awaitable import IterAwaitable, FutureAwaitable
 
 
+@pytest.mark.skipif(
+    sys.implementation.name == "graalpy",
+    reason="GraalPy's asyncio module has a bug with native classes, see oracle/graalpython#365",
+)
 @pytest.mark.asyncio
 async def test_iter_awaitable():
     assert await IterAwaitable(5) == 5
 
 
+@pytest.mark.skipif(
+    sys.implementation.name == "graalpy",
+    reason="GraalPy's asyncio module has a bug with native classes, see oracle/graalpython#365",
+)
 @pytest.mark.asyncio
 async def test_future_awaitable():
     assert await FutureAwaitable(5) == 5
diff --git a/pytests/tests/test_misc.py b/pytests/tests/test_misc.py
index 6645f942f1a..de75f1c8a80 100644
--- a/pytests/tests/test_misc.py
+++ b/pytests/tests/test_misc.py
@@ -27,8 +27,8 @@ def test_multiple_imports_same_interpreter_ok():
     reason="Cannot identify subinterpreters on Python older than 3.9",
 )
 @pytest.mark.skipif(
-    platform.python_implementation() == "PyPy",
-    reason="PyPy does not support subinterpreters",
+    platform.python_implementation() in ("PyPy", "GraalVM"),
+    reason="PyPy and GraalPy do not support subinterpreters",
 )
 def test_import_in_subinterpreter_forbidden():
     import _xxsubinterpreters
diff --git a/pytests/tests/test_objstore.py b/pytests/tests/test_objstore.py
index bfd8bad84df..3f0d23fa97c 100644
--- a/pytests/tests/test_objstore.py
+++ b/pytests/tests/test_objstore.py
@@ -12,6 +12,11 @@ def test_objstore_doesnot_leak_memory():
     # check refcount on PyPy
     getrefcount = getattr(sys, "getrefcount", lambda obj: 0)
 
+    if sys.implementation.name == "graalpy":
+        # GraalPy has an incomplete sys.getrefcount implementation
+        def getrefcount(obj):
+            return 0
+
     before = getrefcount(message)
     store = ObjStore()
     for _ in range(N):
diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs
index 027982461e9..44843141440 100644
--- a/src/conversions/std/num.rs
+++ b/src/conversions/std/num.rs
@@ -175,7 +175,7 @@ int_convert_u64_or_i64!(
     true
 );
 
-#[cfg(not(Py_LIMITED_API))]
+#[cfg(all(not(Py_LIMITED_API), not(GraalPy)))]
 mod fast_128bit_int_conversion {
     use super::*;
 
@@ -242,7 +242,7 @@ mod fast_128bit_int_conversion {
 }
 
 // For ABI3 we implement the conversion manually.
-#[cfg(Py_LIMITED_API)]
+#[cfg(any(Py_LIMITED_API, GraalPy))]
 mod slow_128bit_int_conversion {
     use super::*;
     const SHIFT: usize = 64;
diff --git a/src/err/mod.rs b/src/err/mod.rs
index de1e621fc13..8dd16b26b47 100644
--- a/src/err/mod.rs
+++ b/src/err/mod.rs
@@ -863,8 +863,17 @@ impl PyErr {
     /// associated with the exception, as accessible from Python through `__cause__`.
     pub fn cause(&self, py: Python<'_>) -> Option<PyErr> {
         use crate::ffi_ptr_ext::FfiPtrExt;
-        unsafe { ffi::PyException_GetCause(self.value_bound(py).as_ptr()).assume_owned_or_opt(py) }
-            .map(Self::from_value_bound)
+        let obj = unsafe {
+            ffi::PyException_GetCause(self.value_bound(py).as_ptr()).assume_owned_or_opt(py)
+        };
+        // PyException_GetCause is documented as potentially returning PyNone, but only GraalPy seems to actually do that
+        #[cfg(GraalPy)]
+        if let Some(cause) = &obj {
+            if cause.is_none() {
+                return None;
+            }
+        }
+        obj.map(Self::from_value_bound)
     }
 
     /// Set the cause associated with the exception, pass `None` to clear it.
diff --git a/src/exceptions.rs b/src/exceptions.rs
index bd9c89c425f..add958257ab 100644
--- a/src/exceptions.rs
+++ b/src/exceptions.rs
@@ -409,14 +409,14 @@ impl_native_exception!(
     PyExc_FloatingPointError,
     native_doc!("FloatingPointError")
 );
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 impl_native_exception!(
     PyOSError,
     PyExc_OSError,
     native_doc!("OSError"),
     ffi::PyOSErrorObject
 );
-#[cfg(PyPy)]
+#[cfg(any(PyPy, GraalPy))]
 impl_native_exception!(PyOSError, PyExc_OSError, native_doc!("OSError"));
 impl_native_exception!(PyImportError, PyExc_ImportError, native_doc!("ImportError"));
 
@@ -455,14 +455,14 @@ impl_native_exception!(
     PyExc_NotImplementedError,
     native_doc!("NotImplementedError")
 );
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 impl_native_exception!(
     PySyntaxError,
     PyExc_SyntaxError,
     native_doc!("SyntaxError"),
     ffi::PySyntaxErrorObject
 );
-#[cfg(PyPy)]
+#[cfg(any(PyPy, GraalPy))]
 impl_native_exception!(PySyntaxError, PyExc_SyntaxError, native_doc!("SyntaxError"));
 impl_native_exception!(
     PyReferenceError,
@@ -470,14 +470,14 @@ impl_native_exception!(
     native_doc!("ReferenceError")
 );
 impl_native_exception!(PySystemError, PyExc_SystemError, native_doc!("SystemError"));
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 impl_native_exception!(
     PySystemExit,
     PyExc_SystemExit,
     native_doc!("SystemExit"),
     ffi::PySystemExitObject
 );
-#[cfg(PyPy)]
+#[cfg(any(PyPy, GraalPy))]
 impl_native_exception!(PySystemExit, PyExc_SystemExit, native_doc!("SystemExit"));
 impl_native_exception!(PyTypeError, PyExc_TypeError, native_doc!("TypeError"));
 impl_native_exception!(
@@ -485,14 +485,14 @@ impl_native_exception!(
     PyExc_UnboundLocalError,
     native_doc!("UnboundLocalError")
 );
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 impl_native_exception!(
     PyUnicodeError,
     PyExc_UnicodeError,
     native_doc!("UnicodeError"),
     ffi::PyUnicodeErrorObject
 );
-#[cfg(PyPy)]
+#[cfg(any(PyPy, GraalPy))]
 impl_native_exception!(
     PyUnicodeError,
     PyExc_UnicodeError,
diff --git a/src/gil.rs b/src/gil.rs
index 5ca3167e66c..bb07489fa1f 100644
--- a/src/gil.rs
+++ b/src/gil.rs
@@ -78,7 +78,7 @@ fn gil_is_acquired() -> bool {
 /// Python::with_gil(|py| py.run_bound("print('Hello World')", None, None))
 /// # }
 /// ```
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub fn prepare_freethreaded_python() {
     // Protect against race conditions when Python is not yet initialized and multiple threads
     // concurrently call 'prepare_freethreaded_python()'. Note that we do not protect against
@@ -125,7 +125,7 @@ pub fn prepare_freethreaded_python() {
 ///     });
 /// }
 /// ```
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub unsafe fn with_embedded_python_interpreter<F, R>(f: F) -> R
 where
     F: for<'p> FnOnce(Python<'p>) -> R,
@@ -182,14 +182,14 @@ impl GILGuard {
         //    auto-initialize so this avoids breaking existing builds.
         //  - Otherwise, just check the GIL is initialized.
         cfg_if::cfg_if! {
-            if #[cfg(all(feature = "auto-initialize", not(PyPy)))] {
+            if #[cfg(all(feature = "auto-initialize", not(any(PyPy, GraalPy))))] {
                 prepare_freethreaded_python();
             } else {
                 // This is a "hack" to make running `cargo test` for PyO3 convenient (i.e. no need
                 // to specify `--features auto-initialize` manually. Tests within the crate itself
                 // all depend on the auto-initialize feature for conciseness but Cargo does not
                 // provide a mechanism to specify required features for tests.
-                #[cfg(not(PyPy))]
+                #[cfg(not(any(PyPy, GraalPy)))]
                 if option_env!("CARGO_PRIMARY_PACKAGE").is_some() {
                     prepare_freethreaded_python();
                 }
diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs
index 9ca80e3918c..ba13f7a9841 100644
--- a/src/impl_/pymodule.rs
+++ b/src/impl_/pymodule.rs
@@ -2,10 +2,14 @@
 
 use std::cell::UnsafeCell;
 
-#[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))]
+#[cfg(all(
+    not(any(PyPy, GraalPy)),
+    Py_3_9,
+    not(all(windows, Py_LIMITED_API, not(Py_3_10)))
+))]
 use portable_atomic::{AtomicI64, Ordering};
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 use crate::exceptions::PyImportError;
 use crate::{ffi, sync::GILOnceCell, types::PyModule, Bound, Py, PyResult, Python};
 
@@ -15,7 +19,11 @@ pub struct ModuleDef {
     ffi_def: UnsafeCell<ffi::PyModuleDef>,
     initializer: ModuleInitializer,
     /// Interpreter ID where module was initialized (not applicable on PyPy).
-    #[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))]
+    #[cfg(all(
+        not(any(PyPy, GraalPy)),
+        Py_3_9,
+        not(all(windows, Py_LIMITED_API, not(Py_3_10)))
+    ))]
     interpreter: AtomicI64,
     /// Initialized module object, cached to avoid reinitialization.
     module: GILOnceCell<Py<PyModule>>,
@@ -58,7 +66,11 @@ impl ModuleDef {
             ffi_def,
             initializer,
             // -1 is never expected to be a valid interpreter ID
-            #[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))]
+            #[cfg(all(
+                not(any(PyPy, GraalPy)),
+                Py_3_9,
+                not(all(windows, Py_LIMITED_API, not(Py_3_10)))
+            ))]
             interpreter: AtomicI64::new(-1),
             module: GILOnceCell::new(),
         }
@@ -85,7 +97,7 @@ impl ModuleDef {
         // that static data is not reused across interpreters.
         //
         // PyPy does not have subinterpreters, so no need to check interpreter ID.
-        #[cfg(not(PyPy))]
+        #[cfg(not(any(PyPy, GraalPy)))]
         {
             // PyInterpreterState_Get is only available on 3.9 and later, but is missing
             // from python3.dll for Windows stable API on 3.9
diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs
index 4d77f329125..db493817cba 100644
--- a/src/impl_/trampoline.rs
+++ b/src/impl_/trampoline.rs
@@ -24,12 +24,14 @@ pub unsafe fn module_init(
 }
 
 #[inline]
+#[allow(clippy::used_underscore_binding)]
 pub unsafe fn noargs(
     slf: *mut ffi::PyObject,
-    args: *mut ffi::PyObject,
+    _args: *mut ffi::PyObject,
     f: for<'py> unsafe fn(Python<'py>, *mut ffi::PyObject) -> PyResult<*mut ffi::PyObject>,
 ) -> *mut ffi::PyObject {
-    debug_assert!(args.is_null());
+    #[cfg(not(GraalPy))] // this is not specified and GraalPy does not pass null here
+    debug_assert!(_args.is_null());
     trampoline(|py| f(py, slf))
 }
 
diff --git a/src/lib.rs b/src/lib.rs
index fd5a520fdb5..e444912a63d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -321,7 +321,7 @@ pub use crate::err::{
 };
 #[allow(deprecated)]
 pub use crate::gil::GILPool;
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub use crate::gil::{prepare_freethreaded_python, with_embedded_python_interpreter};
 pub use crate::instance::{Borrowed, Bound, Py, PyNativeType, PyObject};
 pub use crate::marker::Python;
diff --git a/src/macros.rs b/src/macros.rs
index 648bac180ff..09e2acfb45b 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -201,7 +201,7 @@ macro_rules! wrap_pymodule {
 ///
 /// Use it before [`prepare_freethreaded_python`](crate::prepare_freethreaded_python) and
 /// leave feature `auto-initialize` off
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[macro_export]
 macro_rules! append_to_inittab {
     ($module:ident) => {
diff --git a/src/marker.rs b/src/marker.rs
index 08b9042491b..67119b5e55e 100644
--- a/src/marker.rs
+++ b/src/marker.rs
@@ -400,7 +400,7 @@ impl Python<'_> {
     /// If the [`auto-initialize`] feature is enabled and the Python runtime is not already
     /// initialized, this function will initialize it. See
     #[cfg_attr(
-        not(PyPy),
+        not(any(PyPy, GraalPy)),
         doc = "[`prepare_freethreaded_python`](crate::prepare_freethreaded_python)"
     )]
     #[cfg_attr(PyPy, doc = "`prepare_freethreaded_python`")]
diff --git a/src/types/any.rs b/src/types/any.rs
index 19855abbb9a..ab4f5727623 100644
--- a/src/types/any.rs
+++ b/src/types/any.rs
@@ -6,7 +6,7 @@ use crate::ffi_ptr_ext::FfiPtrExt;
 use crate::instance::Bound;
 use crate::py_result_ext::PyResultExt;
 use crate::type_object::{HasPyGilRef, PyTypeCheck, PyTypeInfo};
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 use crate::types::PySuper;
 use crate::types::{PyDict, PyIterator, PyList, PyString, PyTuple, PyType};
 use crate::{err, ffi, Py, PyNativeType, Python};
@@ -929,7 +929,7 @@ impl PyAny {
     /// Return a proxy object that delegates method calls to a parent or sibling class of type.
     ///
     /// This is equivalent to the Python expression `super()`
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn py_super(&self) -> PyResult<&PySuper> {
         self.as_borrowed().py_super().map(Bound::into_gil_ref)
     }
@@ -1708,7 +1708,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// Return a proxy object that delegates method calls to a parent or sibling class of type.
     ///
     /// This is equivalent to the Python expression `super()`
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     fn py_super(&self) -> PyResult<Bound<'py, PySuper>>;
 }
 
@@ -1975,6 +1975,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
         cfg_if::cfg_if! {
             if #[cfg(all(
                 not(PyPy),
+                not(GraalPy),
                 any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // PyObject_CallNoArgs was added to python in 3.9 but to limited API in 3.10
             ))] {
                 // Optimized path on python 3.9+
@@ -2010,7 +2011,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
         N: IntoPy<Py<PyString>>,
     {
         cfg_if::cfg_if! {
-            if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy))))] {
+            if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy))))] {
                 let py = self.py();
 
                 // Optimized path on python 3.9+
@@ -2265,7 +2266,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> {
         inner(self, value.to_object(py).into_bound(py))
     }
 
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     fn py_super(&self) -> PyResult<Bound<'py, PySuper>> {
         PySuper::new_bound(&self.get_type(), self)
     }
diff --git a/src/types/complex.rs b/src/types/complex.rs
index 80ecffdc5cf..e711b054fe3 100644
--- a/src/types/complex.rs
+++ b/src/types/complex.rs
@@ -53,7 +53,7 @@ impl PyComplex {
     }
 }
 
-#[cfg(not(any(Py_LIMITED_API, PyPy)))]
+#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 mod not_limited_impls {
     use crate::ffi_ptr_ext::FfiPtrExt;
     use crate::Borrowed;
@@ -264,10 +264,10 @@ pub trait PyComplexMethods<'py>: crate::sealed::Sealed {
     /// Returns the imaginary part of the complex number.
     fn imag(&self) -> c_double;
     /// Returns `|self|`.
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     fn abs(&self) -> c_double;
     /// Returns `self` raised to the power of `other`.
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     fn pow(&self, other: &Bound<'py, PyComplex>) -> Bound<'py, PyComplex>;
 }
 
@@ -280,7 +280,7 @@ impl<'py> PyComplexMethods<'py> for Bound<'py, PyComplex> {
         unsafe { ffi::PyComplex_ImagAsDouble(self.as_ptr()) }
     }
 
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     fn abs(&self) -> c_double {
         unsafe {
             let val = (*self.as_ptr().cast::<ffi::PyComplexObject>()).cval;
@@ -288,7 +288,7 @@ impl<'py> PyComplexMethods<'py> for Bound<'py, PyComplex> {
         }
     }
 
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     fn pow(&self, other: &Bound<'py, PyComplex>) -> Bound<'py, PyComplex> {
         use crate::ffi_ptr_ext::FfiPtrExt;
         unsafe {
diff --git a/src/types/datetime.rs b/src/types/datetime.rs
index d46a2b77c33..e1620a6f647 100644
--- a/src/types/datetime.rs
+++ b/src/types/datetime.rs
@@ -11,6 +11,8 @@ use crate::ffi::{
     PyDateTime_DATE_GET_FOLD, PyDateTime_DATE_GET_HOUR, PyDateTime_DATE_GET_MICROSECOND,
     PyDateTime_DATE_GET_MINUTE, PyDateTime_DATE_GET_SECOND,
 };
+#[cfg(GraalPy)]
+use crate::ffi::{PyDateTime_DATE_GET_TZINFO, PyDateTime_TIME_GET_TZINFO, Py_IsNone};
 use crate::ffi::{
     PyDateTime_DELTA_GET_DAYS, PyDateTime_DELTA_GET_MICROSECONDS, PyDateTime_DELTA_GET_SECONDS,
 };
@@ -552,6 +554,7 @@ impl<'py> PyTzInfoAccess<'py> for &'py PyDateTime {
 impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> {
     fn get_tzinfo_bound(&self) -> Option<Bound<'py, PyTzInfo>> {
         let ptr = self.as_ptr() as *mut ffi::PyDateTime_DateTime;
+        #[cfg(not(GraalPy))]
         unsafe {
             if (*ptr).hastzinfo != 0 {
                 Some(
@@ -565,6 +568,20 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> {
                 None
             }
         }
+
+        #[cfg(GraalPy)]
+        unsafe {
+            let res = PyDateTime_DATE_GET_TZINFO(ptr as *mut ffi::PyObject);
+            if Py_IsNone(res) == 1 {
+                None
+            } else {
+                Some(
+                    res.assume_borrowed(self.py())
+                        .to_owned()
+                        .downcast_into_unchecked(),
+                )
+            }
+        }
     }
 }
 
@@ -740,6 +757,7 @@ impl<'py> PyTzInfoAccess<'py> for &'py PyTime {
 impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> {
     fn get_tzinfo_bound(&self) -> Option<Bound<'py, PyTzInfo>> {
         let ptr = self.as_ptr() as *mut ffi::PyDateTime_Time;
+        #[cfg(not(GraalPy))]
         unsafe {
             if (*ptr).hastzinfo != 0 {
                 Some(
@@ -753,6 +771,20 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> {
                 None
             }
         }
+
+        #[cfg(GraalPy)]
+        unsafe {
+            let res = PyDateTime_TIME_GET_TZINFO(ptr as *mut ffi::PyObject);
+            if Py_IsNone(res) == 1 {
+                None
+            } else {
+                Some(
+                    res.assume_borrowed(self.py())
+                        .to_owned()
+                        .downcast_into_unchecked(),
+                )
+            }
+        }
     }
 }
 
diff --git a/src/types/dict.rs b/src/types/dict.rs
index 43bade5a80c..a4ba72a59c0 100644
--- a/src/types/dict.rs
+++ b/src/types/dict.rs
@@ -20,11 +20,11 @@ pyobject_native_type!(
 );
 
 /// Represents a Python `dict_keys`.
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(transparent)]
 pub struct PyDictKeys(PyAny);
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pyobject_native_type_core!(
     PyDictKeys,
     pyobject_native_static_type_object!(ffi::PyDictKeys_Type),
@@ -32,11 +32,11 @@ pyobject_native_type_core!(
 );
 
 /// Represents a Python `dict_values`.
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(transparent)]
 pub struct PyDictValues(PyAny);
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pyobject_native_type_core!(
     PyDictValues,
     pyobject_native_static_type_object!(ffi::PyDictValues_Type),
@@ -44,11 +44,11 @@ pyobject_native_type_core!(
 );
 
 /// Represents a Python `dict_items`.
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 #[repr(transparent)]
 pub struct PyDictItems(PyAny);
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pyobject_native_type_core!(
     PyDictItems,
     pyobject_native_static_type_object!(ffi::PyDictItems_Type),
@@ -76,14 +76,14 @@ impl PyDict {
 
     /// Deprecated form of [`from_sequence_bound`][PyDict::from_sequence_bound].
     #[cfg_attr(
-        all(not(PyPy), not(feature = "gil-refs")),
+        all(not(any(PyPy, GraalPy)), not(feature = "gil-refs")),
         deprecated(
             since = "0.21.0",
             note = "`PyDict::from_sequence` will be replaced by `PyDict::from_sequence_bound` in a future PyO3 version"
         )
     )]
     #[inline]
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn from_sequence(seq: &PyAny) -> PyResult<&PyDict> {
         Self::from_sequence_bound(&seq.as_borrowed()).map(Bound::into_gil_ref)
     }
@@ -95,7 +95,7 @@ impl PyDict {
     ///
     /// Returns an error on invalid input. In the case of key collisions,
     /// this keeps the last entry seen.
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn from_sequence_bound<'py>(seq: &Bound<'py, PyAny>) -> PyResult<Bound<'py, PyDict>> {
         let py = seq.py();
         let dict = Self::new_bound(py);
@@ -542,12 +542,12 @@ impl<'a, 'py> Borrowed<'a, 'py, PyDict> {
 }
 
 fn dict_len(dict: &Bound<'_, PyDict>) -> Py_ssize_t {
-    #[cfg(any(not(Py_3_8), PyPy, Py_LIMITED_API))]
+    #[cfg(any(not(Py_3_8), PyPy, GraalPy, Py_LIMITED_API))]
     unsafe {
         ffi::PyDict_Size(dict.as_ptr())
     }
 
-    #[cfg(all(Py_3_8, not(PyPy), not(Py_LIMITED_API)))]
+    #[cfg(all(Py_3_8, not(PyPy), not(GraalPy), not(Py_LIMITED_API)))]
     unsafe {
         (*dict.as_ptr().cast::<ffi::PyDictObject>()).ma_used
     }
@@ -824,7 +824,7 @@ where
 #[cfg_attr(not(feature = "gil-refs"), allow(deprecated))]
 mod tests {
     use super::*;
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     use crate::exceptions;
     use crate::types::PyTuple;
     use std::collections::{BTreeMap, HashMap};
@@ -850,7 +850,7 @@ mod tests {
     }
 
     #[test]
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     fn test_from_sequence() {
         Python::with_gil(|py| {
             let items = PyList::new(py, &vec![("a", 1), ("b", 2)]);
@@ -881,7 +881,7 @@ mod tests {
     }
 
     #[test]
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     fn test_from_sequence_err() {
         Python::with_gil(|py| {
             let items = PyList::new(py, &vec!["a", "b"]);
@@ -955,7 +955,7 @@ mod tests {
 
     #[test]
     #[allow(deprecated)]
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     fn test_get_item_with_error() {
         Python::with_gil(|py| {
             let mut v = HashMap::new();
@@ -1388,7 +1388,7 @@ mod tests {
         });
     }
 
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     fn abc_dict(py: Python<'_>) -> Bound<'_, PyDict> {
         let mut map = HashMap::<&'static str, i32>::new();
         map.insert("a", 1);
@@ -1398,7 +1398,7 @@ mod tests {
     }
 
     #[test]
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     fn dict_keys_view() {
         Python::with_gil(|py| {
             let dict = abc_dict(py);
@@ -1410,7 +1410,7 @@ mod tests {
     }
 
     #[test]
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     fn dict_values_view() {
         Python::with_gil(|py| {
             let dict = abc_dict(py);
@@ -1422,7 +1422,7 @@ mod tests {
     }
 
     #[test]
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     fn dict_items_view() {
         Python::with_gil(|py| {
             let dict = abc_dict(py);
diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs
index 8a60efb8caa..36d5578f701 100644
--- a/src/types/frozenset.rs
+++ b/src/types/frozenset.rs
@@ -60,7 +60,7 @@ impl<'py> PyFrozenSetBuilder<'py> {
 #[repr(transparent)]
 pub struct PyFrozenSet(PyAny);
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pyobject_native_type!(
     PyFrozenSet,
     ffi::PySetObject,
@@ -68,7 +68,7 @@ pyobject_native_type!(
     #checkfunction=ffi::PyFrozenSet_Check
 );
 
-#[cfg(PyPy)]
+#[cfg(any(PyPy, GraalPy))]
 pyobject_native_type_core!(
     PyFrozenSet,
     pyobject_native_static_type_object!(ffi::PyFrozenSet_Type),
diff --git a/src/types/mod.rs b/src/types/mod.rs
index cee45e8678d..5448999d4c7 100644
--- a/src/types/mod.rs
+++ b/src/types/mod.rs
@@ -5,7 +5,7 @@ pub use self::boolobject::{PyBool, PyBoolMethods};
 pub use self::bytearray::{PyByteArray, PyByteArrayMethods};
 pub use self::bytes::{PyBytes, PyBytesMethods};
 pub use self::capsule::{PyCapsule, PyCapsuleMethods};
-#[cfg(all(not(Py_LIMITED_API), not(PyPy)))]
+#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
 pub use self::code::PyCode;
 pub use self::complex::{PyComplex, PyComplexMethods};
 #[allow(deprecated)]
@@ -17,15 +17,15 @@ pub use self::datetime::{
     PyTimeAccess, PyTzInfo, PyTzInfoAccess,
 };
 pub use self::dict::{IntoPyDict, PyDict, PyDictMethods};
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub use self::dict::{PyDictItems, PyDictKeys, PyDictValues};
 pub use self::ellipsis::PyEllipsis;
 pub use self::float::{PyFloat, PyFloatMethods};
-#[cfg(all(not(Py_LIMITED_API), not(PyPy)))]
+#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
 pub use self::frame::PyFrame;
 pub use self::frozenset::{PyFrozenSet, PyFrozenSetBuilder, PyFrozenSetMethods};
 pub use self::function::PyCFunction;
-#[cfg(all(not(Py_LIMITED_API), not(PyPy)))]
+#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
 pub use self::function::PyFunction;
 pub use self::iterator::PyIterator;
 pub use self::list::{PyList, PyListMethods};
@@ -36,7 +36,7 @@ pub use self::none::PyNone;
 pub use self::notimplemented::PyNotImplemented;
 pub use self::num::PyLong;
 pub use self::num::PyLong as PyInt;
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pub use self::pysuper::PySuper;
 pub use self::sequence::{PySequence, PySequenceMethods};
 pub use self::set::{PySet, PySetMethods};
@@ -328,7 +328,7 @@ pub(crate) mod boolobject;
 pub(crate) mod bytearray;
 pub(crate) mod bytes;
 pub(crate) mod capsule;
-#[cfg(all(not(Py_LIMITED_API), not(PyPy)))]
+#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
 mod code;
 pub(crate) mod complex;
 #[cfg(not(Py_LIMITED_API))]
@@ -336,7 +336,7 @@ pub(crate) mod datetime;
 pub(crate) mod dict;
 mod ellipsis;
 pub(crate) mod float;
-#[cfg(all(not(Py_LIMITED_API), not(PyPy)))]
+#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))]
 mod frame;
 pub(crate) mod frozenset;
 mod function;
@@ -348,7 +348,7 @@ pub(crate) mod module;
 mod none;
 mod notimplemented;
 mod num;
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 mod pysuper;
 pub(crate) mod sequence;
 pub(crate) mod set;
diff --git a/src/types/sequence.rs b/src/types/sequence.rs
index 62abb66fa6e..2b37b6d14f0 100644
--- a/src/types/sequence.rs
+++ b/src/types/sequence.rs
@@ -134,7 +134,7 @@ impl PySequence {
     /// Returns the number of occurrences of `value` in self, that is, return the
     /// number of keys for which `self[key] == value`.
     #[inline]
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     pub fn count<V>(&self, value: V) -> PyResult<usize>
     where
         V: ToPyObject,
@@ -870,7 +870,7 @@ mod tests {
     }
 
     #[test]
-    #[cfg(not(PyPy))]
+    #[cfg(not(any(PyPy, GraalPy)))]
     fn test_seq_count() {
         Python::with_gil(|py| {
             let v: Vec<i32> = vec![1, 1, 2, 3, 5, 8];
diff --git a/src/types/set.rs b/src/types/set.rs
index c043fa5ba4f..4f1fcf8499f 100644
--- a/src/types/set.rs
+++ b/src/types/set.rs
@@ -14,7 +14,7 @@ use std::ptr;
 #[repr(transparent)]
 pub struct PySet(PyAny);
 
-#[cfg(not(PyPy))]
+#[cfg(not(any(PyPy, GraalPy)))]
 pyobject_native_type!(
     PySet,
     ffi::PySetObject,
@@ -22,7 +22,7 @@ pyobject_native_type!(
     #checkfunction=ffi::PySet_Check
 );
 
-#[cfg(PyPy)]
+#[cfg(any(PyPy, GraalPy))]
 pyobject_native_type_core!(
     PySet,
     pyobject_native_static_type_object!(ffi::PySet_Type),
diff --git a/src/types/string.rs b/src/types/string.rs
index 81c41adb545..4bbc6fb86b0 100644
--- a/src/types/string.rs
+++ b/src/types/string.rs
@@ -270,7 +270,7 @@ impl PyString {
     ///
     /// By using this API, you accept responsibility for testing that PyStringData behaves as
     /// expected on the targets where you plan to distribute your software.
-    #[cfg(not(Py_LIMITED_API))]
+    #[cfg(not(any(Py_LIMITED_API, GraalPy)))]
     pub unsafe fn data(&self) -> PyResult<PyStringData<'_>> {
         self.as_borrowed().data()
     }
@@ -319,7 +319,7 @@ pub trait PyStringMethods<'py>: crate::sealed::Sealed {
     ///
     /// By using this API, you accept responsibility for testing that PyStringData behaves as
     /// expected on the targets where you plan to distribute your software.
-    #[cfg(not(Py_LIMITED_API))]
+    #[cfg(not(any(Py_LIMITED_API, GraalPy)))]
     unsafe fn data(&self) -> PyResult<PyStringData<'_>>;
 }
 
@@ -345,7 +345,7 @@ impl<'py> PyStringMethods<'py> for Bound<'py, PyString> {
         }
     }
 
-    #[cfg(not(Py_LIMITED_API))]
+    #[cfg(not(any(Py_LIMITED_API, GraalPy)))]
     unsafe fn data(&self) -> PyResult<PyStringData<'_>> {
         self.as_borrowed().data()
     }
@@ -408,7 +408,7 @@ impl<'a> Borrowed<'a, '_, PyString> {
         Cow::Owned(String::from_utf8_lossy(bytes.as_bytes()).into_owned())
     }
 
-    #[cfg(not(Py_LIMITED_API))]
+    #[cfg(not(any(Py_LIMITED_API, GraalPy)))]
     unsafe fn data(self) -> PyResult<PyStringData<'a>> {
         let ptr = self.as_ptr();
 
diff --git a/src/types/tuple.rs b/src/types/tuple.rs
index 4dfe4436bf0..636a2f3e11f 100644
--- a/src/types/tuple.rs
+++ b/src/types/tuple.rs
@@ -34,9 +34,9 @@ fn new_from_iter<'py>(
         let mut counter: Py_ssize_t = 0;
 
         for obj in elements.take(len as usize) {
-            #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+            #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
             ffi::PyTuple_SET_ITEM(ptr, counter, obj.into_ptr());
-            #[cfg(any(Py_LIMITED_API, PyPy))]
+            #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))]
             ffi::PyTuple_SetItem(ptr, counter, obj.into_ptr());
             counter += 1;
         }
@@ -186,7 +186,7 @@ impl PyTuple {
     /// # Safety
     ///
     /// Caller must verify that the index is within the bounds of the tuple.
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     pub unsafe fn get_item_unchecked(&self, index: usize) -> &PyAny {
         self.as_borrowed()
             .get_borrowed_item_unchecked(index)
@@ -194,7 +194,7 @@ impl PyTuple {
     }
 
     /// Returns `self` as a slice of objects.
-    #[cfg(not(Py_LIMITED_API))]
+    #[cfg(not(any(Py_LIMITED_API, GraalPy)))]
     pub fn as_slice(&self) -> &[&PyAny] {
         // This is safe because &PyAny has the same memory layout as *mut ffi::PyObject,
         // and because tuples are immutable.
@@ -293,7 +293,7 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed {
     /// # Safety
     ///
     /// Caller must verify that the index is within the bounds of the tuple.
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny>;
 
     /// Like [`get_item_unchecked`][PyTupleMethods::get_item_unchecked], but returns a borrowed object,
@@ -302,11 +302,11 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed {
     /// # Safety
     ///
     /// Caller must verify that the index is within the bounds of the tuple.
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     unsafe fn get_borrowed_item_unchecked<'a>(&'a self, index: usize) -> Borrowed<'a, 'py, PyAny>;
 
     /// Returns `self` as a slice of objects.
-    #[cfg(not(Py_LIMITED_API))]
+    #[cfg(not(any(Py_LIMITED_API, GraalPy)))]
     fn as_slice(&self) -> &[Bound<'py, PyAny>];
 
     /// Determines if self contains `value`.
@@ -339,9 +339,9 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed {
 impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> {
     fn len(&self) -> usize {
         unsafe {
-            #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+            #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
             let size = ffi::PyTuple_GET_SIZE(self.as_ptr());
-            #[cfg(any(Py_LIMITED_API, PyPy))]
+            #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))]
             let size = ffi::PyTuple_Size(self.as_ptr());
             // non-negative Py_ssize_t should always fit into Rust uint
             size as usize
@@ -376,17 +376,17 @@ impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> {
         self.as_borrowed().get_borrowed_item(index)
     }
 
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny> {
         self.get_borrowed_item_unchecked(index).to_owned()
     }
 
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     unsafe fn get_borrowed_item_unchecked<'a>(&'a self, index: usize) -> Borrowed<'a, 'py, PyAny> {
         self.as_borrowed().get_borrowed_item_unchecked(index)
     }
 
-    #[cfg(not(Py_LIMITED_API))]
+    #[cfg(not(any(Py_LIMITED_API, GraalPy)))]
     fn as_slice(&self) -> &[Bound<'py, PyAny>] {
         // This is safe because Bound<'py, PyAny> has the same memory layout as *mut ffi::PyObject,
         // and because tuples are immutable.
@@ -436,7 +436,7 @@ impl<'a, 'py> Borrowed<'a, 'py, PyTuple> {
         }
     }
 
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     unsafe fn get_borrowed_item_unchecked(self, index: usize) -> Borrowed<'a, 'py, PyAny> {
         ffi::PyTuple_GET_ITEM(self.as_ptr(), index as Py_ssize_t).assume_borrowed(self.py())
     }
@@ -591,9 +591,9 @@ impl<'a, 'py> BorrowedTupleIterator<'a, 'py> {
         tuple: Borrowed<'a, 'py, PyTuple>,
         index: usize,
     ) -> Borrowed<'a, 'py, PyAny> {
-        #[cfg(any(Py_LIMITED_API, PyPy))]
+        #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))]
         let item = tuple.get_borrowed_item(index).expect("tuple.get failed");
-        #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+        #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
         let item = tuple.get_borrowed_item_unchecked(index);
         item
     }
@@ -696,10 +696,10 @@ fn type_output() -> TypeInfo {
         {
             let t = obj.downcast::<PyTuple>()?;
             if t.len() == $length {
-                #[cfg(any(Py_LIMITED_API, PyPy))]
+                #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))]
                 return Ok(($(t.get_borrowed_item($n)?.extract::<$T>()?,)+));
 
-                #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+                #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
                 unsafe {return Ok(($(t.get_borrowed_item_unchecked($n).extract::<$T>()?,)+));}
             } else {
                 Err(wrong_tuple_length(t, $length))
@@ -718,9 +718,9 @@ fn array_into_tuple<const N: usize>(py: Python<'_>, array: [PyObject; N]) -> Py<
         let ptr = ffi::PyTuple_New(N.try_into().expect("0 < N <= 12"));
         let tup = Py::from_owned_ptr(py, ptr);
         for (index, obj) in array.into_iter().enumerate() {
-            #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+            #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
             ffi::PyTuple_SET_ITEM(ptr, index as ffi::Py_ssize_t, obj.into_ptr());
-            #[cfg(any(Py_LIMITED_API, PyPy))]
+            #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))]
             ffi::PyTuple_SetItem(ptr, index as ffi::Py_ssize_t, obj.into_ptr());
         }
         tup
@@ -1010,7 +1010,7 @@ mod tests {
     }
 
     #[test]
-    #[cfg(not(Py_LIMITED_API))]
+    #[cfg(not(any(Py_LIMITED_API, GraalPy)))]
     fn test_as_slice() {
         Python::with_gil(|py| {
             let ob = (1, 2, 3).to_object(py);
@@ -1112,7 +1112,7 @@ mod tests {
         });
     }
 
-    #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+    #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     #[test]
     fn test_tuple_get_item_unchecked_sanity() {
         Python::with_gil(|py| {
@@ -1457,7 +1457,7 @@ mod tests {
                     .unwrap(),
                 2
             );
-            #[cfg(not(any(Py_LIMITED_API, PyPy)))]
+            #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
             {
                 assert_eq!(
                     unsafe { tuple.get_item_unchecked(2) }