New to Rust? Grab our free Rust for Beginners eBook Get it free →
Pandas append: Why df.append is gone and how to add rows now

Pandas append used to be the first method every tutorial reached for when joining two DataFrames. The df.append() method got deprecated in pandas 1.4.0 and removed completely in pandas 2.0, so calling it today throws an AttributeError. This guide covers what replaced it, how to add rows and DataFrames the current way and how to fix old scripts still calling df.append().
What was df.append() in pandas
The append() method added the rows of one DataFrame to the end of another and returned a brand new DataFrame, a two-dimensional table built from rows and columns. It never modified the original object.
Before pandas 2.0, this was a normal part of any pandas append workflow. Below is the syntax as it existed on the DataFrame class, useful mainly for reading old code.
Syntax:
DataFrame.append(other, ignore_index=False, verify_integrity=False, sort=False)
Parameters:
- other: the DataFrame, Series, dictionary or list of these that holds the rows to add.
- ignore_index: if True, the result gets a fresh 0, 1, 2 index. If False, the original index labels carry over and can repeat.
- verify_integrity: if True, pandas raises a ValueError when the result would contain duplicate index labels.
- sort: if True, columns get sorted alphabetically when the two DataFrames don’t line up. Default is False since pandas 1.0.0.
Columns present in other but missing from the caller were added automatically, with missing values filled as NaN. That behavior still applies to the modern replacement.
Why pandas append was removed in pandas 2.0
The pandas core team flagged append() as inefficient. Each call built a whole new DataFrame, so appending inside a loop meant repeated memory allocation on every iteration.
pd.concat() does the same job without that overhead, and it also handles joining more than two DataFrames or joining along columns instead of rows. Since one function already covered everything append() did, keeping both around long term just added confusing overlap to the API.
The deprecation notice appeared in pandas 1.4.0, and the method stayed usable with a warning through the 1.x releases. Pandas 2.0 dropped it outright, so any environment running pandas 2.0 or newer no longer has DataFrame.append() at all.
How to append rows in pandas now with concat
pd.concat() is the direct replacement for pandas append. It lives on the pandas module itself rather than on a single DataFrame, so the call looks like pd.concat([df1, df2]) instead of df1.append(df2).
import pandas as pd
df1 = pd.DataFrame({"age": [16, 14, 10], "qualified": [True, True, True]})
df2 = pd.DataFrame({"age": [55, 40], "qualified": [True, False]})
combined = pd.concat([df1, df2], ignore_index=True)
print(combined)
Output:
age qualified
0 16 True
1 14 True
2 10 True
3 55 True
4 40 False
The ignore_index=True argument works exactly like it did on append(), replacing the old index labels with a clean sequential one. Drop that argument and the original index values from both frames carry over, which can create duplicates.
Appending rows inside a loop the modern way
Most pandas append tutorials jump straight to pd.concat() inside a loop and stop there. That still works, but it isn’t the fastest fix.
The quickest upgrade skips DataFrame construction inside the loop entirely. Collect plain dictionaries in a list, then build the DataFrame once after the loop ends.
import pandas as pd
orders = []
for order_id in range(1, 6):
row = {"order_id": order_id, "amount": order_id * 25, "status": "paid"}
orders.append(row)
orders_df = pd.DataFrame(orders)
print(orders_df)

No pd.concat() shows up anywhere in that loop. orders is a normal Python list, and .append() there is the built-in list method, not the removed DataFrame one.
pd.DataFrame(orders) only runs once, after the loop finishes, so pandas never rebuilds a table on every iteration. For loops that generate one row at a time, this beats concatenating in the loop.
Sometimes each iteration produces a DataFrame already, rather than a plain dictionary, for example when reading several small files or API pages inside a loop. That case still needs concat, just called once outside the loop body.
import pandas as pd
monthly_frames = []
for month in ["jan", "feb", "mar"]:
frame = pd.DataFrame({"month": [month], "revenue": [12000]})
monthly_frames.append(frame)
yearly_df = pd.concat(monthly_frames, ignore_index=True)
print(yearly_df)

