Fix AttributeError: 'DataFrame' object has no attribute 'append'

Quick answer

AttributeError: 'DataFrame' object has no attribute 'append' means you are on pandas 2.0 or newer, which removed DataFrame.append() and Series.append(). Replace df.append(df2) with pd.concat([df, df2], ignore_index=True). If you were appending inside a loop, don't just swap in pd.concat — that is also quadratic. Collect your rows as dicts in a Python list and build the DataFrame once with pd.DataFrame(rows).

Your pandas code worked for years, you upgraded, and now:

AttributeError: 'DataFrame' object has no attribute 'append'

Nothing is wrong with your DataFrame. The method is simply gone: DataFrame.append() and Series.append() were deprecated in pandas 1.4 and removed in pandas 2.0. Any df.append(...) call now raises this AttributeError.

The one-line fix is pd.concat. But there's a bigger trap most guides walk you straight into: if you were appending inside a loop, swapping in pd.concat keeps the same quadratic slowness that made append a bad idea in the first place. This guide fixes both — correctly.

Quick Answer

You're on pandas 2.0+, which removed DataFrame.append(). Replace df.append(df2) with pd.concat([df, df2], ignore_index=True). If you were appending in a loop, don't just swap in pd.concat (still O(n²)) — collect rows as dicts in a Python list and build the DataFrame once with pd.DataFrame(rows).

TL;DR

  • Pandas 2.0 removed DataFrame.append() / Series.append().
  • One frame: pd.concat([df, df2], ignore_index=True).
  • In a loop: collect dicts in a list → pd.DataFrame(rows) (O(n)).
  • pd.concat in a loop is still O(n²) — don't do it.
  • list.append() still works — only the DataFrame method went away.
  • Add ignore_index=True to get append's old fresh index.

The Direct Replacement

Migrating away from DataFrame.append(): replace it with pd.concat()DataFrame.append and Series.append were removed in pandas 2.0. Replace df.append(df2) with pd.concat of a list, adding ignore_index to reset the index, and replace appending a single row with pd.concat of the frame and a new one-row DataFrame.✗ Removed in pandas 2.0✓ Use insteaddf.append(df2)pd.concat([df, df2], ignore_index=True)df.append(row_dict, ...)pd.concat([df, pd.DataFrame([row_dict])])s1.append(s2)pd.concat([s1, s2])
Migrating away from DataFrame.append(): replace it with pd.concat()

Wherever you called .append() on a DataFrame or Series, use pd.concat with a list of objects:

import pandas as pd
 
# ❌ removed in pandas 2.0
combined = df.append(df2)
 
# ✅ pandas 2.0+
combined = pd.concat([df, df2], ignore_index=True)
  • pd.concat([...]) takes a list — that's how you pass two or more frames.
  • ignore_index=True rebuilds a clean 0..n-1 index, matching append's common behavior. Leave it off and you keep the original indices (which can duplicate).

Appending a single row

The old single-row append({...}, ignore_index=True) becomes a concat with a one-row frame:

# ❌ old
df = df.append({"name": "Ada", "score": 95}, ignore_index=True)
 
# ✅ new
new_row = pd.DataFrame([{"name": "Ada", "score": 95}])
df = pd.concat([df, new_row], ignore_index=True)

This works, but if you're doing it repeatedly, read the next section — there's a much faster pattern.

⚠️

list.append() is not affected — only the DataFrame/Series method was removed. The shared name causes confusion: my_list.append(x) is fine and is exactly what you'll use to collect rows below. my_dataframe.append(x) is the one that's gone.

The Real Trap: Appending in a Loop

Appending in a loop is quadratic; collect rows in a list and build the DataFrame onceReassigning df with append or concat inside a loop copies the whole growing DataFrame every iteration, which is quadratic time. Instead append plain dicts to a Python list, then build the DataFrame a single time, which is linear.append/concat in a loop → O(n²)for row in data: df = pd.concat( [df, one_row])# copies the whole df# every iteration✗ 10k rows → millions of copiescollect, then build once → O(n)rows = []for row in data: rows.append(row_dict)df = pd.DataFrame(rows)# one build at the end✓ linear time
Appending in a loop is quadratic; collect rows in a list and build the DataFrame once

Here's what most "just use concat" answers miss. Both append and pd.concat create a new DataFrame every call — they copy all existing rows. Inside a loop, that's quadratic:

# ❌ O(n²) — copies the whole growing DataFrame every iteration
df = pd.DataFrame()
for record in records:
    df = pd.concat([df, pd.DataFrame([record])], ignore_index=True)

For 10,000 rows this copies tens of millions of rows in total. The fix is to build the DataFrame once, after collecting rows in a plain list:

# ✅ O(n) — collect dicts, build once
rows = []
for record in records:
    rows.append({"name": record.name, "score": record.score})
 
df = pd.DataFrame(rows)      # single construction
  • rows.append(...) is a cheap list.append — no copying.
  • pd.DataFrame(rows) builds the frame in one pass at the end.

Rule of thumb: never grow a DataFrame row by row. Accumulate in a Python list (or a list of small DataFrames) and concatenate/construct once. This was true before pandas 2.0 too — the removal just forced everyone to notice.

Concatenating many frames

If you genuinely have many DataFrames (e.g. one per file), collect them in a list and concat once — not in the loop:

frames = [pd.read_csv(f) for f in files]   # list of DataFrames
df = pd.concat(frames, ignore_index=True)  # single concat

Version Check and Timeline

Confirm what you're on and why it changed:

