Have a personal or library account? Click to login
mPickle: Pickle for MicroPython Cover
Open Access
|Jan 2026

Full Article

(1) Overview

Introduction

Each year, GitHub’s Octoverse Report1 highlights trends in open source. In 2024, Python emerged as the most-used programming language on GitHub, while in 2025 it moved to second place, with TypeScript taking the top spot. Despite this, Python remains dominant in AI, data science, and web development, supported by a mature ecosystem of libraries and frameworks. These trends align with the increasing deployment of Python-based workflows executed on edge devices (e.g., single board computers, microcontrollers) where adapting, optimizing, and transferring data structures across heterogeneous runtimes (i.e., CPython and MicroPython) become practical challenges.

MicroPython,2 a lightweight Python implementation for microcontrollers, bridges traditional computing and edge applications, enabling developers to transition seamlessly between resource-rich and constrained devices. However, structured data transfer remains challenging. Common formats like JSON, XML, and CSV, while human-readable and widely supported, are inefficient for complex, nested data structures common in AI models due to high memory overhead, slow parsing, and limited support for advanced structures on microcontrollers [2].

To address this, we introduce mPickle, a port of CPython’s pickle module for MicroPython. Unlike MicroPython’s existing ASCII-only pickle implementation,3 mPickle offers efficient, versatile data serialization, supporting complex Python objects (including nested structures and user-defined types), by implementing Protocol 4 functionalities [4]. This ensures seamless, resource-efficient data transfer while retaining pickle’s familiar interface and functionality. More in detail, the main benefits of mPickle are:

  • Complex data handling: it supports serialization of sophisticated data types and structures, enabling cross-runtime interoperability;

  • Efficiency: it is optimized for constrained devices, reducing memory footprint and processing overhead;

  • Edge/TinyMLOps portability: it enables cross-runtime (de)serialization of model-related artifacts for MicroPython deployments, supporting TinyMLOps-style pipelines [1];

  • Seamless and standardized integration: last but not least, it maintains compatibility with Python workflows, leveraging a familiar Python-native format to enable direct transfer and deserialization of objects across environments while fostering interoperability across tools and systems.

Implementation and architecture

mPickle is a MicroPython port of CPython’s pickle module that implements the Pickle Protocol 4 to enable binary serialization/deserialization of complex Python objects in constrained environments. In addition to supporting the Protocol 4, mPickle introduces a mapping layer that translates modules and symbol names across MicroPython and CPython, so that an object serialized in one environment can be safely reconstructed in the other without changing the original source code.

Software architecture

The CPython’s pickle module provides tools for serializing/deserializing Python objects into/from a binary format. Pickle serialization (i.e., pickling) converts Python objects like integers, strings, lists, dictionaries, and custom objects into byte streams that can be stored or transmitted. Conversely, deserialization (i.e., unpickling) reconstructs the original Python objects from these byte streams. Figure 1 depicts the serialization and deserialization processes.

jors-14-587-g1.png
Figure 1

pickle serialization and deserialization process.

This process efficiently handles nested objects, circular references, and deduplication to ensure that the serialized format remains compact. For more details on this topic, the interested reader is referred to [4, 5, 6].

For custom objects, CPython’s pickle module relies on built-in methods like __reduce__, __reduce_ex__, __new__, and __setstate__, to control serialization and reconstruction. CPython provides default implementations for these methods, which developers can override to customize the serialization process. More details on these methods can be found in Table 1.

Table 1

Summary of main methods used by CPython’s pickle module.

METHODDESCRIPTIONARGUMENTSRETURN VALUE
__reduce__Provides instructions on how to reconstruct an object during pickling. Returns a tuple that describes how to recreate the object. Typically contains a callable (e.g., function), arguments for that callable, and optionally the object’s state.NoneTuple of (callable, args, state, optionally other items)
__reduce_ex__Similar to __reduce__, but allows different pickling protocols. Takes a protocol version to decide how to serialize an object. Used to add compatibility for different pickling versions.protocol (int) – specifies the pickle protocol versionSame as __reduce__: a tuple of (callable, args, state, optionally other items)
__new__Used to create a new instance of a class without initializing it. Pickling uses __new__ to create an instance from a serialized state. It is called during unpickling before initialization.cls (type), followed by optional arguments to initialize an instanceNew instance of the class
__setstate__Allows for setting the state of an object during unpickling. Used to restore the object’s internal state from pickled data, called after the object is created via __new__.state (dict, tuple, or other serialized data) – the state of the objectNone (modifies the instance in place)

