← back

When the conversion should be one line but it's not: AnnData to Seurat

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.

The Promise

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.

What Actually Happened

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.

Why Not Just Re-Process in R?

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.

The Resolution: AnnData to Seurat via Matrix Market

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.

What I Learned

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.

GitHub repository: MohsenRsh/anndata-to-seurat

This is part of a series where I document challenges and resolutions from my ongoing bioinformatics work. If you found this useful or have faced something similar, I'd love to hear from you. Reach out via LinkedIn.