TypeError: cannot unpack non-iterable NoneType object is one of those
errors that blindsides you because the line that crashes looks completely reasonable — a tuple
assignment, a for-loop, a train_test_split call. The problem is always one step
earlier: something that was supposed to return a value returned None instead.
This guide walks through the five most common causes, each with a real traceback, and shows
you exactly how to fix each one.
The message is always the same, but it surfaces in very different contexts. Here are two representative tracebacks you are likely to encounter.
Traceback (most recent call last):
File "pipeline.py", line 14, in <module>
df_clean, df_dirty = preprocess(raw)
File "pipeline.py", line 8, in preprocess
df, remainder = df.sort_values('score', inplace=True), df[df['score'].isna()]
TypeError: cannot unpack non-iterable NoneType object
Traceback (most recent call last):
File "train.py", line 22, in <module>
X_train, X_test, y_train, y_test = split_data(df)
TypeError: cannot unpack non-iterable NoneType object
In both cases Python is trying to iterate over the right-hand side of an unpacking assignment
(a, b = <expr>) and finds None where it expected a sequence.
Python cannot iterate over None, so it raises TypeError rather than
the more familiar ValueError: not enough values to unpack.
Unpacking is how Python lets you assign multiple names from a single iterable in one
statement. Any time you write one of the patterns below, Python calls iter()
internally on the right-hand side:
# Tuple unpacking — needs an iterable with exactly 2 items
a, b = some_function()
# Extended unpacking — first element + rest
first, *rest = some_function()
# For-loop body unpacking — each element must itself be iterable
for key, value in some_function():
print(key, value)
# Nested unpacking in a comprehension
pairs = [(k, v) for k, v in some_function()]
When some_function() returns None, every single one of these patterns
raises the same TypeError: cannot unpack non-iterable NoneType object. The mental
model is simple: before Python can split a result across names it must be able to loop over it,
and None is not loopable.
# Quick proof
x = None
a, b = x # TypeError: cannot unpack non-iterable NoneType object
# iter() is what Python uses internally
iter(None) # TypeError: 'NoneType' object is not iterable
Every pandas method that accepts an inplace=True keyword argument returns
None when that flag is set. The rationale is that the operation modifies the
DataFrame in place rather than producing a new object. This is a common source of confusion
because the same method without inplace=True returns a DataFrame you can unpack or
chain. See also the related pandas KeyError in
groupby article for other pandas anti-patterns.
import pandas as pd
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'score': [88, 72, 95],
})
# WRONG — sort_values with inplace=True returns None
sorted_df = df.sort_values('score', inplace=True)
# Trying to unpack None crashes immediately
top, bottom = sorted_df # TypeError: cannot unpack non-iterable NoneType object
A subtler version of the same bug hides inside a function:
import pandas as pd
def preprocess(df):
df.drop_duplicates(inplace=True)
df.dropna(inplace=True)
df.sort_values('score', ascending=False, inplace=True)
# No return statement — function implicitly returns None
# (see Cause 2 for this half of the bug)
raw = pd.read_csv('scores.csv')
clean, summary = preprocess(raw) # TypeError: cannot unpack non-iterable NoneType object
The same trap applies to df.reset_index(inplace=True),
df.rename(columns={...}, inplace=True),
df.fillna(0, inplace=True), and every other pandas method that accepts
inplace.
import pandas as pd
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'score': [88, 72, 95],
})
# CORRECT — omit inplace=True and capture the return value
sorted_df = df.sort_values('score', ascending=False)
# Now sorted_df is a DataFrame; you can index or unpack it safely
print(sorted_df.head())
# If you genuinely want in-place modification, just don't unpack
df.sort_values('score', inplace=True)
# df is now sorted; no assignment needed
The pandas documentation itself now discourages inplace=True because it prevents
method chaining and is no longer faster than reassignment in modern pandas. Prefer the
reassignment style consistently to avoid this entire class of bug.
import pandas as pd
df = pd.read_csv('scores.csv')
# Chain operations — each method returns a new DataFrame
df_clean = (
df
.drop_duplicates()
.dropna(subset=['score'])
.sort_values('score', ascending=False)
.reset_index(drop=True)
)
print(df_clean.head())
Python functions that reach the end of their body without hitting a return
statement implicitly return None. This is intentional for functions called for
their side effects, but it causes the unpacking error when the caller assumes a tuple is
coming back.
import pandas as pd
from sklearn.preprocessing import StandardScaler
def prepare_features(df):
features = df[['age', 'income', 'score']].copy()
scaler = StandardScaler()
scaled = scaler.fit_transform(features)
# BUG: forgot to return scaled and scaler
# The function falls off the end and returns None
X, scaler = prepare_features(df) # TypeError: cannot unpack non-iterable NoneType object
The same pattern appears when an early conditional path forgets its return but a later one does not:
def load_data(path, mode='train'):
if mode == 'train':
df = pd.read_csv(path)
X = df.drop('label', axis=1)
y = df['label']
return X, y
elif mode == 'test':
df = pd.read_csv(path)
X = df.drop('label', axis=1)
# BUG: forgot return here — falls through to implicit None
y = df['label']
X_test, y_test = load_data('test.csv', mode='test')
# TypeError: cannot unpack non-iterable NoneType object
import pandas as pd
from sklearn.preprocessing import StandardScaler
def prepare_features(df):
features = df[['age', 'income', 'score']].copy()
scaler = StandardScaler()
scaled = scaler.fit_transform(features)
return scaled, scaler # explicit return — always required when caller unpacks
X, scaler = prepare_features(df) # works correctly
print(X.shape)
def load_data(path, mode='train'):
df = pd.read_csv(path)
X = df.drop('label', axis=1)
y = df['label']
return X, y # single return covers both modes; eliminate the branching
X_test, y_test = load_data('test.csv', mode='test') # works correctly
A quick way to catch this early: add -> tuple type hints to functions you
intend to unpack. Static type checkers like mypy and Pyright will warn you about code paths
that return None from a function annotated with a non-None return type.
import pandas as pd
from sklearn.preprocessing import StandardScaler
import numpy as np
def prepare_features(df: pd.DataFrame) -> tuple[np.ndarray, StandardScaler]:
features = df[['age', 'income', 'score']].copy()
scaler = StandardScaler()
scaled = scaler.fit_transform(features)
return scaled, scaler # mypy enforces this return is present
sklearn.model_selection.train_test_split always returns twice as many
arrays as you pass in. Pass two arrays, get four back. Pass three arrays, get six back.
The most common mistake is passing the wrong number of receiving variables, or assigning the
call result to a single name and then trying to unpack it later.
from sklearn.model_selection import train_test_split
import pandas as pd
df = pd.read_csv('housing.csv')
X = df.drop('price', axis=1)
y = df['price']
# BUG: assigned to one name — splits is a tuple of 4 arrays, not None,
# but the following attempt to get X_train etc. is still wrong
splits = train_test_split(X, y, test_size=0.2, random_state=42)
# Caller then tries to unpack wrong number of items
X_train, y_train = splits # ValueError: too many values to unpack (expected 2)
# Or worse — if the user wrote a wrapper that forgets return:
def make_splits(X, y):
train_test_split(X, y, test_size=0.2, random_state=42)
# BUG: no return
result = make_splits(X, y)
X_train, X_test, y_train, y_test = result
# TypeError: cannot unpack non-iterable NoneType object
from sklearn.model_selection import train_test_split
# Passing one array but expecting four names is a mismatch
X_train, X_test, y_train, y_test = train_test_split(X, test_size=0.2)
# ValueError: not enough values to unpack (expected 4, got 2)
# Passing two arrays but only three names
X_train, X_test, y_train = train_test_split(X, y, test_size=0.2)
# ValueError: too many values to unpack (expected 3)
from sklearn.model_selection import train_test_split
import pandas as pd
df = pd.read_csv('housing.csv')
X = df.drop('price', axis=1)
y = df['price']
# CORRECT — 2 arrays in, 4 arrays out
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42,
stratify=None,
)
print(X_train.shape, X_test.shape)
print(y_train.shape, y_test.shape)
# If you use a helper function, always return the result
def make_splits(X, y, test_size=0.2, seed=42):
return train_test_split(X, y, test_size=test_size, random_state=seed)
X_train, X_test, y_train, y_test = make_splits(X, y)
The rule of thumb: the number of names on the left of = must equal
2 * len(arrays_passed_to_train_test_split). If your features and targets both
have missing values that cause issues downstream, see
ValueError: NaN in sklearn estimators.
re.match() and re.search() return a match object on
success, or None when the pattern does not match. If you call
.groups() or try to unpack the match object directly without checking for
None first, you get the error.
import re
log_line = "2026-07-16 ERROR connection refused"
# Pattern expects exactly this format — but what if the line is different?
match = re.match(r'(\d{4}-\d{2}-\d{2}) (\w+) (.+)', log_line)
# Direct unpack of match object — only works if match is not None
date, level, message = match.groups()
# If match is None:
# TypeError: cannot unpack non-iterable NoneType object
# Another common form — unpack the match itself (not .groups())
date, level, message = re.match(r'(\d{4}-\d{2}-\d{2}) (\w+) (.+)', log_line)
# TypeError: cannot unpack non-iterable NoneType object
import re
# Simulating a line that doesn't match
log_line = "INFO server started"
match = re.match(r'(\d{4}-\d{2}-\d{2}) (\w+) (.+)', log_line)
print(match) # None
date, level, message = match.groups()
# AttributeError: 'NoneType' object has no attribute 'groups'
# (or TypeError if you unpack match directly)
import re
def parse_log_line(line: str):
pattern = r'(\d{4}-\d{2}-\d{2}) (\w+) (.+)'
match = re.match(pattern, line)
if match is None:
# Return a sentinel or raise a meaningful error
return None, None, None
date, level, message = match.groups()
return date, level, message
log_line = "2026-07-16 ERROR connection refused"
date, level, message = parse_log_line(log_line)
if date is not None:
print(f"Date={date} Level={level} Message={message}")
import re
# Walrus operator (Python 3.8+) for compact inline guard
log_line = "2026-07-16 WARNING disk usage above 90%"
if m := re.match(r'(\d{4}-\d{2}-\d{2}) (\w+) (.+)', log_line):
date, level, message = m.groups()
print(date, level, message)
else:
print("Line did not match expected format:", repr(log_line))
import re
# Processing a list of log lines safely
lines = [
"2026-07-16 ERROR connection refused",
"INFO server started", # won't match
"2026-07-15 DEBUG cache warm",
]
pattern = re.compile(r'(\d{4}-\d{2}-\d{2}) (\w+) (.+)')
parsed = []
for line in lines:
m = pattern.match(line)
if m:
date, level, message = m.groups()
parsed.append({'date': date, 'level': level, 'message': message})
else:
print(f"Skipping unmatched line: {repr(line)}")
print(parsed)
Note that re.match() only matches at the start of the string, while
re.search() finds a match anywhere. Both return None on failure,
so both need the same guard.
dict.get(key) returns None when the key is absent (rather than
raising KeyError). If the value stored under that key is itself expected to be
a tuple or list that you want to unpack, the silent None propagates to the
unpacking site and crashes there.
model_configs = {
'random_forest': (100, 5, 'gini'),
'gradient_boost': (200, 3, 'friedman_mse'),
}
# Key exists — works fine
n_estimators, max_depth, criterion = model_configs['random_forest']
# Key absent — dict.get() returns None silently
params = model_configs.get('logistic_regression')
print(params) # None
n_estimators, max_depth, criterion = params
# TypeError: cannot unpack non-iterable NoneType object
# Another common variant: chained .get() on nested dicts
config = {
'preprocessing': {
'scaler': 'standard',
}
}
# Inner key missing — returns None, then None is unpacked
train_cols, test_cols = config.get('features', {}).get('columns')
# TypeError: cannot unpack non-iterable NoneType object
model_configs = {
'random_forest': (100, 5, 'gini'),
'gradient_boost': (200, 3, 'friedman_mse'),
}
# Provide a default tuple that matches the expected structure
params = model_configs.get('logistic_regression', (100, None, None))
n_estimators, max_depth, criterion = params
print(n_estimators, max_depth, criterion) # 100 None None
model_name = 'logistic_regression'
params = model_configs.get(model_name)
if params is None:
raise KeyError(
f"No config found for model '{model_name}'. "
f"Available: {list(model_configs.keys())}"
)
n_estimators, max_depth, criterion = params
# If the key is supposed to always be present, use [] not .get()
# This raises KeyError immediately with the missing key name,
# which is much easier to debug than the unpacking TypeError.
try:
n_estimators, max_depth, criterion = model_configs['logistic_regression']
except KeyError as exc:
print(f"Missing model config: {exc}")
raise
The broader lesson: use dict.get() only when a missing key is a valid, expected
condition. When the key must be present for the program to function correctly, use
dict[key] so the error surfaces immediately at the right place.
When you see TypeError: cannot unpack non-iterable NoneType object and the cause
is not immediately obvious, insert a print(type(x)) (or
print(repr(x))) on the line immediately before the unpacking assignment. This
confirms whether the value is None and helps you trace back to where it was set.
import pandas as pd
def get_top_and_bottom(df, col, n=3):
df_sorted = df.sort_values(col, ascending=False, inplace=True) # BUG
top = df_sorted.head(n)
bottom = df_sorted.tail(n)
return top, bottom
df = pd.read_csv('scores.csv')
result = get_top_and_bottom(df, 'score')
# Diagnostic: add this before unpacking
print(type(result)) # <class 'NoneType'> <-- confirms it is None
print(repr(result)) # None
top, bottom = result # TypeError (now expected — trace back to the function)
# More precise diagnostic — check every variable in a pipeline
import pandas as pd
from sklearn.model_selection import train_test_split
def run_pipeline(path):
df = pd.read_csv(path)
X = df.drop('target', axis=1)
y = df['target']
splits = train_test_split(X, y, test_size=0.2)
# forgot to return
print(f"splits type: {type(splits)}") # tuple — but never reaches caller
result = run_pipeline('data.csv')
print(f"result type: {type(result)}") # NoneType — function returned None
X_train, X_test, y_train, y_test = result
# TypeError: cannot unpack non-iterable NoneType object
Once you confirm the value is None, use the traceback to find the last
assignment site for that variable. The bug is always at or before that assignment — either
a function that returned None, an in-place operation that was mistakenly
captured, or a lookup that found no result.
You can also use Python's built-in assert to catch this early in development:
result = some_function()
assert result is not None, f"some_function() returned None — check its return statements"
a, b = result
Across all five causes the safest general pattern is the same: verify the value is not
None before you unpack it. How you handle the None case depends on
your application logic — raise an error, use a default, skip the item, or log a warning.
# Template — adapt to your situation
result = function_that_might_return_none()
# Option 1: raise with a clear message
if result is None:
raise ValueError(
"function_that_might_return_none() returned None. "
"Check that it has a return statement and that all "
"code paths return a value."
)
a, b = result
# Option 2: use a default
if result is None:
result = (default_a, default_b)
a, b = result
# Option 3: skip / continue in a loop
items = [function_that_might_return_none(x) for x in data]
for item in items:
if item is None:
continue
a, b = item
process(a, b)
# Option 4: walrus operator (Python 3.8+)
if result := function_that_might_return_none():
a, b = result
else:
print("No result — skipping")
import re
import pandas as pd
from sklearn.model_selection import train_test_split
# --- Cause 1: pandas inplace ---
df = pd.read_csv('data.csv')
df_sorted = df.sort_values('score') # no inplace — returns DataFrame
assert df_sorted is not None
# --- Cause 2: function return ---
def process(df):
result = df.dropna().reset_index(drop=True)
return result # explicit return — not None
clean = process(df)
assert clean is not None, "process() must return a DataFrame"
# --- Cause 3: train_test_split ---
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=0
)
# 4 names = 2 arrays * 2 — always balanced
# --- Cause 4: regex ---
m = re.search(r'(\w+)=(\d+)', line)
if m is not None:
key, value = m.groups()
# --- Cause 5: dict.get ---
params = config.get('model_params')
if params is None:
raise KeyError("'model_params' missing from config")
learning_rate, n_layers = params
| Cause | Why It Returns None | Fix |
|---|---|---|
pandas inplace=True |
Modifies in place; by design returns None |
Remove inplace=True; reassign the result |
Missing return statement |
Python implicitly returns None at end of function body |
Add return on every code path; use type hints |
train_test_split wrapper |
Wrapper function calls split but forgets to return it | Always return train_test_split(...) from helpers |
re.match() / re.search() |
Returns None when pattern does not match |
Check if m is not None or use walrus operator |
dict.get() |
Returns None for missing keys by default |
Provide default, guard with if, or use dict[key] |
The diagnostic workflow is always the same: add print(type(x)) before the
crashing line, confirm None, then trace back to the assignment that set
x. In almost every case the root cause is one of these five patterns.
For other common data-pipeline errors in the same stack, see pandas KeyError in groupby and ValueError: NaN in sklearn estimators.