MicroPython has implementation limitations4 that require code adaptations to properly handle serialization and deserialization processes within its constrained capabilities and limited memory. To address this, in this paper, we propose mPickle, an adaptation of the pickle library for resource-limited environments. mPickle streamlines data serialization between Python/CPython and MicroPython environments by introducing multiple adaptations, enabling developers to handle the mapping of objects, modules, and functions seamlessly (e.g., without modifying the serialized object code).

Figure 2 illustrates the internal architecture of mPickle, which extends pickle Protocol 4 by integrating a module mapping mechanism directly into the serialization/deserialization pipelines. This mechanism is configured through the register_pickle() function (see Software functionalities section), which stores translation rules for modules/names into the module mapping dict. Then, when needed, it triggers inject_dummy_module_func() to create placeholder modules/functions in the MicroPython runtime. This is particularly important to overcome missing modules/functions availability and mismatched module hierarchies between CPython and MicroPython. Once registered, these rules are used to rewrite global references during pickling and to resolve classes/functions during unpickling, compensating for missing methods described in Table 1.

jors-14-587-g2.png
Figure 2

mPickle architecture and module mapping. Arrows highlight the direction of data movement: from MicroPython to Python (dashed line), and from Python to MicroPython (dotted line). Continuous lines highlight the interaction to internally register functions and modules mapping.

Interoperable serialization workflow and module mapping (MicroPython ↔ CPython)

mPickle enables the bidirectional data exchange between MicroPython and CPython. In Figure 2, dashed and dotted arrows describe the MicroPythonCPython and the CPythonMicroPython serialization/deserialization paths, respectively.

MicroPython → CPython

A MicroPython object needs to be sent to a Python environment, so the mpickle.dump method is invoked (A) and data are passed to the mPickle engine for pickling. During name/module resolution (B), the whichmodule() function queries the MicroPython runtime (C); if the referenced module/function cannot be identified, whichmodule() consults the registered module mapping dict (D) to translate dotted paths (e.g., ulab.numpy is mapped to numpy). Then, the resulting byte stream is transferred to CPython (E), where native pickle deserializes it (F) via pickle.load.

CPython → MicroPython

A Python object is serialized on CPython (1) using the pickle.dump method. The bytestream is transferred to the target MicroPython environment that invokes mpickle.load (2) to initiate the deserialization process. During the unpickling phase, mPickle internally invokes the find_class() function (3) to locate the class/function needed for object reconstruction. It first queries the MicroPython runtime (4). If resolution fails, find_class() applies the registered translations available in the module mapping dictionary (5). When required, it relies on dummy namespaces previously injected via inject_dummy_module_func() to satisfy expected dotted paths. Finally, the object is reconstructed (6) as a MicroPython object with the desired structure (e.g., a Numpy NDArray).

Adaptations for MicroPython compatibility

