If you are struggling with AnnData to Seurat object conversion, this post is for you. Not because I found a magic function. Because I tried all the magic functions and none of them worked. What worked was exporting the AnnData object to Matrix Market format and rebuilding the Seurat object by hand.
Let me walk you through what happened.
In theory, converting between Python and R single-cell formats is a solved problem. You have packages for it. SeuratDisk reads .h5ad files. sceasy converts between AnnData and SingleCellExperiment. zellkonverter does the same through basilisk. Pick one, call a function, done.
That is the promise. Here is the reality.
I had a processed AnnData object from a Scanpy-based pipeline. Integrated, normalized, annotated. Real work had been done on this data. I needed to bring it into R as a Seurat object to run a deconvolution method that only exists in R.
I tried SeuratDisk first. It failed. I tried zellkonverter. It failed. I tried sceasy. It also failed. I tried reticulate, thinking maybe I could load the object through Python inside R and convert it on the fly. That did not work either.
The errors were not always the same, but the pattern was. These tools expect a clean, simple .h5ad file. Mine was not simple. It had layers, it had complex metadata, and the normalized counts were stored as floats that the converters could not handle cleanly.
I spent two days on this before I stopped and asked myself what I was actually doing.
This is the first thing people suggest. Just load the raw counts in R and redo the normalization and integration there.
But think about what that means. The upstream pipeline, the integration, the normalization, all of it was done carefully in Python with specific parameters and decisions. That work is not something you reproduce casually in a different language and expect to get the same result. You would introduce differences, and then your downstream analysis is not actually running on the same data anymore.
So keeping the normalized data matters. That is the whole point of the conversion. You want what was already computed, not a new version of it.
I stopped trying to make the converters work and went manual. The idea is simple. Instead of converting the whole .h5ad object at once, you export each piece separately in formats that R has no trouble reading.
First, I made a lightweight copy of the AnnData. Only the things I actually needed: the count matrix in X, the cell metadata in obs, and the gene metadata in var. No extra layers, no obsm embeddings, nothing that would confuse the export.
Then I exported the count matrix in Matrix Market (.mtx) format. This is a plain text sparse matrix format that R's Matrix package reads natively, so converting an AnnData matrix to a format Seurat can ingest is just a write and a read, no h5ad parsing involved. The key detail here: I had to be careful with how the floats were written. Scientific notation versus decimal notation matters. The normalized values that broke the h5ad converters worked fine in Matrix Market because you control the formatting.
For the metadata, I exported obs and var as CSV files. Simple, portable, no surprises.
On the R side, I read the Matrix Market file with Matrix::readMM, loaded the CSVs as data frames, and assembled a Seurat object by hand with CreateSeuratObject. Counts slot, metadata, gene names. It is not elegant, but every piece is accounted for and nothing is silently dropped.
The tools that promise one-line conversion are built for simple cases. If your AnnData has been through a real pipeline with integration and normalization, you are probably not in a simple case. And when the converter fails silently or throws cryptic errors, you can spend days debugging someone else's code. Or you can spend an afternoon doing the conversion yourself, piece by piece, and actually understand what ended up in your Seurat object.
I built a small repository with the scripts for this workflow. Python side for the export, R side for the import. If you are stuck on the same problem, clone it and adapt it. The link is below.
Sometimes the right answer is not finding the tool that does it for you. It is realizing that doing it yourself, manually, with full control, is faster and safer than trusting a black box that was not built for your data.