Use str.slice(start=None, stop=None, step=None) to extract part of each string in a Series or Index. Positions start at zero, stop is exclusive, and negative bounds count from the end. Validate the source format before using extracted fields.
Positions begin at zero. start=1 skips the first character. The source Series is unchanged.
import pandas as pd
df = pd.DataFrame({"NAME": ["Ravi", "Raju", "Alex"]})
print(df["NAME"].str.slice(start=1))
assert df["NAME"].str.slice(start=1).tolist() == ["avi", "aju", "lex"]Expected output
0 avi
1 aju
2 lex
Name: NAME, dtype: strA negative start counts from the right. start=-1 returns the last character, while start=-2 returns up to two trailing characters.
print(df["NAME"].str.slice(start=-1))
print(df["NAME"].str.slice(start=-2))
assert df["NAME"].str.slice(start=-1).tolist() == ["i", "u", "x"]Expected output
0 i
1 u
2 x
Name: NAME, dtype: str
0 vi
1 ju
2 ex
Name: NAME, dtype: strstop=2 selects positions zero and one. The character at position two is excluded. Omitting start uses the beginning for a positive step.
print(df["NAME"].str.slice(stop=2))
assert df["NAME"].str.slice(stop=2).tolist() == ["Ra", "Ra", "Al"]Expected output
0 Ra
1 Ra
2 Al
Name: NAME, dtype: strstart=1, stop=3 selects positions one and two. Bounds describe positions within each string, not DataFrame rows.
print(df["NAME"].str.slice(start=1, stop=3))
assert df["NAME"].str.slice(start=1, stop=3).tolist() == ["av", "aj", "le"]Expected output
0 av
1 aj
2 le
Name: NAME, dtype: strA positive step specifies the interval between selected positions. step=2 starts at zero by default.
print(df["NAME"].str.slice(step=2))
assert df["NAME"].str.slice(step=2).tolist() == ["Rv", "Rj", "Ae"]Expected output
0 Rv
1 Rj
2 Ae
Name: NAME, dtype: strThe stop remains exclusive even when a step is supplied. Strings shorter than the stop are sliced only as far as their length.
names = pd.Series(["RaviKing", "RajuQueen", "AlexMinister"])
print(names.str.slice(start=1, stop=10, step=2))
assert names.str.slice(start=1, stop=10, step=2).tolist() == ["aiig", "auue", "lxiit"]Expected output
0 aiig
1 auue
2 lxiit
dtype: strs.str[1:3] is shorthand for character slicing. s.iloc[1:3] selects Series rows instead. Bracket indexing is not a function call; see iloc row selection.
s = pd.Series(["Ravi", "Raju", "Alex"], index=[10, 20, 30], dtype="string")
print(s.str[1:3])
print("Rows selected with iloc:")
print(s.iloc[1:3])
assert s.str[1:3].equals(s.str.slice(1, 3))Expected output
10 av
20 aj
30 le
dtype: string
Rows selected with iloc:
20 Raju
30 Alex
dtype: stringstep=-1 reverses the character sequence when bounds are omitted. A zero step is invalid. Reversing a displayed word is a demonstration, not a general cleaning rule.
s = pd.Series(["ABC123", "pen", ""], dtype="string")
print(s.str.slice(step=-1))
assert s.str.slice(step=-1).tolist() == ["321CBA", "nep", ""]Expected output
0 321CBA
1 nep
2
dtype: stringSlicing preserves missing text. Out-of-range bounds produce shorter or empty strings rather than confirming valid input. Use str.len() when the source must have a required length.
s = pd.Series(["AB1234", "A", "", None], dtype="string")
result = s.str.slice(start=2, stop=6)
print(result)
assert result.iloc[:3].tolist() == ["1234", "", ""]
assert pd.isna(result.iloc[3])Expected output
0 1234
1
2
3 <NA>
dtype: stringMixed object input is not reliable text data. Converting with astype("string") preserves missing entries and turns numbers into text deliberately. It cannot restore leading zeros already lost during numeric import. See type conversion.
s = pd.Series(["0012", 1234, None], dtype="object")
text = s.astype("string")
print(text.str.slice(stop=2))
assert text.str.slice(stop=2).iloc[:2].tolist() == ["00", "12"]Expected output
0 00
1 12
2 <NA>
dtype: stringThis synthetic schema requires two uppercase ASCII letters followed by four digits. Trim surrounding whitespace, validate the entire value and then extract fields. Slicing alone would accept partial or malformed codes. Keep rejected values for review.
raw = pd.DataFrame({"code": [" AB0012 ", "CD0456", "A123", "EF12X4", None, "GH0070extra"]})
code = raw["code"].astype("string").str.strip()
valid = code.str.fullmatch(r"[A-Z]{2}[0-9]{4}", na=False)
accepted = raw.loc[valid].copy()
accepted["code"] = code.loc[valid]
accepted["category"] = accepted["code"].str.slice(stop=2)
accepted["item_id"] = accepted["code"].str.slice(start=2, stop=6)
print(accepted)
print("Review rows:")
print(raw.loc[~valid])
assert accepted["item_id"].tolist() == ["0012", "0456"]Expected output
code category item_id
0 AB0012 AB 0012
1 CD0456 CD 0456
Review rows:
code
2 A123
3 EF12X4
4 NaN
5 GH0070extraKeep identifiers as text so their leading zeros remain intact. The checks below verify the schema after extraction. For delimiter-separated fields, prefer str.split(). For matching text, see str.contains().
assert accepted["category"].str.len().eq(2).all()
assert accepted["item_id"].str.len().eq(4).all()
assert (accepted["category"] + accepted["item_id"]).equals(accepted["code"])
print(accepted.to_csv(index=False).rstrip())
# Optional Colab file export:
# accepted.to_csv("validated_code_fields.csv", index=False)Expected output
code,category,item_id
AB0012,AB,0012
CD0456,CD,0456String slicing also works on an Index. If the result will become a key or column label, check uniqueness because shortening labels may create collisions.
labels = pd.Index(["AB0012", "AB0456", "CD0070"], name="product")
prefixes = labels.str.slice(stop=2)
print(prefixes)
print("Prefixes unique:", prefixes.is_unique)
assert not prefixes.is_uniqueExpected output
Index(['AB', 'AB', 'CD'], dtype='str', name='product')
Prefixes unique: FalseUse character slicing when positions are defined by the data specification. Use split() when delimiters define fields, and regex extraction when structure varies. For dates, use to_datetime() rather than assuming a fixed displayed date format. String positions are not UTF-8 byte offsets; combining characters can make displayed-character boundaries differ from simple slicing. Preserve readable names and international text.
Why is the final character missing? stop is exclusive. Why did I get an empty string? Check source length, bounds and step direction. Does slicing edit my column? No; assign the result explicitly if desired. Can every row have different bounds? This method takes shared bounds; use a deliberate per-row approach for variable offsets. Is str.slice() the same as iloc? No; one slices text and the other selects rows by position.
Create codes ORD-0012, ORD-0456, bad and a missing value. Require ORD- followed by exactly four digits. Extract the four-digit identifier and retain invalid rows for review. Expect two accepted codes.
Check the full format before extracting a suffix. The last four characters alone would not prove a valid prefix.
orders = pd.DataFrame({"code": pd.Series(["ORD-0012", "ORD-0456", "bad", None], dtype="string")})
valid_order = orders["code"].str.fullmatch(r"ORD-[0-9]{4}", na=False)
answer = orders.loc[valid_order].copy()
answer["id"] = answer["code"].str.slice(start=-4)
print(answer)
print("Review rows:", int((~valid_order).sum()))
assert answer["id"].tolist() == ["0012", "0456"]Expected output
code id
0 ORD-0012 0012
1 ORD-0456 0456
Review rows: 2Open in Google Colab View on GitHub
Run the examples in order, change slice bounds and sample codes, then compare extracted values and review rows. Save your own copy to keep edits. All sample tables are included in the notebook.
Continue with the Data Cleaning and String Operations hub, string length, splitting text and combining strings.
Reference: Pandas Series.str.slice 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.