emlearn: converting scikit-learn and Keras models into C99 for microcontrollers
Machine Learning inference engine for Microcontrollers and Embedded devices
At a glance
- What is it?
- emlearn is a Python library that turns trained scikit-learn and Keras estimators into portable C99 code with no dynamic allocation. The mechanism is sound and the model coverage is narrow and explicit, which is the main thing to weigh before adopting it.
- Who is it for?
- Adopt emlearn if your model is a tree ensemble, a fully-connected network, a Gaussian Naive Bayes, or one of the Gaussian mixture and EllipticEnvelope detectors, and your target has a C99 compiler. Do not adopt it if your model is a CNN or anything needing convolution, pooling or tensor ops, because the model support table lists none of those.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 60 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The gap emlearn fills between a trained estimator and a device with no Python
A model trained in scikit-learn or Keras lives as Python objects, arrays of floats and library-specific structures. A Cortex-M or AVR target has none of that. The usual workarounds are porting a runtime, which brings an interpreter or a tensor library and its memory footprint, or hand-porting the model, which means reimplementing the traversal logic and hoping the two implementations agree. emlearn takes a third route: it reads the fitted estimator and emits C source that reproduces that estimator's decision function. The README states the split plainly, train in Python, then do inference on any device with a C99 compiler. The audience is embedded developers who already have a Python training pipeline and want the deployment step to be a code generation step rather than a runtime port. It is not a training framework and not a general tensor compiler.
What gets converted, and what the model support table excludes
The README lists supported estimators by name. For classification: eml_trees covers sklearn.RandomForestClassifier, ExtraTreesClassifier and DecisionTreeClassifier; eml_net covers sklearn.MultiLayerPerceptron and Keras.Sequential built from fully-connected layers; eml_bayes covers GaussianNaiveBayes. For regression: eml_trees covers RandomForestRegressor, ExtraTreesRegressor and DecisionTreeRegressor, and eml_net covers Keras.Sequential with fully-connected layers. For unsupervised and outlier detection: eml_distance covers EllipticEnvelope via Mahalanobis distance, and eml_mixture covers GaussianMixture and BayesianGaussianMixture. That list is the whole surface. There is no convolutional layer support, no recurrent layer support, no gradient boosting entry, and no generic ONNX import mentioned. If your model is not one of those estimator classes, emlearn.convert has nothing to read. This is the first thing to check, and it takes one call to check it.
How conversion works: estimator in, C header out
The pipeline has three steps in the README. First you fit an estimator in Python as normal. Then you call emlearn.convert(estimator, method='inline') and save the result with cmodel.save(file='sonar.h', name='sonar'). The method argument selects the code shape. With 'inline' the generated header contains the prediction function itself, so the call site is sonar_predict(values, length). The README also shows the alternative, using the generated data structure directly through eml_trees_predict(&sonar, length), which suggests the converted model is also emitted as a struct you can pass around. The name argument becomes the prefix for the generated symbols, so two models converted with the same name in one translation unit will collide. The generated file is a header, not a compiled object, which is why integration is a copy step rather than a link step. For a neural net the README says to copy the generated .h alongside eml_net.h and eml_common.h, then call nnmodel_regress1(values, 6) for a single float output or nnmodel_regress(values, 6, out, 2) for multiple outputs, the latter returning an EmlError you compare against EmlOk.
The embedded constraints emlearn is designed around
The README claims portable C99, no dynamic allocations, code size from 2kB FLASH and RAM from 50 bytes. Those are the project's own figures and they are lower bounds for small models, not a promise about yours. The design choices behind them are visible in the API. Returning an EmlError code instead of throwing or aborting means the generated code can signal failure without a runtime. The README states that some models need no libc, and that integer and fixed-point math are supported for some models, which implies other models are float-only. Which models fall on which side of that line is not spelled out in the README; the documentation pages for classification, regression and the tree-based models are where that distinction would live. If your target is an 8-bit AVR with no FPU, confirm the fixed-point path for your specific estimator before you build anything around it.
Integration paths: single header, Arduino, Zephyr, MicroPython
The README describes four ways in. The base case is a single header file to include, with a C API that the project notes makes it easy to embed in other languages. For Arduino there is a packaged library, and for Zephyr a packaged module, each with a getting-started page in the docs. For MicroPython the bindings live in a separate repository, emlearn-micropython, so that path adds a second dependency to track. The C API being the common denominator matters for teams that are not writing C: a Rust, C++ or MicroPython layer can call the generated predict function without emlearn generating anything language-specific. The trade-off is that the generated symbols are plain C functions with no namespacing beyond the name prefix you supply, so a large project with several models needs a naming convention of its own.
Where emlearn is the wrong tool
The clearest failure case is a model shape outside the support table. A Keras.Sequential with a Conv2D layer is not covered: the README says fully-connected layers, and lists no convolution, pooling or flattening entry. The second case is a target without a working C99 compiler. The README says emlearn should work anywhere a C99 compiler exists, but that is a statement about the generated code, not about an environment where you cannot build C at all. The third case is model size. The 2kB FLASH figure is the project's floor; a random forest with many deep trees converted to inline code expands the tree into the header, and the README's own validation tooling exists precisely because size is a variable, not a constant. If your flash budget is tight, the hyperparameter and feature quantization examples in the docs are the intended way to explore the trade-off between accuracy and size. There is also no mention of a runtime that adapts to drift or retrains on device, so emlearn is a deployment path, not an online learning system.
How emlearn differs from TensorFlow Lite for Microcontrollers
The obvious alternative for TinyML is TensorFlow Lite for Microcontrollers, which runs a model file through an interpreter with a fixed arena. The difference in approach is structural. TFLite Micro ships a runtime plus an operator library, and you control memory by sizing the arena; emlearn ships no runtime at all, because the model logic is emitted as C functions and structs you compile into your firmware. That means emlearn's flash cost is your model's cost with no interpreter overhead, and its RAM cost is whatever the generated structures need, with no arena to size. The cost of that is coverage. TFLite Micro's operator set includes convolution and other tensor operations that emlearn does not list, so a vision or audio CNN that runs under TFLite Micro has no emlearn equivalent. The choice is not which is faster; it is whether your model is one of the estimator classes emlearn converts. If it is, you get a smaller and simpler deployment. If it is not, the question does not arise.
Verifying the conversion, and the maintenance cost you are taking on
emlearn ships validation tooling, and this is the part worth using rather than skipping. The README describes accessing the generated C classifier from Python to verify prediction correctness, and estimating model computational cost and size using scikit-learn compatible metrics, with linked examples for tree hyperparameters and for feature quantization. That gives you a loop: convert, call the C code from Python, compare against the estimator's own predictions, and only then move to the device. Skipping that step means trusting that the emitted traversal matches the fitted tree, which is exactly the kind of bug that shows up as a handful of misclassified samples in the field. On maintenance: the project is MIT licensed, which permits commercial and closed-source use and modification; the file is LICENSE.md in the repository, and if your organisation has licence review, that is the file to read rather than a summary. The README states the project has been actively maintained since 2018, and the release history shows 0.21.1 in April 2025 after 0.20.4 in April 2024 and 0.17.1 in 2023, so expect roughly annual minor releases rather than a fast cadence. The generated code is yours to keep, so a stalled project would not break an existing deployment, but new estimator support would stop arriving. The citation block in the README lists three authors, Jon Nordby, Mark Cooke and Adam Horvath, which is a small enough group that you should not assume a large contributor bench behind the support table.
Editorial conclusion
Adopt emlearn if your model is a tree ensemble, a fully-connected network, a Gaussian Naive Bayes, or one of the Gaussian mixture and EllipticEnvelope detectors, and your target has a C99 compiler. Do not adopt it if your model is a CNN or anything needing convolution, pooling or tensor ops, because the model support table lists none of those. Before committing, convert your actual estimator and check two things: that emlearn.convert accepts it, and that you can call the generated predict function from a host build so you can compare its output against the Python estimator's predictions.
Community notes