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.concatin a loop is still O(n²) — don't do it.list.append()still works — only the DataFrame method went away.- Add
ignore_index=Trueto get append's old fresh index.
The Direct Replacement
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=Truerebuilds a clean0..n-1index, 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
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 constructionrows.append(...)is a cheaplist.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 concatVersion 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 version | DataFrame.append() |
|---|---|
| ≤ 1.3 | Works, no warning |
| 1.4 – 1.5 | Works, FutureWarning (deprecated) |
| 2.0+ | Removed → AttributeError |
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.0If 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 indexKeeping 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
pd.__version__shows your version;appendcalls are gone from the code.- The previously-failing line now runs with
pd.concat. - Row counts match:
len(result) == len(df) + len(df2). - The index is what you expect (
ignore_index=Truefor a clean0..n-1). - 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.appendwithpd.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=Truewhen 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
| Symptom | Likely cause | Fix |
|---|---|---|
has no attribute 'append' | Pandas 2.0+ removed it | Use pd.concat |
| Slow after switching to concat | pd.concat inside a loop | Collect list, build once |
| Duplicate index values | No ignore_index | Add ignore_index=True |
Unexpected NaN columns | Mismatched column names | Align columns first |
| Works on one machine, not another | Different pandas versions | Pin the version |
Series append fails too | Series.append also removed | pd.concat([s1, s2]) |
_append warning/breakage | Using a private method | Switch to pd.concat |
Related Guides
- Fix ValueError: too many values to unpack (expected 2)
- Fix ImportError: cannot import name 'Mapping' from 'collections'
- Fix ModuleNotFoundError: No module named 'dotenv'
- Fix TypeError: Object of type datetime is not JSON serializable
- Migrating a codebase to pandas 2.0
External References
- pandas — What's new in 2.0.0 (removals)
- pandas — pd.concat
- pandas — DataFrame constructor
- pandas — Merge, join, concatenate
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.