Use str.cat() to combine all values into one string, or supply another column to combine text row by row. Choose a separator and missing-value policy, and check index alignment when combining separate Series.
With no others argument, str.cat() combines the values into one Python string. The default separator is empty. It does not add a column or change the source.
import pandas as pd
df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"]})
result = df["NAME"].str.cat()
print(result)
print(type(result).__name__)
assert result == "RaviRajuAlex"Expected output
RaviRajuAlex
strUse sep for spaces, commas or another display separator. A comma-joined string is not a CSV file with quoting and escaping; use to_csv() to export tabular data.
print(df["NAME"].str.cat(sep=" "))
print(df["NAME"].str.cat(sep=","))
assert df["NAME"].str.cat(sep=" ") == "Ravi Raju Alex"Expected output
Ravi Raju Alex
Ravi,Raju,AlexWhen others is supplied, the operation combines corresponding values and returns a Series for Series input. The example demonstrates address construction, not email validation or proof of deliverability.
df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"], "DOMAIN": ["example.com", "example.net", "example.org"]})
addresses = df["NAME"].str.cat(df["DOMAIN"], sep="@")
print(addresses)
assert addresses.tolist() == ["Ravi@example.com", "Raju@example.net", "Alex@example.org"]Expected output
0 Ravi@example.com
1 Raju@example.net
2 Alex@example.org
Name: NAME, dtype: strWithout others, missing elements are skipped by default. With others, a missing component makes that row result missing unless na_rep is provided. Empty strings are present values and still contribute separators.
left = pd.Series(["A", None, ""], dtype="string")
right = pd.Series(["1", "2", "3"], dtype="string")
print("Whole Series:", left.str.cat(sep="|"))
print("Row combinations:")
print(left.str.cat(right, sep="-"))
assert left.str.cat(sep="|") == "A|"
assert pd.isna(left.str.cat(right, sep="-").iloc[1])Expected output
Whole Series: A|
Row combinations:
0 A-1
1 <NA>
2 -3
dtype: stringna_rep replaces each missing component during concatenation. A placeholder can make incomplete data visible, but does not make an address valid. See fillna() for separate missing-value changes.
df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex", None], "DOMAIN": ["example.com", "example.net", "example.org", "example.co"]})
print(df["NAME"].str.cat(df["DOMAIN"], sep="@", na_rep="#"))
assert df["NAME"].str.cat(df["DOMAIN"], sep="@", na_rep="#").iloc[3] == "#@example.co"
print(pd.Series(["A", None, "B"], dtype="string").str.cat(sep=",", na_rep="[missing]"))Expected output
0 Ravi@example.com
1 Raju@example.net
2 Alex@example.org
3 #@example.co
Name: NAME, dtype: str
A,[missing],BThe default join is left. Reordering the other Series does not switch to position matching. Unique, meaningful labels help prevent accidental cross-row combinations.
codes = pd.Series(["A", "B"], index=[10, 20], dtype="string")
labels = pd.Series(["second", "first"], index=[20, 10], dtype="string")
result = codes.str.cat(labels, sep=":")
print(result)
assert result.tolist() == ["A:first", "B:second"]Expected output
10 A:first
20 B:second
dtype: stringjoin controls the index used when others has labels. Missing labels introduce missing components. Use na_rep only when a display placeholder is intended, rather than concealing unmatched records.
left = pd.Series(["A", "B"], index=[10, 20], dtype="string")
right = pd.Series(["two", "three"], index=[20, 30], dtype="string")
for mode in ["left", "inner", "outer", "right"]:
print(mode)
print(left.str.cat(right, sep=":", join=mode, na_rep="?"))
assert left.str.cat(right, sep=":", join="inner").to_dict() == {20: "B:two"}Expected output
left
10 A:?
20 B:two
dtype: string
inner
20 B:two
dtype: string
outer
10 A:?
20 B:two
30 ?:three
dtype: string
right
20 B:two
30 ?:three
dtype: stringAn unlabelled list has no index to align and must match the caller length. Converting a Series to a list discards its labels; do so only when row order has been verified. See position-based selection.
codes = pd.Series(["A", "B"], index=[10, 20], dtype="string")
print(codes.str.cat(["first", "second"], sep=":"))
assert codes.str.cat(["first", "second"], sep=":").tolist() == ["A:first", "B:second"]Expected output
10 A:first
20 B:second
dtype: stringSupply a list of Series to combine multiple components with the same separator. A DataFrame is also accepted as others. This is string combination, not a relational join of tables.
parts = pd.DataFrame({"region": ["IN", "UK"], "category": ["BOOK", "PEN"], "id": ["0012", "0045"]})
keys = parts["region"].str.cat([parts["category"], parts["id"]], sep="-")
print(keys)
assert keys.tolist() == ["IN-BOOK-0012", "UK-PEN-0045"]Expected output
0 IN-BOOK-0012
1 UK-PEN-0045
Name: region, dtype: strUse nullable string conversion to preserve missing entries. Numeric import may already have removed leading zeros; keep identifiers as text from the start. Explicit formatting rules are needed when numbers represent amounts or dates.
table = pd.DataFrame({"label": ["Order", "Order", "Order"], "id": pd.Series([101, 102, None], dtype="Int64")})
text_id = table["id"].astype("string")
print(table["label"].str.cat(text_id, sep=" "))
assert pd.isna(text_id.iloc[2])Expected output
0 Order 101
1 Order 102
2 NaN
Name: label, dtype: strThis synthetic schema requires a two-letter region, a four-digit identifier and a non-blank description. Trim text, validate each component and preserve rejected rows before combining. Use length checks and case conversion where appropriate.
raw = pd.DataFrame({"region": [" in ", "UK", "US", None], "id": ["0012", "0045", "bad", "0070"], "description": ["Notebook", " Pen ", "Pencil", "Mug"]})
cleaned = raw.astype("string")
cleaned["region"] = cleaned["region"].str.strip().str.upper()
cleaned["id"] = cleaned["id"].str.strip()
cleaned["description"] = cleaned["description"].str.strip()
valid = (cleaned["region"].str.fullmatch(r"[A-Z]{2}", na=False)
& cleaned["id"].str.fullmatch(r"[0-9]{4}", na=False)
& cleaned["description"].notna() & cleaned["description"].ne("")).fillna(False)
accepted = cleaned.loc[valid].copy()
accepted["key"] = accepted["region"].str.cat(accepted["id"], sep="-")
accepted["display"] = accepted["key"].str.cat(accepted["description"], sep=" | ")
print(accepted[["key", "display"]])
print("Review rows:")
print(raw.loc[~valid])
assert accepted["key"].tolist() == ["IN-0012", "UK-0045"]Expected output
key display
0 IN-0012 IN-0012 | Notebook
1 UK-0045 UK-0045 | Pen
Review rows:
region id description
2 US bad Pencil
3 NaN 0070 MugA separator alone does not guarantee a unique or reversible key. Validate allowed component formats and check uniqueness. Keep source columns in exports rather than relying on splitting display labels later. See splitting fields and Excel export.
assert accepted["key"].is_unique
assert accepted["display"].notna().all()
print("Accepted:", len(accepted), "Review:", int((~valid).sum()))
print(accepted.to_csv(index=False).rstrip())
# Optional Colab export:
# accepted.to_csv("product_labels.csv", index=False)Expected output
Accepted: 2 Review: 2
region,id,description,key,display
IN,0012,Notebook,IN-0012,IN-0012 | Notebook
UK,0045,Pen,UK-0045,UK-0045 | Penna_rep="" retains the separator, so it can leave a leading or trailing separator. For a two-part optional label, combine when both parts exist and choose the present part otherwise. Blank strings should be normalised first.
first = pd.Series(["A", "B", None, None], dtype="string")
second = pd.Series(["one", None, "three", None], dtype="string")
display = first.str.cat(second, sep=" - ")
display = display.where(first.notna() & second.notna(), first.fillna(second))
print(display)
assert display.iloc[:3].tolist() == ["A - one", "B", "three"]
assert pd.isna(display.iloc[3])Expected output
0 A - one
1 B
2 three
3 <NA>
dtype: stringstr.cat() combines string values across a Series or aligned components. str.join() joins elements inside each list-like value; applying it to a string joins individual characters. pd.concat() combines pandas objects. A DataFrame merge matches table records by keys. Choose the operation according to the structure, not just the word join.
Why did a row become missing? Check missing components and unmatched index labels. Why were rows paired differently from their displayed order? Series align by labels. Why is a separator left beside a blank? Empty strings are not missing, and na_rep replaces values without removing separators. Can this create a valid email? It can construct text; validate components and follow the email tutorial. Will cat() edit my column? Assign its result explicitly to store it.
Create prefixes AB, CD and a missing value, with identifiers 0012, bad and 0070. Require two uppercase letters and four digits, then combine with a hyphen. Keep invalid records for review. Expect one accepted key.
Validate both components before joining, instead of replacing missing identifiers with a placeholder.
practice = pd.DataFrame({"prefix": ["AB", "CD", None], "id": ["0012", "bad", "0070"]}).astype("string")
ok = practice["prefix"].str.fullmatch(r"[A-Z]{2}", na=False) & practice["id"].str.fullmatch(r"[0-9]{4}", na=False)
answer = practice.loc[ok].copy()
answer["key"] = answer["prefix"].str.cat(answer["id"], sep="-")
print(answer)
print("Review rows:", int((~ok).sum()))
assert answer["key"].tolist() == ["AB-0012"]Expected output
prefix id key
0 AB 0012 AB-0012
Review rows: 2Open in Google Colab View on GitHub
Run the examples in order, change separators, missing components and index labels, then compare the results. Save your own copy to keep edits. All sample tables are included in the notebook.
Continue with the Data Cleaning and String Operations hub, splitting strings, extracting substrings and text matching.
Reference: Pandas Series.str.cat documentation.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.