Pandas str.split(): Separate Text into Validated Columns

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.

Split names on whitespace 🔝

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

Split email text at a literal delimiter 🔝

Choose 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: object

Expand split results into columns 🔝

expand=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: str

Preserve missing values and select pieces with get() 🔝

A 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: object

Limit the number of splits with n 🔝

n=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.in

rsplit() 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    in

Choose literal or regex splitting explicitly 🔝

With 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: object

Inspect empty tokens and absent separators 🔝

Explicit 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>

Split simple URLs into their components 🔝

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

Name dynamically generated split columns 🔝

Use 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        NaN

Parse URLs before splitting their paths 🔝

urlsplit 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.php

Practical workflow: separate a combined product field 🔝

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

Review rejected rows and export validated fields 🔝

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

Choose split(), partition() or a file parser 🔝

partition() 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>

Common questions and mistakes 🔝

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.

Exercise: split a code and label safely 🔝

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.

Exercise solution 🔝

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) == 2

Expected output

          item code     label
0  A1:Blue pen   A1  Blue pen
1  B2:Notebook   B2  Notebook
Review rows: 2

Practice in Google Colab 🔝

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




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