# A plugin catalogue where the loader decides the question

> synthcity is a Python library for generating and evaluating synthetic tabular data, structured so that four data loaders encode four different questions and a plugin category list is the entire public API, with generators catalogued by the assumptions they make about the data, and it explicitly declines to handle missing values rather than guessing at them.

**vanderschaarlab/synthcity** — A library for generating and evaluating synthetic tabular data for privacy, fairness and data augmentation.

- Repository: https://github.com/vanderschaarlab/synthcity
- Website: https://www.vanderschaar-lab.com/
- Stars: 690 · Forks: 98
- Language: Python
- License: Apache-2.0
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/vanderschaarlab-synthcity

## Missing values are explicitly not its job

There is one sentence in the feature list that saves you a day, and it is a limitation stated without hedging.

The library does not handle missing data, and those values must be imputed before anything else happens. The same laboratory publishes a separate imputation package and the documentation points at it for the job.

That is a defensible boundary rather than an omission. Imputation is a modelling decision with its own failure modes, and a generator that silently dropped or filled missing values would produce synthetic data whose distribution you could not reason about. Making the user impute keeps that decision visible.

The cost is that the pipeline has a stage you might not have planned for, and that stage's output becomes part of your training data. If you impute to run the generator and then evaluate the synthetic data against your original, the metrics are measuring a dataset you have already modified.

So the practical advice is to impute once, treat the imputed table as your dataset of record, and keep the missingness pattern as metadata if you need it downstream.

## The category list is the whole public API

The interface is one class with three verbs, and the discovery mechanism is a category list rather than a registry you import.

You construct the plugin object with the categories you care about and ask it to list what matches. You construct it with no arguments and ask it for one plugin by name. Then you fit it and generate from it. Those are the same three calls for tabular, survival, time-series and image data.

That is a good design for a library whose point is comparison. A benchmark can loop over plugin names without importing anything from each generator's own module, and the fit and generate signatures stay uniform even though a time-series model and a survival model consume entirely different data structures.

What varies is the loader, and that is where the real interface decision sits. A generic tabular loader takes a frame, a target column name and a list of sensitive columns. The survival loader takes a target column and a time-to-event column. The time-series loader takes four arguments covering the temporal values, the observation times, static covariates and an outcome. The image loader takes a dataset and samples a number of items.

So the plugin is interchangeable and the loader is not, and choosing the loader is choosing the question your synthetic data will be able to answer.

## Each loader encodes a different problem

Four loaders, four shapes of question, and the parameter names tell you what each one assumes.

The generic loader takes a target column and a list of sensitive columns. That second parameter is the fairness hook, and it is worth noticing that it is available on the plain tabular loader used in the benchmark example, where the sensitive column is the sex column of a bundled regression dataset. A framework that carries a fairness attribute on every generic table is making a statement: no dataset passes through it without the option of declaring what must not be learned.

The survival loader takes a target column and, separately, a time-to-event column. Splitting them is the whole point, because a survival model needs both an event indicator and a duration, and conflating them is a common source of silent errors.

The time-series loader takes four things: the temporal values, the observation times, static data that does not vary over time, and an outcome. A bundled loader for a market dataset returns exactly those four, so the contract is demonstrated rather than described.

The image loader is the outlier: instead of a frame it takes a dataset object and has a sampling step, and generation returns two values, an image array and a label array, which you unpack and convert.

Choosing the wrong loader is the mistake this design makes easy to make and easy to notice, since a mismatched call fails at construction rather than at training.

## Generators are grouped by assumption, and the groups overlap

The reference model list is organised into named categories, and reading them as a partition is the mistake.

General-purpose models span four families: adversarial networks, variational autoencoders, normalising flows, Bayesian networks, plus a random-forest implementation and a language-model-based generator.

Then there are domain groups. Time series and time-to-event survival models, static survival models, privacy-focused models, domain-adaptation models, and image models.

Here is the overlap. Four of the privacy-focused entries also appear in the general-purpose list. That is not sloppiness, it is a tag rather than a bucket: a differentially private generator is both a general-purpose generator and a privacy-focused one, and which category you filter by tells you which of its properties you are asking about.

So the category list is best used for discovery rather than for exclusion. Asking for the privacy category returns differentially private generators and also returns models that are only incidentally private, which is the right behaviour for someone reading the list and the wrong behaviour for someone writing a filtering rule.

The count also tells you the shape of the field the library covers: it is broad enough that no single family is the recommended one, which is consistent with a benchmark-oriented tool.

## The image generators are flagged as not state of the art

One example carries a warning, and it is the kind that only a maintainer bothers to write.

The note says the architectures used for the image generators are not state of the art, and points anyone who wants different architectures to a specific extension point: a method for suggesting the generator and discriminator architectures, in a module named for convolutional networks.

That single sentence reframes the image support. The image plugins exist so the same fit, generate and benchmark path works on images, not because the library claims competitive image generation. The extension point is the giveaway, since naming the exact function to override tells you the authors expect you to replace the architecture rather than tune it.

This is worth reading as a pattern for optional modules in any research library. Rather than omitting the feature or quietly shipping something weak, the project ships the integration, states the limitation, and points at the seam. You get a working path today and an honest note about what you are getting.