To implement the workflow above on MicroPython, mPickle introduces the following changes:

  • MicroPython does not implement the methods described in Table 1. Therefore, we provide a __reduce__-like implementation inspired by CPython default behavior, but without directly calling the original method. For custom objects, developers can register a dedicated reduce function via register_pickle(), along with custom reconstructor by providing a custom (or dummy) function through a custom dotted path, improving interoperability with Python (see Software functionalities section). This does not apply to built-in data types such as int, float, dict, list, tuple, set, str, type, bool, complex, functions, range.

  • The whichmodule function, used during the pickling phase to determine an object’s module name, has been adapted for MicroPython to handle built-in objects, functions, and custom classes that lack the __module__ attribute. It also supports mapping module names (e.g., ulab.numpy to numpy) to resolve library interoperability issues, ensuring compatibility between MicroPython and standard Python environments, as described in the Software functionalities section.

  • The find_class method of the _Unpickler class is used during the unpickling phase to dynamically reconstruct objects that were serialized using Python’s pickle mechanism. Its main responsibility is to locate and return the original class or function required to recreate the serialized object during the unpickling process. It also supports custom mappings to dynamically resolve classes and functions, allowing objects serialized in a standard Python environment to be correctly reconstructed (e.g., a numpy array into a ulab.numpy array) in MicroPython.

  • The load_build method of the _Unpickler class restores an object’s state during unpickling by calling its __setstate__ method, if available. In MicroPython, where __setstate__ is not supported, a fallback mechanism is used. This fallback either invokes a developer-provided custom function mapped to the object type or assigns the state directly to the object’s __dict__. If direct dictionary updates are not feasible, setattr is used for attribute assignment. Slot-based state, if present, is handled with setattr to ensure compatibility, as described in the Software functionalities section.

  • Other minimal code adaptations to make the code compatible with MicroPython: extended the BytesIO class (named uBytesIO) available to add the getbuffer (available in CPython) method and return a memoryview, protected the __new__ calls in the load_newobj and load_newobj_ex methods of _Unpickler with a try/catch block since it can only take one argument (i.e., the class), removed the level argument from __import__ functions since only the module_name argument is available, removed not available functions like sys.audit (audit hooks used to study the code). Moreover, we addressed integer compatibility issues since MicroPython lacks to_bytes, from_bytes, and bit_length methods, thus, we provided the equivalent helper functions to ensure the correct handling. Finally, we fixed the two’s complement handling for signed integers.

mPickle module structure

mPickle is packaged and distributed as a MicroPython module comprising multiple Python files, namely:

  • __init__.py: this is the package initialization file exposing the mPickle APIs;

  • mpickle.py: it contains the implementation of mPickle;

  • _compat_pickle.py: file used for mapping of same Python modules from Python 2 to Python 3 naming conventions (e.g., the Python 2 module __builtin__.long is mapped to the Python 3 builtins.int module);

  • codecs.py: it implements some encoding (latin1, ASCII, UTF-8) and decoding functions;

  • copyreg.py: it provides utilities to extend the pickle module, enabling registration of additional data types, including C extension types and complex user-defined objects;

  • itertools.py: it provides utility functions to simplify the management of iterators;

  • ubytesio.py: it extends the io.BytesIO class and provides a getbuffer method that returns a memory view of the entire buffer content after seeking to the start.

Finally, mPickle depends on two external modules that are downloaded during the installation phase: types, which defines names for built-in data types that are not accessible as built-in data types, and functools, which provides utility functions to simplify functional programming tasks.

Software functionalities

mPickle provides a single entry point to configure and streamline the interoperability across CPython and MicroPython: register_pickle function, which defines how specific Python objects should be serialized and deserialized in MicroPython. As depicted in Figure 2 (solid arrows), this function registers mapping rules into an internal module mapping dictionary and, if needed, it injects dummy module/functions into the MicroPython runtime so that expected dotted paths can be resolved during (de)serialization. Registered rules will be queried by whichmodule() and find_class() during the pickling and unpickling phases, respectively.

Registering mapping rules and handlers

The register_pickle stores, for every custom data type, three sets of information: the MicroPython-side and Python-side identities and references of the target object, and optional handlers to implement missing pickling hooks in MicroPython. It registers:

  • obj_type: it defines the object type needing custom (de)serialization, such as a class or a user-defined type;

  • obj_full_name and obj_module: they uniquely identify the object by specifying its name and originating module in MicroPython;

  • map_obj_module and map_obj_full_name: they map the object to an equivalent in Python, allowing replacement when necessary;

  • obj_reconstructor_func and reconstructor_func: these arguments allow to define the custom function to reconstruct the object after deserialization. The function can be passed as a dotted path (obj_reconstructor_func) or function (reconstructor_func);

  • map_reconstructor_func: it provides the reconstruction function for Python in dotted path format;

  • reduce_func: this custom function emulates the __reduce__ method;

  • setstate_func: this custom function restores the state of an object after reconstruction, emulating the __setstate__ method.

