Fix TypeError: cannot unpack non-iterable NoneType object in Python

Fix TypeError: cannot unpack non-iterable NoneType object in Python

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.


1. The Error — Two Real Tracebacks

The message is always the same, but it surfaces in very different contexts. Here are two representative tracebacks you are likely to encounter.

Variant A — In-place pandas operation

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

Variant B — User function missing a return statement

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.


2. What "Unpack" Means in Python

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

Cause 1: In-Place pandas Operations Return None

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.

The Anti-Pattern

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.

Fix — Drop inplace=True, Reassign Instead

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.

Chaining Is the Modern Idiom

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())

Cause 2: Function Missing a Return Statement

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.

The Anti-Pattern

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

Fix — Add the Missing return

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

Cause 3: train_test_split — Forgot to Assign All Return Values

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.

The Anti-Pattern — Single Variable Then Unpack 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

The Anti-Pattern — Wrong Number of Receivers

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)

Fix — Match Receivers to Inputs

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.


Cause 4: re.match() / re.search() Returning None

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.

The Anti-Pattern

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)

Fix — Guard with an if Check

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.


Cause 5: dict.get() Returning None

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.

The Anti-Pattern

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

Fix Option A — Provide a Default in dict.get()

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

Fix Option B — Guard with if Before Unpacking

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

Fix Option C — Use dict[] Subscript When the Key Must Exist

# 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.


Diagnostic Pattern: Print type() Before Unpacking

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

General Fix Pattern: Guard with if result is not None

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")

Applying the Guard to Each Cause

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

Summary

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.