The rest of the image example is short: download a well-known handwritten-digit dataset through the standard vision library, sample a hundred items, fit an image conditional GAN, generate ten, and unpack the result into an image array and a label array.

## Three ways to persist a model, one assertion

Serialization is documented three times, which suggests it was added incrementally rather than designed once.

The first way is buffer-based. You save a fitted plugin to a byte string and load it back, checking that the two agree.

The second is file-based. Save to a path with one call and load from it with another, and the example shows the generator being constructed with a smaller iteration count so the artefact has an identifiable name.

The third is the plugin interface itself: the fitted object has a save method that returns a buffer, and the plugin class can load it. So a plugin can be persisted without importing a utility module, which is what you want if you are holding plugins behind your own abstraction.

All three examples end with the same assertion, that the name of the original equals the name of the reloaded model. That is a weak check, and its weakness is informative. A name comparison proves the right class came back and nothing more; it does not prove the weights round-tripped or that a trained model reloaded to its pre-training state.

For anything load-bearing you would want to generate from both and compare, which none of the examples do.

## The packaging is generated, and the version comes from a regex

The build configuration is the last place to look and it explains a few oddities.

The modern project file contains only the build system, two requirements and a backend. Above them is a comment in capitals telling you not to change the requirements because they will be overwritten by a scaffolding tool.

That single file tells you the project was generated from a template and is still maintained as one. It also means the real dependency and metadata story is elsewhere, in the older-style configuration files at the root, alongside a requirements file for prerequisites and a pre-commit configuration.

Installing is the expected shape, from the index or from a checkout:

```bash
$ pip install synthcity
$ pip install .
```

Three extras are offered: one with unit-testing support, one described as Google support, and one that installs everything.

The setup script is where the version lives. And the mechanism is worth naming. It opens the version file inside the source package, takes only its first line, and matches a regular expression against a version string assignment. If the match fails, it returns a hardcoded fallback of the first release.

The failure branch is the problem. A renamed variable or a reformatted version line does not raise an error; it silently produces a package claiming to be the earliest version ever released, which is the kind of mistake that surfaces much later on a package index.

Around it is a bare exception handler that catches everything, prints a message telling you to upgrade three build tools, and re-raises. That message mentions a source-control-driven versioning tool, while the actual version comes from the regular expression, so the advice and the mechanism disagree.

## Conclusion

It fits a researcher who needs several generator families behind one interface so a benchmark can compare them without rewriting the data path, since the value is the plugin taxonomy and the loader abstraction rather than any single model. Three things to check before you commit. Missing values are not handled and must be imputed first, with the same lab's imputation package suggested, so an imputation step is part of your pipeline and its artefacts end up in your training data. The categories are tags rather than a partition, so several generators appear under more than one heading and you should select by name rather than by category. And the packaging is generated by a scaffolding tool whose warning tells you not to edit one of its files, while the version is parsed out of a source file by a regular expression.

## FAQ

### What is synthcity?

It is an Apache-2.0 licensed Python library for generating and evaluating synthetic tabular data, aimed at privacy, fairness and data augmentation. It is built around a plugin architecture with one class providing list, get, fit and generate, several evaluation metrics for correctness and privacy, and reference generators spanning adversarial networks, variational autoencoders, normalising flows, Bayesian networks, a random-forest implementation, a language-model-based generator, time-series and survival models, and image models.

### Does synthcity handle missing values?

No, and it says so explicitly. Missing values must be imputed before the library is used, and the documentation points to the same laboratory's separate imputation package for the job. That makes imputation a stage in your own pipeline, and its output becomes part of the data you train on and evaluate against.

### How do I load a generator in synthcity?

Construct the plugin class with the categories you want and call list to see what is available, then construct it with no arguments and call get with a plugin name. Fit the returned object and call generate with a count. The same three calls work across tabular, survival, time-series and image data, but the data loader you pass to fit differs for each.

### What are synthcity's data loaders and how do they differ?

The generic loader takes a frame, a target column and a list of sensitive columns, which is how fairness attributes are carried through. The survival loader takes a target column and a separate time-to-event column. The time-series loader takes temporal values, observation times, static covariates and an outcome. The image loader takes a dataset and has a sampling step, and generation from it returns images and labels as two values.

### How do I save and load a synthcity model?

Three ways are documented: a buffer-based save and load pair, a file-based save and load pair that takes paths, and the plugin object's own save method combined with loading through the plugin class. The examples verify the reload by comparing the model name, which confirms the right class came back rather than that the weights round-tripped, so compare generated output if the distinction matters.

## Sources

- [License: Apache-2.0](https://github.com/vanderschaarlab/synthcity/blob/main/LICENSE)
- [Project website](https://www.vanderschaar-lab.com/)
- [README](https://github.com/vanderschaarlab/synthcity/blob/main/README.md)
- [Releases](https://github.com/vanderschaarlab/synthcity/releases)
- [vanderschaarlab/synthcity on GitHub](https://github.com/vanderschaarlab/synthcity)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/vanderschaarlab-synthcity