Internal Module Mapping Mechanism in mPickle

Addressing differences in module availability and structure between Python and MicroPython environments is crucial to ensuring seamless serialization and deserialization. Modules available in one environment may be absent or structured differently in the other, creating challenges during the pickling process.

Dummy module/function injection

mPickle implements an internal mechanism using the inject_dummy_module_func function, which can be invoked directly or via register_pickle, to create placeholder modules/functions in the MicroPython runtime, replicating the module hierarchy (dotted path) expected by the target environment. These placeholders can then be referenced in the serialized byte stream, ensuring that function and module names are resolved correctly during deserialization.

Reverting module/function injection

A dummy module injection can be easily reverted, if needed, by invoking the revert_dummy_module_func function, which removes the injected module or function from the MicroPython runtime. This function, in particular, cleans up the import system by following a hierarchical approach: first, it removes the specified functions and then any empty module that was created, leaving the system as before the dummy function injection.

This approach aligns with Python’s import system, which relies on the module namespace to locate and load modules during execution. By preemptively creating these placeholder modules and functions, the deserialization process can successfully locate necessary components, even if they are not natively present in MicroPython. This method effectively bridges the gap between differing module structures, enabling smooth interoperability between Python and MicroPython for serialization and deserialization tasks.

An example, elaborated in the following section, is the mapping of NumPy5 arrays to Ulab-NumPy6 arrays, which provides a MicroPython implementation of a subset of NumPy, SciPy, and related functions. This approach ensures that serialized objects referencing NumPy in Python environments can be successfully deserialized and used in MicroPython environments, maintaining functionality and compatibility.

Quality control

Testing a library for binary data serialization, such as mPickle, requires demonstrating that it can accurately convert both simple and complex data structures to and from the binary format, even when data are serialized on a traditional machine and deserialized by MicroPython, and vice versa. To fulfill these requirements, the mPickle package includes 140+ unit tests, to verify functionalities and edge cases that may happen while the library is used, as well as six examples (four simple data serialization/deserialization examples, one complete machine learning example, and one IoT data-sink oriented example) to help developers quickly adopt our library by demonstrating how to serialize and deserialize built-in data types, custom classes, NumPy arrays, and complex hybrid data structures (e.g., NumPy arrays in native data structures). Moreover, mPickle library comes with a benchmarking suite to evaluate the serialized output size against JSON. The unit tests are located in src/tests, the examples are placed in the src/examples directory, while the benchmarking code is available in benchmarks.

For completeness, we properly present the unit tests, benchmarks, and examples in the following sections.

Unit tests

The unit tests have been specifically designed to cover and verify the main functionalities and edge cases that may happen while the mPickle library is adopted. Currently, we have developed 143 unique tests covering core functionalities (e.g., pickling/unpickling simple data types, nested structures, basic exceptions, etc.), data types (e.g., integers, float, boolean, strings, null, etc.), custom classes (e.g., with custom methods, attributes, inherited classes, empty classes, etc.), error handling and edge cases (e.g., pickling/unpickling errors, malformed data, data format, unknown data format, compatibility, long strings, etc.), integration (e.g., builtins data types, protocol compatibility, etc.), function registration and edge cases (e.g., dummy function injection and reverting, module mapping, multiple registrations, etc.), and utility functions (e.g., encoding/decoding variables and escape sequences, etc.). All of these tests have been successfully executed with the UNIX port of MicroPython and require additional dependencies such as os-path, shutil, tempfile, unittest-discover. More details about unit tests are available in the /src/tests/README.md file.

Examples

