Scanpy: what the scverse single-cell toolkit actually commits to
Single-cell analysis in Python. Scales to >100M cells.
At a glance
- What is it?
- Scanpy covers preprocessing, clustering, trajectory inference and differential expression on top of anndata, with an experimental dask path for out-of-core data. The interesting part is not the feature list but where the project draws the line between its public API and everything else.
- Who is it for?
- Adopt scanpy if your work is Python-side single-cell analysis on anndata objects and you can stay inside the documented API. Do not adopt it expecting a stable internal surface: the README states plainly that function locations and arguments outside the API docs are not guaranteed, so any code importing from scanpy submodules is a migration liability.
- Can I use it commercially?
- Yes. BSD-3-Clause 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 4 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 problem scanpy solves is the glue between single-cell steps
Single-cell expression analysis is a sequence of transformations on one matrix: normalize, select variable genes, reduce dimensions, cluster, annotate, test for differential expression, sometimes infer a trajectory. Each step is a published method with its own reference implementation, and stitching those implementations together across languages and file formats is where most of the engineering time goes. Scanpy's answer is to make the dataset itself the interface. It is built jointly with anndata, so the AnnData object carries the expression matrix alongside cell metadata, gene metadata and the embeddings and cluster labels produced along the way. A pipeline becomes a chain of calls that mutate one object rather than a set of scripts exchanging files. The audience is computational biologists and data scientists who already work in Python and want the analysis steps in the same process as their plotting, statistics and machine learning code, instead of moving between a Python environment and an R one. The README also positions it for scale: it describes the implementation as dealing efficiently with datasets of more than one million cells, and the repository description goes further, claiming scale to over 100M cells.
anndata is the contract, and the dask path is the escape hatch
The architecture is not complicated, which is the point. AnnData holds a primary matrix plus aligned annotations for observations and variables, and scanpy functions read from and write to that structure. Preprocessing, visualization, clustering, trajectory inference and differential expression testing are all listed in the README as included capabilities, and they operate on the same object. This means the data flow is inspectable: after a call, the result is a named slot on the object rather than a return value you have to track. The scaling story has two tiers. The first is the in-memory tier, which the README frames as handling more than a million cells. The second is dask: the README states that many scanpy functions are now compatible with dask for datasets too large to fit into memory, and immediately flags this as experimental, linking to a tracking issue rather than an API page. That word experimental is doing real work. It tells you the dask path is not the default, is not the recommended route for a production pipeline, and is tracked as an open problem. If your dataset fits in memory, you never touch it.
Installing scanpy and the first calls
The README surfaces two distribution channels through its badges: PyPI and conda-forge. That gives you the standard pair of install lines, pip install scanpy or conda install -c conda-forge scanpy. The README itself does not print either command, so treat the package names as the fact and the exact invocation as something to confirm against the installation page in the documentation. What the README does point at directly is the docs site and the Discourse forum for usage discussion. For the workflow itself, the material here does not give a worked example, so I am not going to invent one. The honest description is that the API section of the documentation is the reference, and the README explicitly directs you there, calling the API section the place where the public API is documented. If you are evaluating scanpy, the first thing to read is that API page, not this article, because the function names and their arguments are the actual product surface and they are not reproduced in the README.
The public API boundary is stated more bluntly than most projects state it
Most projects have an implicit line between supported and unsupported code. Scanpy writes it down. The README says the public API is documented in the API section, and then says the project cannot guarantee the stability of internal APIs, whether that is the location of a function, its arguments, or something else. It gives a concrete example: importing from scanpy.logging is not supported, even though the module has no leading underscore, because logging is not documented. The PEP 8 reference is there to explain why the underscore convention is not the test. The project also acknowledges that users do reach into these internals and asks them to open an issue or migrate to the public API, and it invites requests to export things that are missing. This is a coherent position and it is also a warning. Any codebase that imports from scanpy submodules, wraps undocumented helpers, or depends on argument names that are not in the API docs is carrying risk that the maintainers have explicitly declined to absorb. The mitigation is cheap: grep your code for scanpy imports that do not resolve to documented API entries, and file issues for the ones you cannot remove.
Where scanpy is the wrong tool
The clearest boundary is memory. The README's own framing puts the in-memory tier at more than a million cells and offers dask as an experimental route beyond that. A project whose data does not fit in memory and whose pipeline cannot tolerate experimental components has a mismatch with scanpy as it stands, and the README's warning label is the project telling you so. The second boundary is language. Scanpy is Python, and its value proposition includes staying in Python. If your group's existing analysis code, reference implementations or collaborators are R-based, adopting scanpy means either a mixed pipeline with conversion at the boundary or a rewrite, and the README does not claim to solve that. The third boundary is subtler: because scanpy is a toolkit spanning preprocessing through differential expression, it is possible to use it as a thin shell around methods you would rather call directly. If you only need one step, and you need to control its parameters precisely, the orchestration layer adds indirection without adding capability. None of these are defects. They are the shape of the tool.
The alternative is not another Python toolkit, it is a different data model
The natural comparison is Seurat, the R counterpart that covers a similar span of single-cell steps. The difference is not feature coverage, it is the object. Seurat's analysis state lives in an R object manipulated through R's semantics and its own accessor conventions; scanpy's lives in AnnData, a Python object that interoperates with the array and dataframe ecosystem around NumPy and pandas. That difference propagates. In scanpy's world, the matrix on the object is something you can hand to arbitrary Python code, and the dask compatibility path exists precisely because the underlying structure is general enough to swap the backing array. In Seurat's world, the analysis state is idiomatic R and the surrounding ecosystem is R packages. Choosing between them is mostly a question of which language your downstream work, your collaborators and your deployment target already live in. The scverse ecosystem around scanpy is a second consideration: the README notes that scanpy is part of scverse, which is fiscally sponsored by NumFOCUS, and cites a separate scverse publication describing the ecosystem. That matters for longevity questions, not for day-to-day API decisions.
Maintenance, releases and what the licence does not decide for you
The repository is not archived and the last push recorded here is 2026-09-10. The release list shows a stable line and a parallel alpha line: 1.12.4 as a stable release, and 1.13.0a1 and 1.13.0a2 as pre-releases, with the second alpha dated a day after the stable release. That pattern tells you the project ships fixes on the stable branch while developing the next minor version in the open. For a production pipeline, the practical consequence is that 1.13.0a2 is an alpha and should not be pinned for anything you need to reproduce. The upgrade cost is dominated by the anndata coupling. Because scanpy is built jointly with anndata, the object model is a shared dependency, and a scanpy upgrade can move in step with an anndata upgrade. The README does not state a version compatibility policy, so that is something to verify against the release notes for the specific version you move to, not something this material can settle. On licensing, scanpy is BSD-3-Clause. That is a permissive licence, and it is the licence of the code, not of your analysis. It does not govern the data you process, the methods you cite, or the terms of any upstream dataset. The README asks that you cite the 2018 Genome Biology paper by Wolf, Angerer and Theis if you use scanpy in your work, and separately offers the scverse paper for citation. Citation is not a licence condition, but it is a stated expectation, and for academic users it is the operative obligation.
Editorial conclusion
Adopt scanpy if your work is Python-side single-cell analysis on anndata objects and you can stay inside the documented API. Do not adopt it expecting a stable internal surface: the README states plainly that function locations and arguments outside the API docs are not guaranteed, so any code importing from scanpy submodules is a migration liability. Before committing, check that the functions you depend on appear in the API section of the docs, and confirm whether your dataset size forces the dask path, which the README labels experimental. Verify the anndata version your pipeline pins against the scanpy release you install, because the two are developed jointly and the object model is the shared contract.
Community notes