diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2de93919c7b1e412d3caf4948e200a6a13dda762..e568f1ae611aac1e63c96c4542119097ae6900eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ### Added
 - Support for `Eigen::SparseMatrix` types ([#426](https://github.com/stack-of-tasks/eigenpy/pull/426))
-- Support for `boost::variant` types with `BoostVariantConvertor` ([#430](https://github.com/stack-of-tasks/eigenpy/pull/430))
+- Support for `boost::variant` types with `VariantConverter` ([#430](https://github.com/stack-of-tasks/eigenpy/pull/430))
+- Support for `std::variant` types with `VariantConverter` ([#431](https://github.com/stack-of-tasks/eigenpy/pull/431))
 
 ### Fixed
 - Fix the issue of missing exposition of Eigen types with __int64 scalar type ([#426](https://github.com/stack-of-tasks/eigenpy/pull/426))
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1c9024257ea76c5c90809e179234591945fe939a..d8d8a79e61072d0e53893512824df798a89ffe74 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -166,6 +166,7 @@ set(${PROJECT_NAME}_HEADERS
     include/eigenpy/sparse/eigen-from-python.hpp
     include/eigenpy/scipy-allocator.hpp
     include/eigenpy/scipy-type.hpp
+    include/eigenpy/variant.hpp
     include/eigenpy/swig.hpp
     include/eigenpy/version.hpp)
 
diff --git a/include/eigenpy/boost-variant.hpp b/include/eigenpy/boost-variant.hpp
deleted file mode 100644
index f3735a953fe4f718041b457921c74a816a2bda40..0000000000000000000000000000000000000000
--- a/include/eigenpy/boost-variant.hpp
+++ /dev/null
@@ -1,136 +0,0 @@
-//
-// Copyright (c) 2024 INRIA
-//
-
-#ifndef __eigenpy_utils_boost_variant_hpp__
-#define __eigenpy_utils_boost_variant_hpp__
-
-#include <boost/python.hpp>
-#include <boost/variant.hpp>
-#include <boost/mpl/for_each.hpp>
-
-namespace eigenpy {
-
-namespace details {
-
-/// Convert boost::variant<class...> alternative to a Python object.
-/// This converter copy the alternative.
-template <typename Variant>
-struct BoostVariantValueToObject : boost::static_visitor<PyObject*> {
-  typedef Variant variant_type;
-
-  static result_type convert(const variant_type& gm) {
-    return apply_visitor(BoostVariantValueToObject(), gm);
-  }
-
-  template <typename T>
-  result_type operator()(T& t) const {
-    return boost::python::incref(boost::python::object(t).ptr());
-  }
-};
-
-/// Convert boost::variant<class...> alternative reference to a Python object.
-/// This converter return the alternative reference.
-/// The code that create the reference holder is taken from
-/// \see boost::python::to_python_indirect.
-template <typename Variant>
-struct BoostVariantRefToObject : boost::static_visitor<PyObject*> {
-  typedef Variant variant_type;
-
-  static result_type convert(const variant_type& gm) {
-    return apply_visitor(BoostVariantRefToObject(), gm);
-  }
-
-  template <typename T>
-  result_type operator()(T& t) const {
-    return boost::python::detail::make_reference_holder::execute(&t);
-  }
-};
-
-/// Converter used in \see ReturnInternalBoostVariant.
-/// This is inspired by \see boost::python::reference_existing_object.
-/// It will call \see BoostVariantRefToObject to extract the alternative
-/// reference.
-template <typename Variant>
-struct BoostVariantConverter {
-  typedef Variant variant_type;
-
-  template <class T>
-  struct apply {
-    struct type {
-      PyObject* operator()(const variant_type& gm) const {
-        return BoostVariantRefToObject<variant_type>::convert(gm);
-      }
-
-#ifndef BOOST_PYTHON_NO_PY_SIGNATURES
-      PyTypeObject const* get_pytype() const {
-        return boost::python::converter::registered_pytype<
-            variant_type>::get_pytype();
-      }
-#endif
-    };
-  };
-};
-
-/// Declare a variant alternative implicitly convertible to the variant
-template <typename Variant>
-struct BoostVariantImplicitlyConvertible {
-  typedef Variant variant_type;
-
-  template <class T>
-  void operator()(T) {
-    boost::python::implicitly_convertible<T, variant_type>();
-  }
-};
-
-}  // namespace details
-
-/// Variant of \see boost::python::return_internal_reference that
-/// extract boost::variant<class...> alternative reference before
-/// converting it into a PyObject
-template <typename Variant>
-struct ReturnInternalBoostVariant : boost::python::return_internal_reference<> {
-  typedef Variant variant_type;
-
-  typedef details::BoostVariantConverter<variant_type> result_converter;
-};
-
-/// Define a defaults converter to convert a boost::variant alternative to a
-/// Python object by copy and to convert implicitly an alternative to a
-/// boost::variant.
-///
-/// Example:
-///
-///   typedef boost::variant<Struct1, Struct2> MyVariant;
-///   struct VariantHolder {
-///     MyVariant variant;
-///   };
-///   ...
-///   void expose() {
-///     boost::python::class_<Struct1>("Struct1", bp::init<>());
-///     boost::python::class_<Struct2>("Struct1", bp::init<>())
-///     typedef eigenpy::BoostVariantConvertor<MyVariant> Convertor;
-///     Convertor::registration();
-///
-///     boost::python::class_<VariantHolder>("VariantHolder", bp::init<>())
-///       .add_property("variant",
-///         bp::make_getter(&VariantHolder::variant,
-///                         Convertor::return_internal_reference()),
-///         bp::make_setter(&VariantHolder::variant));
-///   }
-template <typename Variant>
-struct BoostVariantConvertor {
-  typedef Variant variant_type;
-  typedef ReturnInternalBoostVariant<variant_type> return_internal_reference;
-
-  static void registration() {
-    typedef details::BoostVariantValueToObject<variant_type> variant_to_value;
-    boost::python::to_python_converter<variant_type, variant_to_value>();
-    boost::mpl::for_each<typename variant_type::types>(
-        details::BoostVariantImplicitlyConvertible<variant_type>());
-  }
-};
-
-}  // namespace eigenpy
-
-#endif  // ifndef __eigenpy_utils_boost_variant_hpp__
diff --git a/include/eigenpy/variant.hpp b/include/eigenpy/variant.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..4028f856bc721c56177e019963eccdd6cb950768
--- /dev/null
+++ b/include/eigenpy/variant.hpp
@@ -0,0 +1,361 @@
+//
+// Copyright (c) 2024 INRIA
+//
+
+#ifndef __eigenpy_utils_variant_hpp__
+#define __eigenpy_utils_variant_hpp__
+
+#include "eigenpy/fwd.hpp"
+
+#include <boost/python.hpp>
+#include <boost/variant.hpp>
+#include <boost/mpl/for_each.hpp>
+#include <boost/mpl/vector.hpp>
+
+#include <type_traits>
+
+#ifdef EIGENPY_WITH_CXX17_SUPPORT
+#include <variant>
+#endif
+
+namespace eigenpy {
+
+namespace details {
+
+/// Allow to use std::variant and boost::variant with the same API
+template <typename ResultType, typename Variant>
+struct VariantVisitorType {};
+
+/// Allow to get all alternatives in a boost::mpl vector
+template <typename Variant>
+struct VariantAlternatives {};
+
+template <typename Variant>
+struct empty_variant {};
+
+template <typename T>
+struct is_empty_variant : std::false_type {};
+
+#ifdef EIGENPY_WITH_CXX17_SUPPORT
+
+/// std::variant implementation
+template <typename ResultType, typename... Alternatives>
+struct VariantVisitorType<ResultType, std::variant<Alternatives...> > {
+  typedef std::variant<Alternatives...> variant_type;
+  typedef ResultType result_type;
+
+  template <typename Visitor, typename Visitable>
+  static result_type visit(Visitor&& visitor, Visitable&& v) {
+    return std::visit(std::forward<Visitor>(visitor),
+                      std::forward<Visitable>(v));
+  }
+
+  result_type operator()(std::monostate) const {
+    return bp::incref(bp::object().ptr());  // None
+  }
+};
+
+template <typename... Alternatives>
+struct VariantAlternatives<std::variant<Alternatives...> > {
+  typedef boost::mpl::vector<Alternatives...> types;
+};
+
+template <typename... Alternatives>
+struct empty_variant<std::variant<Alternatives...> > {
+  typedef std::monostate type;
+};
+
+template <>
+struct is_empty_variant<std::monostate> : std::true_type {};
+
+#endif
+
+/// boost::variant implementation
+template <typename ResultType, typename... Alternatives>
+struct VariantVisitorType<ResultType, boost::variant<Alternatives...> >
+    : boost::static_visitor<ResultType> {
+  typedef boost::variant<Alternatives...> variant_type;
+  typedef ResultType result_type;
+
+  template <typename Visitor, typename Visitable>
+  static result_type visit(Visitor&& visitor, Visitable&& visitable) {
+    return std::forward<Visitable>(visitable).apply_visitor(visitor);
+  }
+
+  result_type operator()(boost::blank) const {
+    return bp::incref(bp::object().ptr());  // None
+  }
+};
+
+template <typename... Alternatives>
+struct VariantAlternatives<boost::variant<Alternatives...> > {
+  typedef typename boost::variant<Alternatives...>::types types;
+};
+
+template <typename... Alternatives>
+struct empty_variant<boost::variant<Alternatives...> > {
+  typedef boost::blank type;
+};
+
+template <>
+struct is_empty_variant<boost::blank> : std::true_type {};
+
+/// Convert None to a {boost,std}::variant with boost::blank or std::monostate
+/// value
+template <typename Variant>
+struct EmptyConvertible {
+  static void registration() {
+    bp::converter::registry::push_back(convertible, construct,
+                                       bp::type_id<Variant>());
+  }
+
+  // convertible only for None
+  static void* convertible(PyObject* obj) {
+    return (obj == Py_None) ? obj : nullptr;
+  };
+
+  // construct in place
+  static void construct(PyObject*,
+                        bp::converter::rvalue_from_python_stage1_data* data) {
+    void* storage =
+        reinterpret_cast<bp::converter::rvalue_from_python_storage<Variant>*>(
+            data)
+            ->storage.bytes;
+    new (storage) Variant(typename empty_variant<Variant>::type());
+    data->convertible = storage;
+  };
+};
+
+/// Implement convertible and expected_pytype for bool, integer and float
+template <typename T, class Enable = void>
+struct NumericConvertibleImpl {};
+
+template <typename T>
+struct NumericConvertibleImpl<
+    T, typename std::enable_if<std::is_same<T, bool>::value>::type> {
+  static void* convertible(PyObject* obj) {
+    return PyBool_Check(obj) ? obj : nullptr;
+  }
+
+  static PyTypeObject const* expected_pytype() { return &PyBool_Type; }
+};
+
+template <typename T>
+struct NumericConvertibleImpl<
+    T, typename std::enable_if<!std::is_same<T, bool>::value &&
+                               std::is_integral<T>::value>::type> {
+  static void* convertible(PyObject* obj) {
+    // PyLong return true for bool type
+    return (PyLong_Check(obj) && !PyBool_Check(obj)) ? obj : nullptr;
+  }
+
+  static PyTypeObject const* expected_pytype() { return &PyLong_Type; }
+};
+
+template <typename T>
+struct NumericConvertibleImpl<
+    T, typename std::enable_if<std::is_floating_point<T>::value>::type> {
+  static void* convertible(PyObject* obj) {
+    return PyFloat_Check(obj) ? obj : nullptr;
+  }
+
+  static PyTypeObject const* expected_pytype() { return &PyFloat_Type; }
+};
+
+/// Convert numeric type to Variant without ambiguity
+template <typename T, typename Variant>
+struct NumericConvertible {
+  static void registration() {
+    bp::converter::registry::push_back(
+        &convertible, &bp::converter::implicit<T, Variant>::construct,
+        bp::type_id<Variant>()
+#ifndef BOOST_PYTHON_NO_PY_SIGNATURES
+            ,
+        &expected_pytype
+#endif
+    );
+  }
+
+  static void* convertible(PyObject* obj) {
+    return NumericConvertibleImpl<T>::convertible(obj);
+  }
+  static PyTypeObject const* expected_pytype() {
+    return NumericConvertibleImpl<T>::expected_pytype();
+  }
+};
+
+/// Convert {boost,std}::variant<class...> alternative to a Python object.
+/// This converter copy the alternative.
+template <typename Variant>
+struct VariantValueToObject : VariantVisitorType<PyObject*, Variant> {
+  typedef VariantVisitorType<PyObject*, Variant> Base;
+  typedef typename Base::result_type result_type;
+  typedef typename Base::variant_type variant_type;
+
+  static result_type convert(const variant_type& v) {
+    return Base::visit(VariantValueToObject(), v);
+  }
+
+  template <typename T>
+  result_type operator()(T& t) const {
+    return bp::incref(bp::object(t).ptr());
+  }
+
+  using Base::operator();
+};
+
+/// Trait to detect if T is a class or an union
+template <typename T>
+struct is_class_or_union
+    : std::integral_constant<bool, std::is_class<T>::value ||
+                                       std::is_union<T>::value> {};
+
+/// Trait to remove cvref and call is_class_or_union
+template <typename T>
+struct is_class_or_union_remove_cvref
+    : is_class_or_union<typename std::remove_cv<
+          typename std::remove_reference<T>::type>::type> {};
+
+/// Convert {boost,std}::variant<class...> alternative reference to a Python
+/// object. This converter return the alternative reference. The code that
+/// create the reference holder is taken from \see
+/// bp::to_python_indirect.
+template <typename Variant>
+struct VariantRefToObject : VariantVisitorType<PyObject*, Variant> {
+  typedef VariantVisitorType<PyObject*, Variant> Base;
+  typedef typename Base::result_type result_type;
+  typedef typename Base::variant_type variant_type;
+
+  static result_type convert(const variant_type& v) {
+    return Base::visit(VariantRefToObject(), v);
+  }
+
+  template <typename T,
+            typename std::enable_if<!is_class_or_union_remove_cvref<T>::value,
+                                    bool>::type = true>
+  result_type operator()(T t) const {
+    return bp::incref(bp::object(t).ptr());
+  }
+
+  template <typename T,
+            typename std::enable_if<is_class_or_union_remove_cvref<T>::value,
+                                    bool>::type = true>
+  result_type operator()(T& t) const {
+    return bp::detail::make_reference_holder::execute(&t);
+  }
+
+  /// Copy the object when it's None
+  using Base::operator();
+};
+
+/// Converter used in \see ReturnInternalVariant.
+/// This is inspired by \see bp::reference_existing_object.
+/// It will call \see VariantRefToObject to extract the alternative
+/// reference.
+template <typename Variant>
+struct VariantConverter {
+  typedef Variant variant_type;
+
+  template <class T>
+  struct apply {
+    struct type {
+      PyObject* operator()(const variant_type& v) const {
+        return VariantRefToObject<variant_type>::convert(v);
+      }
+
+#ifndef BOOST_PYTHON_NO_PY_SIGNATURES
+      PyTypeObject const* get_pytype() const {
+        return bp::converter::registered_pytype<variant_type>::get_pytype();
+      }
+#endif
+    };
+  };
+};
+
+/// Convert an Alternative type to a Variant
+template <typename Variant>
+struct VariantConvertible {
+  typedef Variant variant_type;
+
+  template <class T, typename std::enable_if<is_empty_variant<T>::value,
+                                             bool>::type = true>
+  void operator()(T) {
+    EmptyConvertible<variant_type>::registration();
+  }
+
+  template <class T, typename std::enable_if<!is_empty_variant<T>::value &&
+                                                 std::is_arithmetic<T>::value,
+                                             bool>::type = true>
+  void operator()(T) {
+    NumericConvertible<T, variant_type>::registration();
+  }
+
+  template <class T, typename std::enable_if<!is_empty_variant<T>::value &&
+                                                 !std::is_arithmetic<T>::value,
+                                             bool>::type = true>
+  void operator()(T) {
+    bp::implicitly_convertible<T, variant_type>();
+  }
+};
+
+}  // namespace details
+
+/// Variant of \see bp::return_internal_reference that
+/// extract {boost,std}::variant<class...> alternative reference before
+/// converting it into a PyObject
+template <typename Variant>
+struct ReturnInternalVariant : bp::return_internal_reference<> {
+  typedef Variant variant_type;
+
+  typedef details::VariantConverter<variant_type> result_converter;
+
+  template <class ArgumentPackage>
+  static PyObject* postcall(ArgumentPackage const& args_, PyObject* result) {
+    // Don't run return_internal_reference postcall on primitive type
+    if (PyLong_Check(result) || PyBool_Check(result) || PyFloat_Check(result)) {
+      return result;
+    }
+    return bp::return_internal_reference<>::postcall(args_, result);
+  }
+};
+
+/// Define a defaults converter to convert a {boost,std}::variant alternative to
+/// a Python object by copy and to convert implicitly an alternative to a
+/// {boost,std}::variant.
+///
+/// Example:
+///
+///   typedef boost::variant<Struct1, Struct2> MyVariant;
+///   struct VariantHolder {
+///     MyVariant variant;
+///   };
+///   ...
+///   void expose() {
+///     bp::class_<Struct1>("Struct1", bp::init<>());
+///     bp::class_<Struct2>("Struct1", bp::init<>())
+///     typedef eigenpy::VariantConverter<MyVariant> Converter;
+///     Converter::registration();
+///
+///     bp::class_<VariantHolder>("VariantHolder", bp::init<>())
+///       .add_property("variant",
+///         bp::make_getter(&VariantHolder::variant,
+///                         Converter::return_internal_reference()),
+///         bp::make_setter(&VariantHolder::variant));
+///   }
+template <typename Variant>
+struct VariantConverter {
+  typedef Variant variant_type;
+  typedef ReturnInternalVariant<variant_type> return_internal_reference;
+
+  static void registration() {
+    typedef details::VariantValueToObject<variant_type> variant_to_value;
+    typedef typename details::VariantAlternatives<variant_type>::types types;
+
+    bp::to_python_converter<variant_type, variant_to_value>();
+    boost::mpl::for_each<types>(details::VariantConvertible<variant_type>());
+  }
+};
+
+}  // namespace eigenpy
+
+#endif  // ifndef __eigenpy_utils_variant_hpp__
diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt
index c65c6cf1995d436de6a614ca141551e3f851d4f8..e1356f6d4d4ca6557cf4f906b5445d99a8660138 100644
--- a/unittest/CMakeLists.txt
+++ b/unittest/CMakeLists.txt
@@ -45,24 +45,33 @@ add_lib_unit_test(std_vector)
 add_lib_unit_test(std_array)
 add_lib_unit_test(std_pair)
 add_lib_unit_test(user_struct)
-add_lib_unit_test(boost_variant)
 
-function(config_bind_optional tagname opttype)
-  set(MODNAME bind_optional_${tagname})
-  set(OPTIONAL ${opttype})
-  configure_file(bind_optional.cpp.in ${MODNAME}.cpp)
+function(config_test test tagname opttype)
+  set(MODNAME ${test}_${tagname})
+  set(TEST_TYPE ${opttype})
+  configure_file(${test}.cpp.in ${CMAKE_CURRENT_BINARY_DIR}/${MODNAME}.cpp)
 
-  set(py_file test_optional_${tagname}.py)
-  configure_file(python/test_optional.py.in
+  set(py_file test_${test}_${tagname}.py)
+  configure_file(python/test_${test}.py.in
                  ${CMAKE_CURRENT_BINARY_DIR}/python/${py_file})
   add_lib_unit_test(${MODNAME})
-  add_python_unit_test("py-optional-${tagname}" "unittest/python/${py_file}"
-                       "unittest")
+  set(PYTHON_TEST_NAME "py-${test}-${tagname}")
+  add_test(NAME ${PYTHON_TEST_NAME}
+           COMMAND ${PYTHON_EXECUTABLE}
+                   "${CMAKE_CURRENT_BINARY_DIR}/python/${py_file}")
+  compute_pythonpath(ENV_VARIABLES "unittest")
+  set_tests_properties(${PYTHON_TEST_NAME} PROPERTIES ENVIRONMENT
+                                                      "${ENV_VARIABLES}")
 endfunction()
 
-config_bind_optional(boost "boost::optional")
+config_test(variant boost "boost::variant")
+if(CMAKE_CXX_STANDARD GREATER 14 AND CMAKE_CXX_STANDARD LESS 98)
+  config_test(variant std "std::variant")
+endif()
+
+config_test(bind_optional boost "boost::optional")
 if(CMAKE_CXX_STANDARD GREATER 14 AND CMAKE_CXX_STANDARD LESS 98)
-  config_bind_optional(std "std::optional")
+  config_test(bind_optional std "std::optional")
 endif()
 
 add_lib_unit_test(bind_virtual_factory)
@@ -133,10 +142,6 @@ add_python_unit_test("py-user-struct" "unittest/python/test_user_struct.py"
                      "python;unittest")
 set_tests_properties("py-user-struct" PROPERTIES DEPENDS ${PYWRAP})
 
-add_python_unit_test("py-boost-variant" "unittest/python/test_boost_variant.py"
-                     "python;unittest")
-set_tests_properties("py-boost-variant" PROPERTIES DEPENDS ${PYWRAP})
-
 add_python_unit_test("py-bind-virtual" "unittest/python/test_bind_virtual.py"
                      "python;unittest")
 set_tests_properties("py-bind-virtual" PROPERTIES DEPENDS ${PYWRAP})
diff --git a/unittest/bind_optional.cpp.in b/unittest/bind_optional.cpp.in
index 844449b19e0437cdd5b7d6248586d74f4e43800a..30b9ac249f9d5d4844cd30ad35c053265af859de 100644
--- a/unittest/bind_optional.cpp.in
+++ b/unittest/bind_optional.cpp.in
@@ -8,7 +8,8 @@
 #include <optional>
 #endif
 
-#cmakedefine OPTIONAL @OPTIONAL@
+#cmakedefine TEST_TYPE @TEST_TYPE@
+#define OPTIONAL TEST_TYPE
 
 typedef eigenpy::detail::nullopt_helper<OPTIONAL> none_helper;
 static auto OPT_NONE = none_helper::value();
@@ -74,6 +75,7 @@ BOOST_PYTHON_MODULE(@MODNAME@) {
           bp::make_setter(&mystruct::msg));
 
   bp::def("none_if_zero", none_if_zero, bp::args("i"));
-  bp::def("create_if_true", create_if_true, (bp::arg("flag"), bp::arg("b") = OPT_NONE));
+  bp::def("create_if_true", create_if_true,
+          (bp::arg("flag"), bp::arg("b") = OPT_NONE));
   bp::def("random_mat_if_true", random_mat_if_true, bp::args("flag"));
 }
diff --git a/unittest/boost_variant.cpp b/unittest/boost_variant.cpp
deleted file mode 100644
index 096b69a489f532a006c31b745bcb75fe10eaa043..0000000000000000000000000000000000000000
--- a/unittest/boost_variant.cpp
+++ /dev/null
@@ -1,41 +0,0 @@
-/// @file
-/// @copyright Copyright 2024 CNRS INRIA
-
-#include <eigenpy/eigenpy.hpp>
-#include <eigenpy/boost-variant.hpp>
-
-namespace bp = boost::python;
-
-struct V1 {
-  int v;
-};
-struct V2 {
-  char v;
-};
-typedef boost::variant<V1, V2> MyVariant;
-
-MyVariant make_variant() { return V1(); }
-
-struct VariantHolder {
-  MyVariant variant;
-};
-
-BOOST_PYTHON_MODULE(boost_variant) {
-  using namespace eigenpy;
-
-  enableEigenPy();
-
-  bp::class_<V1>("V1", bp::init<>()).def_readwrite("v", &V1::v);
-  bp::class_<V2>("V2", bp::init<>()).def_readwrite("v", &V2::v);
-
-  typedef eigenpy::BoostVariantConvertor<MyVariant> Convertor;
-  Convertor::registration();
-
-  bp::def("make_variant", make_variant);
-
-  boost::python::class_<VariantHolder>("VariantHolder", bp::init<>())
-      .add_property("variant",
-                    bp::make_getter(&VariantHolder::variant,
-                                    Convertor::return_internal_reference()),
-                    bp::make_setter(&VariantHolder::variant));
-}
diff --git a/unittest/python/test_optional.py.in b/unittest/python/test_bind_optional.py.in
similarity index 100%
rename from unittest/python/test_optional.py.in
rename to unittest/python/test_bind_optional.py.in
diff --git a/unittest/python/test_boost_variant.py b/unittest/python/test_boost_variant.py
deleted file mode 100644
index 023b940bdc91150e97f80d9d96f142cca73fff54..0000000000000000000000000000000000000000
--- a/unittest/python/test_boost_variant.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from boost_variant import V1, V2, VariantHolder, make_variant
-
-variant = make_variant()
-assert isinstance(variant, V1)
-
-v1 = V1()
-v1.v = 10
-
-v2 = V2()
-v2.v = "c"
-
-variant_holder = VariantHolder()
-
-# Test copy from variant alternative V1 to non initialized variant
-variant_holder.variant = v1
-assert isinstance(variant_holder.variant, V1)
-assert variant_holder.variant.v == v1.v
-
-# variant_holder.variant is a copy of v1
-variant_holder.variant.v = 11
-assert v1.v != variant_holder.variant.v
-
-# Test variant_holder.variant return by reference
-# v1 reference variant_holder.variant
-v1 = variant_holder.variant
-variant_holder.variant.v = 100
-assert variant_holder.variant.v == 100
-assert v1.v == 100
-v1.v = 1000
-assert variant_holder.variant.v == 1000
-assert v1.v == 1000
-
-# Test with the second alternative type
-variant_holder.variant = v2
-assert isinstance(variant_holder.variant, V2)
-assert variant_holder.variant.v == v2.v
diff --git a/unittest/python/test_optional_boost.py b/unittest/python/test_optional_boost.py
deleted file mode 100644
index fc818739d90e89be9d41f8f26c0f1f560ef5f4b7..0000000000000000000000000000000000000000
--- a/unittest/python/test_optional_boost.py
+++ /dev/null
@@ -1,67 +0,0 @@
-import importlib
-
-bind_optional = importlib.import_module("bind_optional_boost")
-
-
-def test_none_if_zero():
-    x = bind_optional.none_if_zero(0)
-    y = bind_optional.none_if_zero(-1)
-    assert x is None
-    assert y == -1
-
-
-def test_struct_ctors():
-    # test struct ctors
-
-    struct = bind_optional.mystruct()
-    assert struct.a is None
-    assert struct.b is None
-    assert struct.msg == "i am struct"
-
-    ## no 2nd arg automatic overload using bp::optional
-    struct = bind_optional.mystruct(2)
-    assert struct.a == 2
-    assert struct.b is None
-
-    struct = bind_optional.mystruct(13, -1.0)
-    assert struct.a == 13
-    assert struct.b == -1.0
-
-
-def test_struct_setters():
-    struct = bind_optional.mystruct()
-    struct.a = 1
-    assert struct.a == 1
-
-    struct.b = -3.14
-    assert struct.b == -3.14
-
-    # set to None
-    struct.a = None
-    struct.b = None
-    struct.msg = None
-    assert struct.a is None
-    assert struct.b is None
-    assert struct.msg is None
-
-
-def test_factory():
-    struct = bind_optional.create_if_true(False, None)
-    assert struct is None
-    struct = bind_optional.create_if_true(True, None)
-    assert struct.a == 0
-    assert struct.b is None
-
-
-def test_random_mat():
-    M = bind_optional.random_mat_if_true(False)
-    assert M is None
-    M = bind_optional.random_mat_if_true(True)
-    assert M.shape == (4, 4)
-
-
-test_none_if_zero()
-test_struct_ctors()
-test_struct_setters()
-test_factory()
-test_random_mat()
diff --git a/unittest/python/test_optional_std.py b/unittest/python/test_optional_std.py
deleted file mode 100644
index 69949a44f46b3f2ea010af24a3478d0a67fb8e5f..0000000000000000000000000000000000000000
--- a/unittest/python/test_optional_std.py
+++ /dev/null
@@ -1,67 +0,0 @@
-import importlib
-
-bind_optional = importlib.import_module("bind_optional_std")
-
-
-def test_none_if_zero():
-    x = bind_optional.none_if_zero(0)
-    y = bind_optional.none_if_zero(-1)
-    assert x is None
-    assert y == -1
-
-
-def test_struct_ctors():
-    # test struct ctors
-
-    struct = bind_optional.mystruct()
-    assert struct.a is None
-    assert struct.b is None
-    assert struct.msg == "i am struct"
-
-    ## no 2nd arg automatic overload using bp::optional
-    struct = bind_optional.mystruct(2)
-    assert struct.a == 2
-    assert struct.b is None
-
-    struct = bind_optional.mystruct(13, -1.0)
-    assert struct.a == 13
-    assert struct.b == -1.0
-
-
-def test_struct_setters():
-    struct = bind_optional.mystruct()
-    struct.a = 1
-    assert struct.a == 1
-
-    struct.b = -3.14
-    assert struct.b == -3.14
-
-    # set to None
-    struct.a = None
-    struct.b = None
-    struct.msg = None
-    assert struct.a is None
-    assert struct.b is None
-    assert struct.msg is None
-
-
-def test_factory():
-    struct = bind_optional.create_if_true(False, None)
-    assert struct is None
-    struct = bind_optional.create_if_true(True, None)
-    assert struct.a == 0
-    assert struct.b is None
-
-
-def test_random_mat():
-    M = bind_optional.random_mat_if_true(False)
-    assert M is None
-    M = bind_optional.random_mat_if_true(True)
-    assert M.shape == (4, 4)
-
-
-test_none_if_zero()
-test_struct_ctors()
-test_struct_setters()
-test_factory()
-test_random_mat()
diff --git a/unittest/python/test_variant.py.in b/unittest/python/test_variant.py.in
new file mode 100644
index 0000000000000000000000000000000000000000..b019514cd112f36a2b5e17d852730afc7d757c48
--- /dev/null
+++ b/unittest/python/test_variant.py.in
@@ -0,0 +1,83 @@
+import importlib
+
+variant_module = importlib.import_module("@MODNAME@")
+V1 = variant_module.V1
+V2 = variant_module.V2
+VariantHolder = variant_module.VariantHolder
+VariantFullHolder = variant_module.VariantFullHolder
+make_variant = variant_module.make_variant
+make_variant_full = variant_module.make_variant_full
+
+variant = make_variant()
+assert isinstance(variant, V1)
+
+v1 = V1()
+v1.v = 10
+
+v2 = V2()
+v2.v = "c"
+
+variant_holder = VariantHolder()
+
+# Test copy from variant alternative V1 to non initialized variant
+variant_holder.variant = v1
+assert isinstance(variant_holder.variant, V1)
+assert variant_holder.variant.v == v1.v
+
+# variant_holder.variant is a copy of v1
+variant_holder.variant.v = 11
+assert v1.v != variant_holder.variant.v
+
+# Test variant_holder.variant return by reference
+# v1 reference variant_holder.variant
+v1 = variant_holder.variant
+variant_holder.variant.v = 100
+assert variant_holder.variant.v == 100
+assert v1.v == 100
+v1.v = 1000
+assert variant_holder.variant.v == 1000
+assert v1.v == 1000
+
+# Test with the second alternative type
+variant_holder.variant = v2
+assert isinstance(variant_holder.variant, V2)
+assert variant_holder.variant.v == v2.v
+
+# Test variant that hold a None value
+v_full = make_variant_full()
+assert v_full is None
+
+variant_full_holder = VariantFullHolder()
+
+# Test None
+v_none = variant_full_holder.variant
+assert v_none is None
+variant_full_holder.variant = None
+assert v_none is None
+
+# Test V1
+v1 = V1()
+v1.v = 10
+variant_full_holder.variant = v1
+assert variant_full_holder.variant.v == 10
+assert isinstance(variant_full_holder.variant, V1)
+# Test V1 ref
+v1 = variant_full_holder.variant
+v1.v = 100
+assert variant_full_holder.variant.v == 100
+variant_full_holder.variant = None
+
+# Test bool
+variant_full_holder.variant = True
+assert variant_full_holder.variant
+assert isinstance(variant_full_holder.variant, bool)
+
+# Test int
+variant_full_holder.variant = 3
+assert variant_full_holder.variant == 3
+assert isinstance(variant_full_holder.variant, int)
+
+# Test float
+variant_full_holder.variant = 3.14
+assert variant_full_holder.variant == 3.14
+assert isinstance(variant_full_holder.variant, float)
diff --git a/unittest/variant.cpp.in b/unittest/variant.cpp.in
new file mode 100644
index 0000000000000000000000000000000000000000..12f669937ecf760fc72d054a1187a96511f94e20
--- /dev/null
+++ b/unittest/variant.cpp.in
@@ -0,0 +1,78 @@
+/// @file
+/// @copyright Copyright 2024 CNRS INRIA
+
+#include <eigenpy/eigenpy.hpp>
+#include <eigenpy/variant.hpp>
+
+#cmakedefine TEST_TYPE @TEST_TYPE@
+#define VARIANT TEST_TYPE
+
+namespace bp = boost::python;
+
+struct V1 {
+  int v;
+};
+struct V2 {
+  char v;
+};
+typedef VARIANT<V1, V2> MyVariant;
+
+template <typename Variant>
+struct MyVariantNoneHelper {};
+
+template <typename... Alternatives>
+struct MyVariantNoneHelper<boost::variant<Alternatives...> > {
+  typedef VARIANT<boost::blank, Alternatives...> type;
+};
+
+#ifdef EIGENPY_WITH_CXX17_SUPPORT
+template <typename... Alternatives>
+struct MyVariantNoneHelper<std::variant<Alternatives...> > {
+  typedef VARIANT<std::monostate, Alternatives...> type;
+};
+#endif
+
+typedef typename MyVariantNoneHelper<VARIANT<V1, bool, int, double> >::type
+    MyVariantFull;
+
+MyVariant make_variant() { return V1(); }
+
+MyVariantFull make_variant_full() { return MyVariantFull(); }
+
+struct VariantHolder {
+  MyVariant variant;
+};
+
+struct VariantFullHolder {
+  MyVariantFull variant;
+};
+
+BOOST_PYTHON_MODULE(@MODNAME@) {
+  using namespace eigenpy;
+
+  enableEigenPy();
+
+  bp::class_<V1>("V1", bp::init<>()).def_readwrite("v", &V1::v);
+  bp::class_<V2>("V2", bp::init<>()).def_readwrite("v", &V2::v);
+
+  typedef eigenpy::VariantConverter<MyVariant> Converter;
+  Converter::registration();
+
+  bp::def("make_variant", make_variant);
+
+  boost::python::class_<VariantHolder>("VariantHolder", bp::init<>())
+      .add_property("variant",
+                    bp::make_getter(&VariantHolder::variant,
+                                    Converter::return_internal_reference()),
+                    bp::make_setter(&VariantHolder::variant));
+
+  typedef eigenpy::VariantConverter<MyVariantFull> ConverterFull;
+  ConverterFull::registration();
+  bp::def("make_variant_full", make_variant_full);
+
+  boost::python::class_<VariantFullHolder>("VariantFullHolder", bp::init<>())
+      .add_property("variant",
+                    bp::make_getter(&VariantFullHolder::variant,
+                                    ConverterFull::return_internal_reference()),
+                    bp::make_setter(&VariantFullHolder::variant));
+}