Each example follows the workflow shown in Figure 3 to validate interoperability between mPickle running on MicroPython and native Pickle running on CPython. Starting from a data object in MicroPython (Input data), the object is serialized using mpickle.dump and then stored or transmitted. On the CPython side, the data are deserialized (Output data) using pickle.load. The same serialized data can also be loaded back into MicroPython (e.g., when storing a configuration data structure) using mpickle.load. The examples also support the reverse direction: a CPython object that can be serialized with pickle.dump can be transferred to MicroPython and reconstructed using mpickle.load.

jors-14-587-g3.png
Figure 3

mPickle examples workflow. Data objects can be serialized (dumped) in MicroPython using mPickle and deserialized (loaded) in CPython using native Pickle, and vice versa.

Each example resides in its own folder with a main example.py script and any required support code. A top-level src/examples/README.md file provides instructions and prerequisites for running the tests and examples. Examples have been successfully executed on a Unix-like system running Ubuntu 24.04, as well as on various ESP32 devices, including ESP32-S3 variants. Moreover, we provide a unified script (src/examples/run_test.sh) to run the builtins-data-types, custom-class, and numpy-ndarray examples as cross-compatibility tests, streamlining both testing and demonstration. This unified run sequentially covers cross-platform scenarios, including serialization in Python with deserialization in MicroPython, and vice versa. The remaining examples are run manually as described in src/examples/README.md. All runs have been successfully executed on a Linux system using the Ubuntu 24.04 distribution.

Example 0: Hello-world

This basic example (/src/examples/hello-world/) demonstrates the process of serializing and deserializing simple data structures, such as a string and an integer. It converts a string and an integer into their corresponding binary representations and then converts them back to their original object forms. This is the simplest getting started for a practitioner who wants to adopt mPickle.

Example 1: Pickling Built-In Data Types

This example (/src/examples/builtins-data-types) demonstrates the serialization and deserialization of various Python built-in data types using the mPickle library. This highlights the mPickle’s ability to ensure compatibility across different Python environments, such as MicroPython and standard Python. The example showcases how diverse data structures, including basic types, collections, and nested structures, can be efficiently serialized into a byte stream and accurately reconstructed. The considered data types are: string, int, float, complex, bool, bytes, bytearray, list, tuple, set, dict, None, range, frozenset, nested list, nested dict in tuple.

Example 2: Pickling a Custom-Defined Class

This example (/src/examples/custom-class) illustrates how to serialize and deserialize a custom Python class using the mPickle library. It demonstrates mPickle’s capability to efficiently convert complex Python objects into a byte stream that can be stored, transferred, and reconstructed. By leveraging mPickle, developers can ensure compatibility and efficient memory handling in resource-constrained environments like MicroPython. This process preserves the structure and behavior of the original class, enabling seamless restoration of the object’s state when needed.

Example 3: Pickling a 3rd-party object: NumPy NDArray

This example (/src/examples/numpy-ndarray) demonstrates how the mPickle library can be adopted in real scientific computing ecosystems. Natively, Python extensively relies on NumPy for numerical computations and array manipulation. However, NumPy is not available in MicroPython, where the ulab library provides a lightweight alternative implementation of selected NumPy (ulab.numpy) functionalities along with some scipy functions, optimized for microcontroller environments. This divergence between Python and MicroPython environments creates challenges when serializing and deserializing numerical data structures, especially arrays, across the two platforms.

To address these challenges, this example heavily relies on the register_pickle function described in the Software functionalities section. When exporting a NumPy array, the binary serialization must capture the array elements and their data types (dtype) to ensure correct reconstruction during deserialization. This example demonstrates implementing reducing and reconstruction functions, as well as mapping package names and values for arrays and data types, to bridge the gap between Python and MicroPython numerical libraries.

Example 4: Pickling complex data structures: NumPy-based LeNet-5 weights

