Pandas str.cat(): Combine Text with Missing-Value and Index Rules

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.

Join a whole Series into one string 🔝

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
str

Choose a separator 🔝

Use 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,Alex

Combine two columns row by row 🔝

When 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: str

Understand missing values in both modes 🔝

Without 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: string

Use na_rep for a deliberate display placeholder 🔝

na_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],B

Series values align by index labels 🔝

The 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: string

Compare left, inner, outer and right joins 🔝

join 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: string

Use an unlabelled list for intentional positional pairing 🔝

An 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: string

Combine three columns 🔝

Supply 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: str

Convert numeric components carefully 🔝

Use 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: str

Practical workflow: build validated product labels 🔝

This 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         Mug

Check combined keys and export columns separately 🔝

A 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 | Pen

Avoid separators around optional display parts 🔝

na_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: string

Choose cat(), join() or table concatenation 🔝

str.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.

Common questions 🔝

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.

Exercise: combine only valid identifiers 🔝

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.

Exercise solution 🔝

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: 2

Practice in Google Colab 🔝

Open 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.




Subscribe to our YouTube Channel here



plus2net.com







Python Video Tutorials
Python SQLite Video Tutorials
Python MySQL Video Tutorials
Python Tkinter Video Tutorials
✖
We use cookies to improve your browsing experience. . Learn more
HTML MySQL PHP JavaScript ASP Photoshop Articles Contact us
© 2000-2026 plus2net.com All rights reserved worldwide Privacy Policy Disclaimer