monthly_frames collects one small DataFrame per loop pass. pd.concat() runs a single time after the loop, joining all three into one table with a clean index.
The rule of thumb: if the loop is producing rows, skip DataFrames until the very end and build one from a list of dictionaries. If it’s producing DataFrames already, collect them in a list and concatenate once.
Migrating old append() patterns to concat
Every classic pandas append pattern has a direct concat equivalent. This table maps the ones that show up most often in older code, so a rewrite is a search-and-replace rather than a rethink.
| Old pattern (removed) | New pattern (pandas 2.0+) |
|---|---|
df1.append(df2) | pd.concat([df1, df2]) |
df1.append(df2, ignore_index=True) | pd.concat([df1, df2], ignore_index=True) |
df1.append([df2, df3]) | pd.concat([df1, df2, df3]) |
df.append(pd.Series(row_dict), ignore_index=True) | pd.concat([df, pd.DataFrame([row_dict])], ignore_index=True) |
df = pd.DataFrame() then df = df.append(row, ignore_index=True) in a loop | Collect row dictionaries in a list, then pd.DataFrame(list_of_rows) once |
The middle three rows are near-identical swaps. The last two change shape slightly, since concat expects an iterable of DataFrame-like objects rather than a single Series or dict.
Appending DataFrames with different columns
Real datasets rarely line up column for column. When the two DataFrames being combined have different columns, pandas keeps every column from both sides and fills the gaps with NaN.
import pandas as pd
a = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
b = pd.DataFrame({"a": [5], "c": [7]})
result = pd.concat([a, b], ignore_index=True)
print(result)
Output:
a b c
0 1.0 3.0 NaN
1 2.0 4.0 NaN
2 5.0 NaN 7.0
Row 2 came from b, which never had a b column, so pandas fills that cell with NaN. The same happens for column c on the rows that came from a.
Numeric columns switch to float dtype once NaN shows up, since integers can’t represent missing values. If those NaN rows need to be removed afterward, filtering the DataFrame with a null check handles that cleanly.
Appending a single row to a DataFrame
Series-based row appends were common with the old append() method. The concat-based replacement wraps the new row in a single-row DataFrame first.
import pandas as pd
df = pd.DataFrame({"Name": ["Alice", "Bob"], "Age": [25, 30]})
new_row = pd.DataFrame({"Name": ["Charlie"], "Age": [35]})
df = pd.concat([df, new_row], ignore_index=True)
print(df)
Output:
Name Age
0 Alice 25
1 Bob 30
2 Charlie 35
Wrapping the new data as pd.DataFrame({...}) instead of pd.Series({...}) keeps the syntax consistent with every other concat call in this guide. It also avoids a dtype warning that pandas raises when concatenating an empty or all-NaN DataFrame with a Series.
For a single row added occasionally, this pattern works fine. For dozens of individual rows being added in sequence, updating existing rows instead of re-adding them can be a cleaner fit depending on the task.
Appending columns instead of rows
Everything so far stacks DataFrames on top of each other. pd.concat() can also join them side by side using axis=1.
import pandas as pd
names = pd.DataFrame({"Name": ["Alice", "Bob"]})
ages = pd.DataFrame({"Age": [25, 30]})
result = pd.concat([names, ages], axis=1)
print(result)
Output:
Name Age
0 Alice 25
1 Bob 30
axis=0, the default, stacks rows. axis=1 lines DataFrames up by their index and adds new columns instead. This only produces clean results when both DataFrames share the same index, otherwise mismatched rows fill with NaN.
For adding a single new column from a list or a calculation rather than joining two full DataFrames, assigning it directly is usually the simpler option.
Pandas append vs concat: what actually changed
The two functions solve the same problem, so the migration is mostly a syntax change rather than a logic change.
other.append(other2)becomespd.concat([other, other2]).ignore_indexandsortbehave the same way on both.verify_integritystill exists as a concat argument, kept for the same duplicate-index check.- concat lives on the pandas module (
pd.concat), not on a single DataFrame, since it can take more than two frames at once.
The only real functional gain is flexibility. append() could only add one object to a single DataFrame. concat() accepts a full list of DataFrames or Series and stitches them together in one pass, along either axis.
If a project is still running on an older pandas release, checking the installed version quickly confirms whether df.append() is even available before debugging further.
Performance: why looping still needs care
Switching every append() call to concat() doesn’t automatically fix performance. Calling pd.concat() inside a loop, once per iteration, still rebuilds a DataFrame every single time.
import pandas as pd
import time
start = time.time()
df = pd.DataFrame(columns=["A"])
for i in range(2000):
df = pd.concat([df, pd.DataFrame({"A": [i]})], ignore_index=True)
print("Looped concat:", time.time() - start)
start = time.time()
rows = [{"A": i} for i in range(2000)]
df = pd.DataFrame(rows)
print("Single build:", time.time() - start)
The first block calls concat() 2,000 times, so pandas reallocates memory on every pass. The second block collects plain dictionaries in a Python list and builds the DataFrame exactly once.
That second pattern is the same list-then-build approach used earlier in this guide, and it’s the reason that pattern beats the naive loop-and-append style, regardless of which function does the actual appending.
Fixing old code that still calls df.append()
Upgrading pandas and hitting AttributeError: 'DataFrame' object has no attribute 'append' is the most common way people run into this change. The fix is almost always mechanical.
# Old code, breaks on pandas 2.0+
result = df1.append(df2, ignore_index=True)
# Fixed
result = pd.concat([df1, df2], ignore_index=True)
Wrap every appended object in a list, swap the method call for pd.concat and keep every other argument as it was. Search the codebase for .append( calls on DataFrame variables specifically, since plain Python lists still have their own valid .append() method that shouldn’t be touched.
Key takeaways
- df.append() was deprecated in pandas 1.4.0 and removed in pandas 2.0.
- pd.concat() is the direct replacement for every pandas append use case.
- Wrap objects in a list for concat, since it takes an iterable, not a single argument.
- Collecting dictionaries in a list and building the DataFrame once beats concat inside a loop.
- ignore_index and sort behave the same way on both functions.
- Mismatched columns get filled with NaN automatically.
- axis=1 joins DataFrames by column instead of by row.
- Check the installed pandas version if df.append() still seems to work.
Frequently asked questions
Is df.append() still available in pandas?
No. It was deprecated in pandas 1.4.0 and removed entirely in pandas 2.0. Calling it on a current install raises an AttributeError.
What replaced pandas append?
pd.concat() replaced it completely. It takes a list of DataFrames or Series and combines them along rows or columns in a single call.
Why does my code say DataFrame has no attribute append?
The installed pandas version is 2.0 or newer, where append() was removed. Replace df.append(other) with pd.concat([df, other]).
Does pd.concat() support ignore_index like append() did?
Yes. Passing ignore_index=True to pd.concat() resets the resulting index to a clean sequential range, exactly like it did on the old append() method.
Can I append columns instead of rows with concat?
Yes. Passing axis=1 to pd.concat() joins DataFrames side by side by column instead of stacking their rows.
Is looping with concat slow like looping with append() was?
Yes, calling concat() once per loop iteration still rebuilds the DataFrame each time. Collecting rows in a list and concatenating once is faster.
What’s the fastest way to build a DataFrame inside a loop?
Collect plain dictionaries in a Python list during the loop, then call pd.DataFrame() on the whole list once. Skip concat entirely for this case.
Conclusion
Pandas append is gone as a DataFrame method, but the underlying task never went away. pd.concat() handles every case df.append() used to, with a small syntax change and better performance on large loops.




