Which scikit-learn base classes do you inherit to write a custom transformer?
answer
- two classes, and the order matters
- one gives introspection, one gives behaviour
- get_params comes from the base
- mixins first so super() terminates
- score() is what the classifier mixin adds
basics
~10 sInherit both, mixin first: class MyTransformer(TransformerMixin, BaseEstimator). BaseEstimator supplies get_params, set_params, the repr and the default tags; TransformerMixin supplies fit_transform and set_output. You then implement only init, fit returning self, and transform.
solid answer
~40 sThe idiomatic declaration is `class MyTransformer(TransformerMixin, BaseEstimator)`, and the order is not arbitrary. `BaseEstimator` gives you the introspection half of the contract — `get_params`/`set_params` driven by the `__init__` signature, a readable `__repr__`, and the default `__sklearn_tags__` — which is what makes the object cloneable and therefore usable inside pipelines and searches. `TransformerMixin` gives the behavioural half: a `fit_transform` composed from your `fit` and `transform`, plus `set_output` for pandas or polars output. The parallel mixins are `ClassifierMixin` (a `score` returning accuracy, and the classifier estimator type), `RegressorMixin` (a `score` returning R-squared), and `ClusterMixin` (`fit_predict`). Mixins must come **before** `BaseEstimator` in the bases, because since scikit-learn 1.6 their `__sklearn_tags__` implementations call `super().__sklearn_tags__()` and need `BaseEstimator` last in the MRO to terminate the chain.
code
python · 18 linesfrom sklearn.base import BaseEstimator, TransformerMixin
from sklearn.utils.validation import check_is_fitted, validate_data
class MeanCenterer(TransformerMixin, BaseEstimator): # mixin first
def __init__(self, with_std=False):
self.with_std = with_std # stored verbatim
def fit(self, X, y=None):
X = validate_data(self, X) # sets n_features_in_
self.mean_ = X.mean(axis=0)
self.scale_ = X.std(axis=0) if self.with_std else None
return self # fit returns self
def transform(self, X):
check_is_fitted(self)
X = validate_data(self, X, reset=False)
centered = X - self.mean_
return centered / self.scale_ if self.with_std else centeredgo deeper
Be able to write the class line from memory — TransformerMixin then BaseEstimator — and say which methods you still have to write yourself: init, fit returning self, and transform.
Explain the division of labour: BaseEstimator supplies get_params/set_params and the repr, mixins supply fit_transform or score, and the classifier and regressor mixins differ in the metric their score returns.
Show that you have hit the 1.6 tags migration — recognise the 'super object has no attribute sklearn_tags' error as a base-class ordering problem, and run check_estimator on in-house estimators as part of the test suite.
Own the standardisation argument: making internal estimators conform to this contract is what lets one evaluation and deployment harness serve every model in the organisation, rather than each team wiring its own.
## The building blocks scikit-learn splits the estimator contract across small classes in `sklearn.base` so that an author picks up exactly the behaviour their estimator has. **`BaseEstimator`** is the one nearly everything needs. It provides: - `get_params(deep=True)` and `set_params(**params)`, both driven by introspection of your `__init__` signature — this is what makes the object cloneable, searchable and printable as a configuration. - A `__repr__` that renders the estimator with its non-default parameters, which is why estimators display readably in a notebook. - The default `__sklearn_tags__`, the capability metadata the common checks consume. **`TransformerMixin`** adds `fit_transform` (default: fit on the data, then transform that same data, forwarding `y` when given) and `set_output`, which lets a caller request `transform="pandas"` or `"polars"` instead of a NumPy array. **`ClassifierMixin`** adds a `score(X, y)` returning mean accuracy and marks the estimator's type as a classifier, which is what `sklearn.base.is_classifier` reports and what makes stratified splitting the default in cross-validation helpers. **`RegressorMixin`** adds a `score(X, y)` returning the coefficient of determination, R-squared, and marks the estimator as a regressor for `is_regressor`. **`ClusterMixin`** adds `fit_predict`, the joint form clusterers are used through. **`OneToOneFeatureMixin`** and **`ClassNamePrefixFeaturesOutMixin`** supply `get_feature_names_out` for the two common output-naming shapes: columns that correspond one-to-one with the input columns, and columns named after the class with an index suffix. They matter as soon as anything downstream wants named output. ## What you implement yourself For a transformer: `__init__` that stores hyperparameters verbatim; `fit(X, y=None)` that validates input, computes trailing-underscore attributes and returns `self`; `transform(X)` that calls `check_is_fitted(self)` and applies the learned state. That is all — `fit_transform`, `get_params`, `set_params`, the repr and the tags all come from the bases. For a classifier: `fit` must also set `classes_`, and you implement `predict`; `score` arrives from the mixin. If your model produces probabilities, add `predict_proba` — no mixin supplies it, because it is not part of the minimum contract. ## The MRO rule, and the error it produces Write `class MyTransformer(TransformerMixin, BaseEstimator)`, never the reverse. The reason became load-bearing in scikit-learn 1.6, when tags moved to the `__sklearn_tags__` protocol. Each mixin implements that method by calling `super().__sklearn_tags__()` to get the defaults and then setting its own fields. That chain must reach `BaseEstimator`, which provides the terminating implementation. Put `BaseEstimator` first and the mixin's `super()` call runs past the end of the chain, producing `AttributeError: 'super' object has no attribute '__sklearn_tags__'`. The same error appears when a custom class inherits from a third-party base that predates the protocol, or when someone inherits a mixin without inheriting `BaseEstimator` at all. Recognising that message and answering "wrong base-class order, or a base that never migrated off the old tag protocol" is a strong signal of hands-on 1.6+ experience. ## Do you have to inherit anything? Strictly, no. scikit-learn is duck-typed: an object with `fit` and `predict` will work in plenty of places. But you lose `get_params` and `set_params`, and without those `clone` fails — which removes the object from cross-validation, hyperparameter search, pipelines and every meta-estimator, i.e. the entire reason for writing a scikit-learn-shaped estimator in the first place. You also lose the tags, so the common checks cannot reason about it. Inheriting is cheap; skipping it is not. ## Verifying the result `sklearn.utils.estimator_checks.check_estimator(MyTransformer())` runs the library's conformance suite against your class: constructor inertness, clone round-trip, fitted-attribute conventions, `NotFittedError` before fit, shape validation, `n_features_in_` handling, determinism, and more. For a test suite, `parametrize_with_checks([MyTransformer()])` from the same module turns each check into an individual pytest case. Running it once while writing a custom estimator catches most contract violations before they surface as a confusing failure three layers deep inside a search. ## The shape to memorise Mixin(s) first, `BaseEstimator` last; hyperparameters verbatim in `__init__`; learned state with a trailing underscore in `fit`; `return self`; `check_is_fitted` at the top of every method that uses learned state. Everything else the library needs, it derives.
- What exactly does inheriting ClassifierMixin give you?A `score(X, y)` method returning mean accuracy of `predict` against `y`, and the classifier estimator type in the tags — which is what `sklearn.base.is_classifier` reads and what makes helper routines choose stratified splitting. It does not give you `fit`, `predict` or `predict_proba`; those remain yours to implement, and `fit` is still expected to set `classes_`.
- Why must the mixin come before BaseEstimator in the base-class list?Because since scikit-learn 1.6 each mixin's `__sklearn_tags__` calls `super().__sklearn_tags__()` to fetch the defaults before setting its own fields, and `BaseEstimator` provides the implementation that terminates that chain. With `BaseEstimator` first, the mixin's `super()` call finds nothing and you get `AttributeError: 'super' object has no attribute '__sklearn_tags__'`.
- Can you write a working scikit-learn estimator without inheriting from anything?Duck typing gets you as far as calling `fit` and `predict` yourself. But without `get_params`/`set_params` the object cannot be cloned, and clone is the primitive underneath cross-validation, hyperparameter search, pipelines and every meta-estimator — so it is excluded from all of them. It also has no tags for the common checks to consult. Inheriting `BaseEstimator` costs nothing and buys all of it.
saying these in an interview costs you the question
- Writes class T(BaseEstimator, TransformerMixin) with the bases reversed
- Thinks TransformerMixin supplies get_params
- Implements fit_transform by hand instead of inheriting it
- Expects a mixin to provide fit or predict
- Skips BaseEstimator and wonders why the estimator fails inside a search