As an extension of the previous example on the pickling of NumPy NDArrays, this example (/src/examples/numpy-lenet5) provides a complete machine learning example covering the entire life cycle of a model, from training to evaluation and inference loop. It implements the LeNet-5 [3] convolutional neural network, which can be trained on both MicroPython and Python environments, with the trained model weights serializable using mPickle for cross-platform compatibility. Internally, the model weights are handled as a dictionary where keys are the name of the network layers while the actual weights are NumPy NDArrays. As before, this example heavily makes use of the register_pickle function to properly deliver the serialization and deserialization processes. The example comes with multiple scripts to support the different phase of the model life cycle:

  • Training script: a complete script to generate MNIST-like random digits, train the LeNet-5 network, evaluate it, and store its weights in the flash using mPickle.

  • Evaluation script: a script to generate MNIST-like random digits and evaluate the LeNet-5 network with the model weights loaded using mPickle.

  • Model loading script: it loads weights using mPickle and verifies them.

  • Model storing script: it generates random weights and stores them using mPickle.

  • Inference demo: it uses mPickle to load weights and runs an infinite inference loop by generating random digits and feeding the model. This is the main example file.

This example demonstrates end-to-end cross-platform (de)serialization of a model-weight dictionary (layer name → NDArray) between CPython and MicroPython, supporting Edge AI workflows on MicroPython targets without requiring any microcontroller-specific model conversion beyond the lightweight mapping and handlers described above.

Example 5: Pickling data streams: IoT sensor streaming

Since MicroPython has been designed to run on microcontrollers, which commonly are adopted as computing units in IoT devices, this example (/src/examples/iot-datastream) demonstrates how mPickle can be used as packaging format for sensors data streams, enabling a seamless data interoperability among heterogeneous computing environments. More in detail, this example includes:

  • MicroPython Client: this MicroPython scripts simulates an IoT device that collects sensor data (random temperature, humidity, battery values) and sends it to a server using both mPickle and JSON formats.

  • CPython Client: this script is the CPython equivalent client that simulates an IoT device that collects sensor data (random temperature, humidity, battery values) and sends it to a server using both native pickle and JSON formats.

  • CPython Flask Server: this CPython script implements a Flask-based server that exposes data-sink APIs for collecting client data in pickle-compatible and JSON formats. It also provides statistics on the received data and a complete list of all received readings.

This example is particularly useful for developers and practitioners working on IoT applications where data interoperability usually plays a critical role.

Benchmarking

The performance evaluation of a serialization library such as mPickle is strictly related to the specific use case in which it is adopted, which makes broad generalization difficult. Among the available alternatives, JSON (JavaScript Object Notation) is one of mPickle’s main competitor. JSON is a widely adopted human-readable text format for data interchange and it relies heavily on key-value pairs and arrays to structure data.

In this paper, we evaluate mPickle against JSON in scenarios where simple data structures, like lists (list) and dictionaries (dict), must be serialized. In particular, we measure the size (in bytes) of serialized output produced by JSON and mPickle for data structures ranging from 1 to 2000 elements. We run separate experiments for four different element types: int (uniformly random in [0;10000)), float (uniformly random from [0.0;10000.0)), mixed (randomly alternating int and float values drawn from the same range), and int sequences (values from 0 to 10000.0). For dict objects, we generated key-value pairs in which the value is the generated element and the key is named key_id, where id denotes the element index. We perform 1,000 runs per experiment, indexing them with run_id (0 to 999). For each run, we set the random seed to 42 + 100 × run_id, then average the output sizes across runs. This allows us to determine the data structure size, called break-even point, at which mPickle becomes more space efficient than JSON.

In order to speed-up the benchmarking process, we applied a binary search strategy to identify faster the break-even point. Starting from the extremes, 1 and 2000 elements, we measure the serialized sizes at both ends to establish which format, JSON or mPickle, is smaller. We then evaluate the midpoint and iteratively we narrow the search interval by keeping the half in which the break-even point can still lie: the lower half if JSON is smaller at the lower bound while mPickle is smaller at the midpoint, or the upper half if mPickle is smaller at the midpoint while JSON is smaller at the upper bound. We stop the search when the lower and upper bounds collapse to a single size, which is reported as break-even point. Additionally, once we identify the break-even point, we evaluate all data structures with sizes ranging from 1 up to twice the break-even-point value to explore the transition more clearly. Finally, all the serialization experiments are carried out on MicroPython (UNIX port) to ensure a fair comparison. It is worth noting that MicroPython’s JSON dump function does not allow to set the indentation, thus the output is generated without pretty-printing (i.e., no additional white spaces that could unnecessarily increase the output size are introduced). Moreover, JSON strings are UTF-8 encoded.