import pandas as pd
print(pd.__version__)     # 2.x → append is gone; 1.x → append still (deprecated)
Pandas versionDataFrame.append()
≤ 1.3Works, no warning
1.4 – 1.5Works, FutureWarning (deprecated)
2.0+RemovedAttributeError

You may see suggestions to use df._append() (with an underscore). It's a private method that still exists but is undocumented and can change or vanish without notice. Don't use it in real code — use pd.concat.

Edge Cases

Mismatched columns

pd.concat aligns by column name, just like append did. Missing columns become NaN:

a = pd.DataFrame({"x": [1]})
b = pd.DataFrame({"y": [2]})
pd.concat([a, b], ignore_index=True)
#      x    y
# 0  1.0  NaN
# 1  NaN  2.0

If you expect identical columns, validate them before concatenating.

Concatenating along columns

append only added rows. For side-by-side (column) joins, use axis=1 (or a proper merge):

pd.concat([df_left, df_right], axis=1)   # add columns, align on index

Keeping a downgrade temporary

If you must ship today, pip install "pandas<2.0" restores append — but treat it as a stopgap and migrate to pd.concat, since you'll be stuck off security and performance fixes otherwise.

Verification Steps

  1. pd.__version__ shows your version; append calls are gone from the code.
  2. The previously-failing line now runs with pd.concat.
  3. Row counts match: len(result) == len(df) + len(df2).
  4. The index is what you expect (ignore_index=True for a clean 0..n-1).
  5. Loops build the DataFrame once at the end, not per iteration.

Green state: no .append() on any DataFrame/Series, loops accumulate into a list and build once, and results have the expected shape and index.

Prevention

  • Replace every DataFrame.append / Series.append with pd.concat.
  • Never call pd.concat (or append) inside a loop — collect then build once.
  • Prefer pd.DataFrame(list_of_dicts) for row accumulation.
  • Add ignore_index=True when you want a fresh index.
  • Pin a pandas version range in requirements to avoid surprise upgrades.
  • Avoid the private _append; it's not a supported API.

Troubleshooting Matrix

SymptomLikely causeFix
has no attribute 'append'Pandas 2.0+ removed itUse pd.concat
Slow after switching to concatpd.concat inside a loopCollect list, build once
Duplicate index valuesNo ignore_indexAdd ignore_index=True
Unexpected NaN columnsMismatched column namesAlign columns first
Works on one machine, not anotherDifferent pandas versionsPin the version
Series append fails tooSeries.append also removedpd.concat([s1, s2])
_append warning/breakageUsing a private methodSwitch to pd.concat

External References

FAQs

Is pd.concat slower than the old append? For a single call, they're comparable — both copy. The performance win comes from how often you call it: once at the end (fast) versus every loop iteration (slow). The removal nudged people toward the once-at-the-end pattern.

Do I need ignore_index=True every time? Only when you want a fresh 0..n-1 index. If the existing indices are meaningful and unique, leave it off. If they'd collide or you don't care, add it — it's the safest default when stacking rows.

Can I append a dict directly like before? Not with a method call. Wrap it: pd.concat([df, pd.DataFrame([the_dict])], ignore_index=True). Better, collect dicts in a list and do one pd.DataFrame(list_of_dicts).

What about Series.append? Removed as well. Use pd.concat([s1, s2]) for Series. The same list-then-concat guidance applies if you're building a Series incrementally.

Why was append removed at all? It encouraged the quadratic row-by-row pattern and duplicated pd.concat's functionality with subtly different behavior. Removing it simplified the API and steered users toward the faster approach.

Key takeaways

  • Pandas 2.0 removed DataFrame.append() and Series.append() — the method no longer exists.
  • Replace df.append(df2) with pd.concat([df, df2], ignore_index=True).
  • Do NOT call pd.concat inside a loop — that is O(n²), the same trap append was.
  • The correct loop pattern: append dicts to a list, then pd.DataFrame(rows) once — O(n).
  • Python's list.append() still works — only the DataFrame/Series method was removed.
  • Pin pandas<2.0 only as a temporary stopgap; migrate to concat for the long term.

Frequently asked questions

Why did DataFrame.append() stop working?

It was deprecated in pandas 1.4 and removed in pandas 2.0 (April 2023). Upgrading pandas past 2.0 makes any df.append() call raise AttributeError: 'DataFrame' object has no attribute 'append'. Replace it with pd.concat([...]) to fix it.

What replaces DataFrame.append() in pandas 2.0?

pd.concat. For two frames use pd.concat([df, df2], ignore_index=True). For a single new row use pd.concat([df, pd.DataFrame([row_dict])], ignore_index=True). Add ignore_index=True to get the fresh 0..n-1 index that append gave you by default.

How do I append rows to a DataFrame in a loop now?

Don't build the DataFrame inside the loop. Append each row as a dict to a plain Python list, then create the DataFrame once after the loop with pd.DataFrame(rows). This is linear time; calling append or pd.concat every iteration is quadratic because it copies the whole frame each time.

Is list.append() also removed in pandas 2.0?

No. Only DataFrame.append() and Series.append() were removed. Python's built-in list.append() is unaffected and is exactly what you should use to collect rows before building a DataFrame. The shared method name is a common source of confusion.

Can I keep using append by downgrading pandas?

You can pin pandas<2.0 as a short-term stopgap, but it's not recommended — you miss bug fixes, performance, and new features, and you'll have to migrate eventually. Replacing append with pd.concat is a small change; do that instead of freezing an old version.

Does pd.concat reset the index like append did?

Only if you ask it to. append(..., ignore_index=True) reset the index by default in many uses; pd.concat keeps the original indices unless you pass ignore_index=True. Add it when you want a clean 0..n-1 index, or you'll get duplicate index labels.


Related Posts