New to Rust? Grab our free Rust for Beginners eBook Get it free →
10 ways to add a column to pandas DataFrames (with examples)

Adding a column to a pandas DataFrame comes up in almost every data cleaning or feature engineering task I’ve worked on, and pandas gives you more than one way to do it because each method fits a different situation. Some are quick one-liners, others let you control where the column lands, and a few handle conditional logic or dtype conversion along the way. Here are 10 ways to add a column in pandas, from the basic bracket assignment through assign(), conditional columns and dtype handling.
Setting up a DataFrame to work with
Every example below uses the same starting DataFrame so you can copy any section on its own and see the same result. I’m using a small table of employees with a name, a department and a salary, since that mix of text and numeric data makes it easy to show conditional logic and dtype conversion later on.
import pandas as pd
data = {
"name": ["Bruce", "Tony", "Natasha", "Steve"],
"department": ["Legal", "Engineering", "Security", "Ops"],
"salary": [72000, 98000, 81000, 69000]
}
df = pd.DataFrame(data)
print(df)
This creates a DataFrame from a dictionary, which is the most common way people build a DataFrame from lists and dictionaries when starting from raw Python data instead of a file. Running the code above gives you four rows and three columns to add to for the rest of this guide. I’m using pandas 3.0 for these examples, and every method here has worked the same way since at least pandas 1.x, so none of this depends on a recent release.
1. Assign a list with bracket notation
The fastest way to add a column pandas supports is direct bracket assignment. You name the column inside square brackets and assign a list, array or scalar to it. Pandas appends the new column to the end of the DataFrame and modifies it in place, so there’s no need to reassign the result.
df["bonus_eligible"] = [True, True, False, True]
print(df)
The list must have the same number of values as there are rows in the DataFrame, or pandas raises a ValueError. If you’re not sure how many rows you’re working with, check the row count first, since it’s an easy way to catch a length mismatch before it breaks your script. This method is destructive in the sense that it changes the original DataFrame directly rather than returning a copy, which is worth knowing if you’re passing the same DataFrame into other functions later in a pipeline.
2. Insert a column at a chosen position with insert()
Bracket assignment always puts the new column last. When you need it somewhere in the middle, insert() takes over. It accepts a position, a column name and the values to fill in.
df.insert(1, "employee_id", [101, 102, 103, 104])
print(df)
The first argument is the zero-based index where the column should land, so 1 places employee_id right after name. Pandas raises a ValueError if you pass a column name that already exists, unless you explicitly set allow_duplicates=True, which most people never need and should avoid since duplicate column names cause confusing lookups later. Like bracket assignment, insert() modifies the DataFrame in place. If you want to check where a column would land or confirm a name isn’t already taken, it helps to look at the current column names before calling insert.
3. Add a column with assign() without touching the original
assign() works differently from the first two methods. Instead of modifying the DataFrame directly, it returns a brand new DataFrame with the column added, and leaves the original untouched unless you reassign the result back to your variable.
df_with_tax = df.assign(after_tax_salary=df["salary"] * 0.7)
print(df_with_tax)
print(df)
Notice that df still has no after_tax_salary column after this runs. That’s the whole point of assign(), it’s non-destructive by default, which makes it a good fit for chained operations where you want to build up a series of transformations without losing the original data at any step. You can chain multiple assign() calls together, and each one sees the columns added by the calls before it in the chain. That chaining is where assign() earns its keep over plain bracket assignment, since you can write df.assign(a=...).assign(b=...) and read the whole transformation as a single expression.
4. Build a column from existing columns
A lot of the time, a new column isn’t independent data, it’s derived from columns you already have. Pandas handles this element-wise, meaning the operation runs across every row automatically without a loop.
df["salary_in_thousands"] = df["salary"] / 1000
print(df)
Arithmetic operators like +, -, * and / all work column to column this way, as do comparison operators like <, > and ==. If you needed the average salary across the whole department instead of a per-row value, you’d reach for a different tool since that’s an aggregation rather than an element-wise operation, and it’s worth knowing the difference before you try to force a groupby result into a bracket assignment. You can also combine more than two columns in a single expression, for example df["total_comp"] = df["salary"] + df["bonus"] once a bonus column exists.
5. Add a conditional column with np.where and loc
This is where “expand with conditional columns” comes in, and it’s one of the most common needs I run into on actual projects. You want a new column whose value depends on a condition in another column, like flagging high earners or short-tenure employees.
For a simple two-way split, numpy.where() is the cleanest option.
import numpy as np
df["pay_tier"] = np.where(df["salary"] >= 80000, "senior", "standard")
print(df)
np.where() takes a condition, a value for when it’s true and a value for when it’s false, and it works across the whole column at once. For conditions with more than two outcomes, .loc[] with boolean masks handles it just as well, applied one condition at a time.
df.loc[df["salary"] >= 90000, "pay_tier"] = "lead"
df.loc[(df["salary"] >= 70000) & (df["salary"] < 90000), "pay_tier"] = "senior"
df.loc[df["salary"] < 70000, "pay_tier"] = "standard"
print(df)
Each .loc[] line selects the rows matching a condition and only sets the column for those rows, leaving the rest as they were on a previous line. This reads a lot like a manual if-elif chain, just expressed as DataFrame operations. If you have more than three or four tiers, pd.cut() for numeric bins or numpy.select() for a list of conditions will scale better than a long chain of .loc[] calls. When your conditional logic gets complex enough that you’re filtering rows before deciding on a value, it can help to pull that subset out with a dedicated filter first rather than cramming everything into one boolean mask.
6. Add a column with apply() and a custom function
Sometimes a condition isn’t a simple comparison, it’s real logic that’s easier to express as a Python function. apply() runs a function against every value in a column or every row in a DataFrame, depending on the axis you choose.
def grade_bonus(salary):
if salary >= 90000:
return salary * 0.15
elif salary >= 75000:
return salary * 0.1
else:
return salary * 0.05
df["bonus"] = df["salary"].apply(grade_bonus)
print(df)
Here apply() runs grade_bonus() against every value in the salary Series, since we called it on a single column. If you need access to more than one column inside the function, call apply() on the whole DataFrame with axis=1, which passes each row as an argument instead.
def flag_review(row):
return row["salary"] < 70000 and row["department"] == "Ops"
df["needs_review"] = df.apply(flag_review, axis=1)
print(df)
apply() is flexible but slower than the vectorized options above, since it calls your Python function once per row instead of running compiled numpy operations across the whole column. For a small DataFrame like this one, the difference is invisible. On a dataset with millions of rows, swapping apply() for np.where() or a direct arithmetic expression can cut runtime by a large factor, so it’s worth reaching for vectorized methods first whenever the logic allows it.
7. Map a column from a dictionary
When the new column’s values come from a lookup table rather than a formula, map() with a dictionary is the shortest path. It matches each value in an existing column against dictionary keys and returns the corresponding value.
manager_lookup = {
"Legal": "Pepper",
"Engineering": "Tony",
"Security": "Fury",
"Ops": "Maria"
}
df["manager"] = df["department"].map(manager_lookup)
print(df)
If a value in the source column doesn’t exist as a key in the dictionary, pandas fills that row with NaN instead of raising an error, so it’s worth checking for unmatched values after running this if your lookup table might be incomplete. map() only works on a single Series, not the whole DataFrame, which makes it a good fit for this kind of one-to-one recoding but the wrong tool if you need logic that spans more than one input column.
8. Add multiple columns in one go
Adding one column at a time is fine for small edits, but if you’re bringing in three or four related columns, doing it as a batch keeps the code shorter and avoids repeating yourself. assign() accepts multiple keyword arguments at once.
df = df.assign(
tax_rate=0.22,
net_salary=df["salary"] * 0.78,
is_manager=df["department"].isin(["Legal", "Security"])
)
print(df)
Assigning a list of columns directly
Bracket notation supports batch assignment too, as long as you’re adding columns of the same shape at once.
df[["region", "office_floor"]] = [["East", 4], ["West", 2], ["East", 4], ["Central", 1]]
print(df)
Each inner list becomes one row across the two new columns, so the outer list needs exactly as many entries as there are rows in the DataFrame. This is a useful shortcut when your new data is already shaped like a small table, for example data pulled from an API response or a second CSV keyed by the same row order.
9. Control the dtype of a new column
This is the other piece the original version of this article was missing. Adding a column doesn’t guarantee it has the dtype you expect, especially when the source data is a mix of strings and numbers, or came from a CSV where everything defaults to object type. Getting this wrong quietly breaks later math or sorting, so it’s worth setting the dtype explicitly rather than assuming pandas inferred it correctly.
df["salary_str"] = df["salary"].astype(str)
print(df["salary_str"].dtype)
df["salary_back_to_int"] = df["salary_str"].astype(int)
print(df["salary_back_to_int"].dtype)
astype() works well when the data is already clean and consistently formatted. It breaks down the moment a column has stray characters, like a currency symbol or a comma in a number, since astype(int) has no way to strip those out on its own. For messier input, pd.to_numeric() is the safer choice because it accepts an errors argument.
messy = pd.Series(["72000", "98,000", "not available", "69000"])
cleaned = pd.to_numeric(messy.str.replace(",", ""), errors="coerce")
print(cleaned)
Setting errors="coerce" turns anything pandas can’t parse into NaN instead of raising an exception and stopping the whole operation. That trade-off matters, since you get a column back but need to check for missing values afterward rather than assuming every row converted cleanly. For datetime columns, the equivalent function is pd.to_datetime(), and for a full-DataFrame cleanup pass, df.convert_dtypes() will try to infer the best nullable dtype for every column at once, which is a fast way to tidy up a frame you loaded from an inconsistent source.
10. Add columns at scale without fragmenting the DataFrame
Every method above works fine for adding a handful of columns. If you’re adding dozens or hundreds, one at a time, in a loop, pandas will warn you about something called DataFrame fragmentation, and the loop gets noticeably slower with every iteration.
df_loop = pd.DataFrame({"id": range(3)})
for i in range(150):
df_loop[f"col_{i}"] = 0
Running that produces a PerformanceWarning once you pass roughly 100 columns added this way. The fix is to build all the new columns as a separate DataFrame first, then combine it with the original in one pass using pd.concat() with axis=1, the same approach you’d use when combining two DataFrames side by side.
new_cols = pd.DataFrame({f"col_{i}": [0, 0, 0] for i in range(150)})
df_loop = pd.concat([df_loop, new_cols], axis=1)
This runs in a single internal operation instead of 150 separate ones, and in practice it’s an order of magnitude faster once the column count climbs into the hundreds. It’s not a change you’ll notice on a four-column example DataFrame, but it’s the difference between a script that finishes instantly and one that visibly slows down as a loop grinds through a wide dataset.
Key takeaways
- Bracket assignment is the fastest way to add a column and modifies the DataFrame directly
- insert() is the only method that lets you choose the column’s position
- assign() returns a new DataFrame and supports clean method chaining
- np.where() and .loc[] handle conditional columns without writing a full function
- apply() is flexible but slower than vectorized alternatives on large data
- map() is the shortest path when values come from a lookup dictionary
- assign() and bracket batch assignment both add several columns in one step
- astype() suits clean data, pd.to_numeric() with errors=”coerce” suits messy data
- Adding many columns in a loop triggers a fragmentation warning past around 100 columns
- pd.concat(axis=1) is the fix for adding a large batch of columns efficiently
Frequently asked questions
What’s the fastest way to add a column to a pandas DataFrame?
Bracket assignment, like df["new_col"] = values, is the fastest and most common way. It modifies the DataFrame in place and needs no extra function calls.
What’s the difference between assign() and bracket assignment?
Bracket assignment changes the DataFrame directly. assign() returns a new DataFrame with the column added, leaving the original unchanged unless you reassign the result.
How do I add a column based on a condition in pandas?
Use np.where() for a two-way condition, or .loc[] with a boolean mask for more than two outcomes. Both apply the logic across the whole column at once.
Why did my new column end up with the wrong data type?
Pandas infers dtype automatically, and mixed or messy source data often ends up as object type. Use astype() for clean data or pd.to_numeric() with errors=”coerce” for messy values.
Can I add multiple columns to a DataFrame at the same time?
Yes. assign() accepts several keyword arguments in one call, and bracket notation accepts a list of column names paired with a matching list of row values.
Does adding columns in a loop slow pandas down?
Yes, past roughly 100 columns added one at a time, pandas issues a fragmentation warning. Building the columns separately and combining them with pd.concat() avoids the slowdown.
Is insert() faster than assign() for adding one column?
Not meaningfully. insert() places the column anywhere and edits the DataFrame directly. assign() always appends the column and returns a new copy instead.
Conclusion
Adding a column is one of those pandas operations that looks trivial until the data gets messy or the DataFrame gets wide, and that’s usually when picking the right method out of these 10 actually matters.




