Use str.split() to separate text into pieces, and expand=True to turn those pieces into columns. Choose a literal or regex delimiter explicitly, limit splits where needed and validate the resulting fields before using them.
With no delimiter, split() separates runs of whitespace and ignores surrounding whitespace. The result contains lists, one per source value. Splitting a personal name does not reliably identify first and last names in every naming system.
import pandas as pd
df = pd.DataFrame({"name": ["Ravi King", "Raju Queen", "Alex Jack"]})
print(df["name"].str.split())Expected output
0 [Ravi, King]
1 [Raju, Queen]
2 [Alex, Jack]
Name: name, dtype: objectChoose regex=False when the delimiter is literal. This demonstration separates simple addresses at @; splitting is not a complete email validation method.
df = pd.DataFrame({"email": ["Ravi@example.com", "Raju@example.com", "Alex@example.com"]})
print(df["email"].str.split("@", regex=False))Expected output
0 [Ravi, example.com]
1 [Raju, example.com]
2 [Alex, example.com]
Name: email, dtype: objectexpand=True returns a DataFrame for a Series input. Columns initially have integer labels. A missing separator may produce fewer pieces for a row, and absent pieces are padded with missing values.
parts = df["email"].str.split("@", n=1, expand=True, regex=False)
print(parts)
print("User part:")
print(parts[0])
print("Domain part:")
print(parts[1])Expected output
0 1
0 Ravi example.com
1 Raju example.com
2 Alex example.com
User part:
0 Ravi
1 Raju
2 Alex
Name: 0, dtype: str
Domain part:
0 example.com
1 example.com
2 example.com
Name: 1, dtype: strA missing source value remains missing. str.get(0) retrieves the first element of each list without expanding all pieces. Empty text and a missing value are distinct.
email = pd.Series(["Ravi@example.com", "Raju@example.com", None, "Alex@example.com"], dtype="string")
split = email.str.split("@", regex=False)
print(split)
print(split.str.get(0))
assert pd.isna(split.str.get(0).iloc[2])Expected output
0 [Ravi, example.com]
1 [Raju, example.com]
2 <NA>
3 [Alex, example.com]
dtype: object
0 Ravi
1 Raju
2 <NA>
3 Alex
dtype: objectn=1 performs at most one split from the left. The remainder stays in the second piece. n=-1, None and 0 request all splits; zero does not mean no splits.
email = pd.Series(["id.Ravi@example.co.in", "id.Raju@example.co.in", None, "id.Alex@example.co.in"], dtype="string")
print(email.str.split(".", n=1, expand=True, regex=False))
assert email.str.split(".", n=1, regex=False).str.get(1).iloc[0] == "Ravi@example.co.in"Expected output
0 1
0 id Ravi@example.co.in
1 id Raju@example.co.in
2 <NA> <NA>
3 id Alex@example.co.inrsplit() is useful when the last separator has a special meaning. Unlike split(), rsplit() has no regex parameter and uses a literal separator. The final domain segment in this example is in.
print(email.str.rsplit(".", n=1, expand=True))
assert email.str.rsplit(".", n=1).str.get(1).iloc[0] == "in"Expected output
0 1
0 id.Ravi@example.co in
1 id.Raju@example.co in
2 <NA> <NA>
3 id.Alex@example.co inWith regex=None, a one-character pattern is treated literally while longer patterns are interpreted as regex. Avoid relying on that length-dependent behavior when the delimiter comes from configuration. Non-capturing groups avoid inserting captured separator text into the output.
text = pd.Series(["red|blue;green", "black;white", None], dtype="string")
print("Literal pipe:")
print(text.str.split("|", regex=False))
print("Either separator:")
print(text.str.split(r"[|;]", regex=True))Expected output
Literal pipe:
0 [red, blue;green]
1 [black;white]
2 <NA>
dtype: object
Either separator:
0 [red, blue, green]
1 [black, white]
2 <NA>
dtype: objectExplicit separators retain empty pieces from repeated or trailing delimiters. A value without a delimiter produces one piece. Reindex the expanded result when a fixed output schema is required, because the observed number of pieces may change with the input.
values = pd.Series(["a,,b", "a,", "solo", "", None], dtype="string")
print(values.str.split(",", regex=False))
parts = values.str.split(",", n=1, expand=True, regex=False).reindex(columns=[0, 1])
parts.columns = ["first", "remainder"]
print(parts)Expected output
0 [a, , b]
1 [a, ]
2 [solo]
3 []
4 <NA>
dtype: object
first remainder
0 a ,b
1 a
2 solo <NA>
3 <NA>
4 <NA> <NA>For these fixed-depth absolute URLs, splitting on / yields scheme, an empty separator piece, host, directory and file. This positional approach assumes the same path depth and no extra slash. A URL parser is safer for variable paths, queries and fragments.
pages = pd.DataFrame({"Page": [
"https://www.plus2net.com/html_tutorial/button-linking.php",
"https://www.plus2net.com/c-tutorial/grade.php",
"https://www.plus2net.com/sql_tutorial/between-date.php",
"https://www.plus2net.com/php_tutorial/variables2.php",
"https://www.plus2net.com/sql_tutorial/sql_like.php",
"https://www.plus2net.com/sql_tutorial/sql_sum-multiple.php",
"https://www.plus2net.com/sql_tutorial/date-lastweek.php",
"https://www.plus2net.com/sql_tutorial/sql_max.php",
"https://www.plus2net.com/sql_tutorial/sql_count.php",
"https://www.plus2net.com/html_tutorial/html_marquee_behvr.php",
"https://www.plus2net.com/javascript_tutorial/clock.php",
"https://www.plus2net.com/php_tutorial/php_drop_down_list.php"
]})
pieces = pages["Page"].str.split("/", expand=True, regex=False)
print("Directories:")
print(pieces[3])
print("Files:")
print(pieces[4])
assert pieces.shape[1] == 5
pages[["scheme", "separator", "host", "directory", "file"]] = pieces
print(pages[["directory", "file"]].head(3))Expected output
Directories:
0 html_tutorial
1 c-tutorial
2 sql_tutorial
3 php_tutorial
4 sql_tutorial
5 sql_tutorial
6 sql_tutorial
7 sql_tutorial
8 sql_tutorial
9 html_tutorial
10 javascript_tutorial
11 php_tutorial
Name: 3, dtype: str
Files:
0 button-linking.php
1 grade.php
2 between-date.php
3 variables2.php
4 sql_like.php
5 sql_sum-multiple.php
6 date-lastweek.php
7 sql_max.php
8 sql_count.php
9 html_marquee_behvr.php
10 clock.php
11 php_drop_down_list.php
Name: 4, dtype: str
directory file
0 html_tutorial button-linking.php
1 c-tutorial grade.php
2 sql_tutorial between-date.phpUse the observed output columns to create labels when path depth varies. Keep source index alignment when joining. Avoid collisions with existing column names; see column validation.
paths = pd.DataFrame({"page": ["python/list.php", "python/data/clean.php", "index.php"]})
pieces = paths["page"].str.split("/", expand=True, regex=False)
pieces.columns = [f"page_id{i + 1}" for i in range(pieces.shape[1])]
print(paths.join(pieces))Expected output
page page_id1 page_id2 page_id3
0 python/list.php python list.php NaN
1 python/data/clean.php python data clean.php
2 index.php index.php NaN NaNurlsplit separates URL structure so queries and fragments do not become part of the filename. The example keeps an empty filename for a directory URL ending in /. Do not infer a filename where none exists.
from urllib.parse import urlsplit
urls = pd.Series(["https://www.plus2net.com/python/list.php?q=list#example", "https://www.plus2net.com/python/", "https://www.plus2net.com/python/data/clean.php"], dtype="string")
parsed = urls.map(urlsplit)
paths = parsed.map(lambda item: item.path)
print(pd.DataFrame({"path": paths, "file": paths.str.rsplit("/", n=1).str.get(1)}))Expected output
path file
0 /python/list.php list.php
1 /python/
2 /python/data/clean.php clean.phpThis synthetic extract expects exactly one | separator between code and label. Validate delimiter count before splitting; n=1 alone would hide extra separators in the remainder. Keep identifiers as text so leading zeros remain intact.
sales = pd.DataFrame({"order_id": [101, 102, 103, 104, 105], "product": ["0012 | Notebook A5", "0045|Pen blue", "0070", None, "0081|Pencil|extra"]})
text = sales["product"].astype("string")
separator_count = text.str.count(r"\|")
structure_ok = separator_count.eq(1).fillna(False)
accepted = sales.loc[structure_ok].copy()
parts = accepted["product"].astype("string").str.split("|", n=1, expand=True, regex=False).reindex(columns=[0, 1])
accepted["code"] = parts[0].str.strip()
accepted["label"] = parts[1].str.strip()
fields_ok = accepted[["code", "label"]].notna().all(axis=1) & accepted["code"].ne("") & accepted["label"].ne("")
print(accepted.loc[fields_ok])
print("Invalid structure IDs:", sales.loc[~structure_ok, "order_id"].tolist())
assert accepted["code"].tolist() == ["0012", "0045"]Expected output
order_id product code label
0 101 0012 | Notebook A5 0012 Notebook A5
1 102 0045|Pen blue 0045 Pen blue
Invalid structure IDs: [103, 104, 105]Malformed structure, missing input and empty fields require separate review. Splitting does not validate code format or establish that a product exists. Apply the relevant rules before export; see length checks and Excel export.
print("Missing product inputs:", text.isna().sum())
print("Present but wrong separator count:", (text.notna() & ~structure_ok).sum())
clean = accepted.loc[fields_ok].copy()
print("Accepted rows:", len(clean))
assert len(clean) == 2
# Optional Colab export:
# clean.to_csv("split_product_fields.csv", index=False)Expected output
Missing product inputs: 1
Present but wrong separator count: 2
Accepted rows: 2partition() separates around the first occurrence and always returns three pieces: before, separator and after. Use it when delimiter presence itself matters. For quoted CSV text containing delimiters inside fields, use read_csv() with CSV quoting support instead of naive splitting.
values = pd.Series(["A:B:C", "solo", None], dtype="string")
print(values.str.partition(":"))Expected output
0 1 2
0 A : B:C
1 solo
2 <NA> <NA> <NA>Why did assignment raise a column-count error? The expanded width differs from your expected schema; validate and reindex deliberately. Why is there an empty piece? A repeated or trailing literal delimiter creates it. Does splitting validate email? No. Why did a delimiter act as regex? Specify regex=False for literal matching. Can I split names into first and last reliably? Not universally; keep original names and follow your data specification. How do I flatten tag lists into rows? explode() is appropriate when one tag per row is intended, but increases row count.
Create values "A1:Blue pen", "B2:Notebook", "bad" and a missing value. Require exactly one colon, split and trim the fields, and preserve rejected inputs for review. Expect two accepted rows.
Validate the structure before expansion and reject empty fields after trimming.
raw = pd.DataFrame({"item": pd.Series(["A1:Blue pen", "B2:Notebook", "bad", None], dtype="string")})
valid = raw["item"].str.count(":").eq(1).fillna(False)
result = raw.loc[valid].copy()
parts = result["item"].str.split(":", n=1, expand=True, regex=False).reindex(columns=[0, 1])
result["code"] = parts[0].str.strip()
result["label"] = parts[1].str.strip()
assert result["code"].ne("").all() and result["label"].ne("").all()
print(result)
print("Review rows:", (~valid).sum())
assert len(result) == 2Expected output
item code label
0 A1:Blue pen A1 Blue pen
1 B2:Notebook B2 Notebook
Review rows: 2Open in Google Colab View on GitHub
Run the examples in order, change delimiters and source values, then compare split fields and rejected rows. Save your own copy to keep edits. All sample tables are included in the notebook.
Continue with data cleaning, text matching, case conversion, slicing and combining text.
Reference: Pandas Series.str.split 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.