Figure 4 compares the average serialized output sizes produced by JSON and mPickle for lists (Figure 4a) and dictionaries (Figure 4b). In particular, the break-even point occurs between 5 and 20 elements for lists and between 5 and 10 for dictionaries (see Table 2). This indicates that mPickle becomes more space-efficient than JSON already for small data structures, and the gap increases even more for bigger data structures.

jors-14-587-g4.png
Figure 4

Comparison of serialized output sizes for JSON (dotted lines) and mPickle (continuous lines). Dots are the break-even points. The zoom-in plots highlight the break-even points regions, where mPickle becomes more efficient than JSON. (a) and (b) show the output sizes for lists and dictionaries, respectively. Finally, lower values are better.

Table 2

Break-Even points and space saving (percentage).

STRUCTURE TYPEDATA TYPEBREAK-EVEN STRUCTURE SIZESAVING AT BREAK-EVENMAX SAVINGS (PERCENTAGE)MAX SAVINGS STRUCTURE SIZE (ELEMENTS)
dictfloat90.579.021000
listfloat190.468.912000
dictinteger54.8123.65125
listinteger64.2349.342000
dictmixed61.5215.38125
listmixed91.6824.02000
dictsequential91.8523.12250
listsequential144.3454.74250

This trend is even more highlighted by Figure 5, which reports the space saving ratio as the number of element in data structures increases. For lists, mPickle achieves space saving up to 54% when serializing a list of sequential integer numbers, while up to 9% for a list of floating numbers. On the dictionary side, we can save up 23.65% when serializing integers and 23.1% while serializing sequential integers. As expected, savings are generally smaller for dictionaries than for lists, since dictionaries also require storing the keys, in addition to the values.

jors-14-587-g5.png
Figure 5

Space saving in percentage of mPickle vs JSON. Negative values mean JSON is more efficient, while positive values mean mPickle is better. (a) and (b) show the space saving for lists and dictionaries, respectively. The dashed line indicates the break-even boundary. Finally, higher values are better.

(2) Availability

Operating system

MicroPython ≥1.25.0 on Unix-like or on a compatible microcontroller

Programming language

MicroPython ≥1.25.0 (compatible with Python 3.4)

Additional system requirements

A MicroPython-compatible device is required to run the code on an embedded target.

List of contributors

Mattia Antonini (main contributor)

Software location

Archive

Name: mPickle

Persistent identifier: 10.5281/zenodo.18036136

Licence: MIT License

Publisher: Mattia Antonini

Version published: v0.2.1

Date published: 08/01/26

Code repository

Name: mPickle

Persistent identifier: https://github.com/FBK-OpenIoT/mPickle

Licence: MIT License

Date published: 11/06/25 (v0.1.0), 23/12/25 (v0.2.0), 08/01/26 (v0.2.1)

Emulation environment

Name: MicroPython

Persistent identifier: https://micropython.org/

Licence: MIT License

Language

English

(3) Reuse potential

The mPickle library provides a practical solution for binary serialization and deserialization of Python objects within MicroPython environments, enabling interoperable data exchange with CPython. It supports a wide range of data structures, including nested structures and custom objects, and can be used to transfer or persist structured artifacts such as configurations, model parameters, metadata, and logs on constrained environments. mPickle reduces the need for ad-hoc converters and custom wire formats, enhancing reproducibility and lowering integration effort across heterogeneous edge and IoT systems. Table 3 summarizes the reuse scenarios and maps them on the provided examples.

Table 3

Main reuse scenarios enabled by mPickle across CPython (CP) and MicroPython (MP), mapped to the examples included.

