NotFittedError: "This Estimator Is Not Fitted Yet"The sklearn.exceptions.NotFittedError is one of the most common errors in scikit-learn workflows. It always means the same thing: you called a method that requires learned parameters (like predict(), transform(), or score()) on an estimator that has never seen training data. This article covers every root cause and the exact fix for each.
Every scikit-learn estimator follows the same contract: call fit(X, y) first, then call predict(), transform(), score(), etc. Internally, fit() stores learned attributes (weights, feature names, vocabulary, scaling parameters) on the estimator instance. Methods like predict() look for those attributes. If they are absent, scikit-learn raises NotFittedError.
Here is the full traceback you will typically see:
Traceback (most recent call last):
File "train.py", line 14, in <module>
predictions = clf.predict(X_test)
File "/usr/local/lib/python3.11/site-packages/sklearn/utils/validation.py", line 1461, in inner_f
return f(*args, **kwargs)
File "/usr/local/lib/python3.11/site-packages/sklearn/linear_model/_base.py", line 389, in predict
check_is_fitted(self)
sklearn.exceptions.NotFittedError: This RandomForestClassifier instance is not fitted yet.
Call 'fit' with appropriate arguments before using this estimator.
The key phrase is "Call 'fit' with appropriate arguments before using this estimator." The estimator class name in the message tells you exactly which object is the problem. Armed with that, trace backwards through your code to find where that object should have been fitted.
sklearn.exceptions.NotFittedError. It is also aliased at sklearn.utils.validation.NotFittedError for backward compatibility, so both import paths will catch it with except NotFittedError.
predict() Called Before fit()This is the most straightforward cause. You instantiated the estimator and immediately tried to use it without training it first.
# WRONG — predict() called on a fresh, unfitted instance
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
clf = RandomForestClassifier(n_estimators=100, random_state=42)
# Missing: clf.fit(X_train, y_train)
predictions = clf.predict(X) # <-- NotFittedError here
# CORRECT — always call fit() before predict()
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X_train, y_train) # learn from training data first
predictions = clf.predict(X_test) # now safe to call
This pattern also applies to transformers. Calling scaler.transform(X) without a preceding scaler.fit(X) or scaler.fit_transform(X) produces the same error:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
# WRONG: scaler.transform(X_train)
# CORRECT:
X_train_scaled = scaler.fit_transform(X_train) # fits AND transforms in one call
X_test_scaled = scaler.transform(X_test) # uses parameters learned on train set
fit_transform() on the test set. This re-fits the scaler to test data, leaking information and invalidating your evaluation. Always fit on train, transform on test.
When you use sklearn.pipeline.Pipeline, fitting the pipeline calls fit() on each step in sequence. The state is stored on the step objects inside the pipeline. A common mistake is to create a separate standalone estimator, fit the pipeline, and then call predict() or transform() on the standalone object — which was never fitted.
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
# Create a standalone scaler AND a pipeline that contains its OWN scaler
standalone_scaler = StandardScaler()
pipeline = Pipeline([
('scaler', StandardScaler()), # this is a different object from standalone_scaler
('clf', SVC()),
])
pipeline.fit(X_train, y_train)
# WRONG: standalone_scaler was never fitted — NotFittedError
X_scaled = standalone_scaler.transform(X_test)
# CORRECT option A: access the fitted step from inside the pipeline
fitted_scaler = pipeline.named_steps['scaler']
X_scaled = fitted_scaler.transform(X_test)
# CORRECT option B: just use the pipeline for everything
predictions = pipeline.predict(X_test) # pipeline handles transform internally
The pipeline's predict() method automatically passes data through every prior step's transform() before calling the final estimator's predict(). You almost never need to call individual step methods manually after the pipeline is fitted.
fit()Serialization bugs are especially tricky because the error appears in a completely different script, often a production inference service, far removed from the training code.
import pickle
from sklearn.linear_model import LogisticRegression
# WRONG: model pickled before training
clf = LogisticRegression()
with open('model.pkl', 'wb') as f:
pickle.dump(clf, f) # saved an unfitted model!
# ... later, in inference.py ...
with open('model.pkl', 'rb') as f:
loaded_clf = pickle.load(f)
loaded_clf.predict(X_new) # <-- NotFittedError: model was never fitted
import pickle
from sklearn.linear_model import LogisticRegression
# CORRECT: fit first, then serialize
clf = LogisticRegression()
clf.fit(X_train, y_train) # <-- must happen before pickle
with open('model.pkl', 'wb') as f:
pickle.dump(clf, f)
# Now the loaded model is safe to use
with open('model.pkl', 'rb') as f:
loaded_clf = pickle.load(f)
predictions = loaded_clf.predict(X_new) # works correctly
The same applies to joblib, which is preferred over raw pickle for large NumPy arrays:
import joblib
from sklearn.ensemble import GradientBoostingClassifier
clf = GradientBoostingClassifier()
clf.fit(X_train, y_train)
joblib.dump(clf, 'model.joblib')
# In inference
clf = joblib.load('model.joblib')
predictions = clf.predict(X_test)
NotFittedError from a loaded model, check your training script's save logic. Add an assertion immediately before the dump: assert hasattr(clf, 'classes_') (for classifiers) or assert hasattr(scaler, 'mean_') (for StandardScaler). These attributes only exist after a successful fit().
transform() Before fit_transform()When building preprocessing pipelines manually (without sklearn.pipeline.Pipeline), it is easy to call the steps out of order, especially when refactoring code.
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.linear_model import Ridge
scaler = StandardScaler()
poly = PolynomialFeatures(degree=2)
ridge = Ridge(alpha=1.0)
# WRONG order: transform before fit
X_poly = poly.transform(X_train) # NotFittedError — poly never fitted
X_scaled = scaler.fit_transform(X_poly)
ridge.fit(X_scaled, y_train)
# CORRECT order
X_poly = poly.fit_transform(X_train) # fit AND transform train set
X_scaled = scaler.fit_transform(X_poly) # fit AND transform train set
ridge.fit(X_scaled, y_train)
# For test set — transform only, do NOT refit
X_test_poly = poly.transform(X_test)
X_test_scaled = scaler.transform(X_test_poly)
predictions = ridge.predict(X_test_scaled)
The rule of thumb: for each transformer, use fit_transform(X_train) exactly once on training data, then transform(X_test) (no re-fitting) on all other splits. The cleanest way to enforce this ordering automatically is to wrap the whole preprocessing chain in a Pipeline:
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler, PolynomialFeatures
from sklearn.linear_model import Ridge
pipe = Pipeline([
('poly', PolynomialFeatures(degree=2)),
('scaler', StandardScaler()),
('model', Ridge(alpha=1.0)),
])
pipe.fit(X_train, y_train) # fits every step in order, automatically
predictions = pipe.predict(X_test) # transforms through every step, then predicts
check_is_fitted() Proactivelyscikit-learn exposes the same internal check it uses in predict() and transform() as a public utility: sklearn.utils.validation.check_is_fitted(). You can call it yourself to validate estimator state before handing it to downstream code, or to build informative error messages in production systems.
from sklearn.utils.validation import check_is_fitted
from sklearn.exceptions import NotFittedError
from sklearn.ensemble import RandomForestClassifier
clf = RandomForestClassifier()
# Guard before predict — raise a clear error instead of a cryptic one later
try:
check_is_fitted(clf)
except NotFittedError:
raise RuntimeError(
"Model must be trained before inference. "
"Call clf.fit(X_train, y_train) first."
)
check_is_fitted() checks for the presence of any attributes ending in _ (the scikit-learn convention for fitted parameters — e.g. coef_, n_features_in_, classes_). You can also check for specific attributes:
from sklearn.utils.validation import check_is_fitted
# Check for a specific fitted attribute
check_is_fitted(clf, attributes=['n_estimators_', 'estimators_'])
# Or use hasattr for a quick inline check
def is_fitted(estimator):
"""Return True if the estimator has been fitted."""
try:
check_is_fitted(estimator)
return True
except NotFittedError:
return False
clf = RandomForestClassifier()
print(is_fitted(clf)) # False
clf.fit(X_train, y_train)
print(is_fitted(clf)) # True
In production inference services, wrapping predict in a guard function prevents NotFittedError from propagating to end users:
from sklearn.utils.validation import check_is_fitted
from sklearn.exceptions import NotFittedError
def safe_predict(estimator, X):
"""Predict with a clear error if the model is not ready."""
try:
check_is_fitted(estimator)
except NotFittedError as exc:
raise RuntimeError(
f"{type(estimator).__name__} is not fitted. "
"Ensure the model training pipeline ran successfully "
"and the model artifact was saved after fit()."
) from exc
return estimator.predict(X)
# Usage
predictions = safe_predict(clf, X_test)
Different estimators set different trailing-underscore attributes after fitting. Here are the most common ones you can check with hasattr():
# Classifiers
hasattr(clf, 'classes_') # LogisticRegression, RandomForest, SVC, etc.
hasattr(clf, 'estimators_') # RandomForest, GradientBoosting
# Regressors
hasattr(reg, 'coef_') # LinearRegression, Ridge, Lasso
hasattr(reg, 'feature_importances_') # tree-based regressors
# Transformers
hasattr(scaler, 'mean_') # StandardScaler
hasattr(scaler, 'scale_') # StandardScaler, RobustScaler
hasattr(enc, 'categories_') # OneHotEncoder
hasattr(imputer, 'statistics_') # SimpleImputer
hasattr(pca, 'components_') # PCA
NotFittedError root cause Fix
───────────────────────────── ─────────────────────────────────────────────
predict() before fit() Add clf.fit(X_train, y_train) before predict()
Standalone estimator vs pipeline Use pipeline.named_steps['step'] or pipeline.predict()
Model pickled before training Call fit() first, then joblib.dump() / pickle.dump()
transform() before fit_transform Fix step order; prefer Pipeline for enforcement
Loaded model from disk is empty Check training script — assert hasattr(clf, 'coef_')