REUSE SCENARIOWHAT IS SERIALIZEDDIRECTIONWHERE SHOWN
Core interoperabilitySimple & built-in data types, nested structuresMP ↔ CPEx. 0 and 1
Custom objectsUser-defined classes and object stateMP ↔ CPEx. 2
Embedded systemsConfigurations, structured runtime state, parametersMP ↔ CPEx. 0, 1 and 2
Education and prototypingDidactic examples, small projects, structured data exchangeMP ↔ CPythonEx. 0, 1 and 2
Numerical computingNumPy arrays ↔ ulab arrays (via mapping)MP ↔ CPEx. 3
Edge AI artifactsModel weights/layer params as dict of arraysCP → MP (typical)Ex. 4
IoT integrationSensor readings, batched records, device logsMP/CP → serverEx. 5
Data science/experimentationStructured objects and arrays moved to embedded targetsCP → MP (common)Ex. 3, 4, and 5

Reuse in Edge AI and TinyMLOps

A primary reuse of mPickle is in Edge AI development, where experimentation and training commonly occur in CPython, while the deployment may target microcontrollers running MicroPython. In this context, mPickle simplifies cross-platform data management by providing a Pythonic serialization interface that abstracts away low-level implementation details, supports portable exchange of structured artifacts across heterogeneous environments, and enhancing developer productivity. In particular, mPickle supports the package phase of the TinyMLOps methodology by enabling the transfer of model-related artifacts (e.g., weights, logs, metadata) without requiring any platform-specific data formats. The examples presented in this paper illustrate this workflow (the develop–package–deploy–monitor pipeline) for numerical arrays and model weights. Moreover, the binary format of serialized output reduces the storage and bandwidth requirements, which are commonly limited resources in edge computing scenarios.

Other reuse scenarios

Beyond ML-oriented workflows, mPickle is useful whenever it is necessary to persist or exchange complex data structures. This includes multiple scenarios in many different research and engineering fields:

  • Embedded Systems: facilitates software prototyping and deployment on microcontrollers for applications in robotics, automation, and remote monitoring.

  • Education and Prototyping: provides a lightweight tool for students and educators building MicroPython-based applications, eliminating the need for manual data formatting and conversions, while keeping a Pythonic workflow.

  • IoT Integration: supports packaging and exchanging sensor streams and device logs, enabling interoperability across heterogeneous computing environments.

  • Data Science: Enables seamless transfer of serialized Python objects (including numerical structures) to embedded devices while preserving object structure and reducing time spent on custom interfaces.

Finally, as a security note, like CPython’s pickle, mPickle deserialization can execute arbitrary code; therefore, users must not load serialized data from untrusted sources.

Future Development

The project is open to contributions, and developers are invited to customize or expand its capabilities based on their needs. Potential directions for further development include:

  • enhancing compatibility with Pickle protocol 5 and other serialization standards;

  • refining performance on memory-constrained microcontroller platforms;

  • increasing integration with external MicroPython libraries.

All contributions, feedback, or issue reports are welcome through the GitHub repository. Community participation is encouraged via GitHub’s issue tracker.7 The repository includes documentation and examples to assist users and contributors.

Notes

Acknowledgements

The authors want to thank Riccardo Libanora for providing the support in preparing the delivery of the code for the submission.

Competing Interests

The authors have no competing interests to declare.

Author Contributions

Mattia Antonini – Main contributor (code, writing, review).

Massimo Vecchio – Contributor (writing, review).

Fabio Antonelli – Contributor (funding, review).

DOI: https://doi.org/10.5334/jors.587 | Journal eISSN: 2049-9647
Language: English
Submitted on: Jun 12, 2025
|
Accepted on: Dec 30, 2025
|
Published on: Jan 20, 2026
Published by: Ubiquity Press
In partnership with: Paradigm Publishing Services
Publication frequency: 1 issue per year

© 2026 Mattia Antonini, Massimo Vecchio, Fabio Antonelli, published by Ubiquity Press
This work is licensed under the Creative Commons Attribution 4.0 License.