2.4. Strings#
A great deal of business data is text: customer names, email addresses, product codes, and free-text survey answers. Text that people type is often messy. The same answer can arrive as "Yes", " yes ", and "YES", and a product code such as "SKU-1024-BLUE" packs several facts into one string.
Python Syntax introduced strings: how to create them with quotes, join them with +, repeat them with *, and measure them with len(). This section adds the tools you need to work with text data:
select parts of a string by position,
clean text with string methods,
split a string into pieces and join pieces back together,
format numbers as currency and percentages, and
loop over the characters and words in a string.
These skills carry forward. In Chapter 4, pandas applies the same methods to a whole column of text at once through .str, for example df["email"].str.lower().
2.4.1. Indexing and Slicing Strings#
A string is a sequence of characters, so it uses the same indexing and slicing rules as a list (Lists): positions start at 0, negative indexes count from the end, and a slice [start:stop] includes start but not stop. Spaces and punctuation count as characters too.
Many business identifiers have a fixed layout, so slicing can pull out each part.
code = "SKU-1024-BLUE"
print(len(code)) ### 13 characters, including the hyphens
print(code[0]) ### 'S', the first character
print(code[-1]) ### 'E', the last character
print(code[:3]) ### 'SKU', positions 0, 1, 2
print(code[4:8]) ### '1024', positions 4 to 7
print(code[-4:]) ### 'BLUE', the last four characters
13
S
E
SKU
1024
BLUE
Slicing works the same way on any fixed-layout value. In this invoice ID, positions 3 to 6 hold the year and the last five characters hold the invoice number.
invoice_id = "INV2026-00042"
year = invoice_id[3:7]
number = invoice_id[-5:]
print("Year:", year)
print("Number:", number)
Year: 2026
Number: 00042
### Exercise: Slice an Order ID
# Order IDs use the layout ORD-YYYY-MM-NNNN.
# 1. Use slicing to extract the prefix, year, month, and sequence number.
# 2. Print each part.
# Print exactly:
# Prefix: ORD
# Year: 2026
# Month: 10
# Sequence: 0587
order_id = "ORD-2026-10-0587"
### Your code starts here.
### Your code ends here.
Prefix: ORD
Year: 2026
Month: 10
Sequence: 0587
2.4.2. Cleaning Text with String Methods#
A string method is a function that belongs to a string. You call it with the dot operator, just like the list methods in Lists: text.method(). String methods do not change the original string. Each one returns a new string, which you can print or save.
2.4.2.1. Changing Case#
lower(), upper(), and title() return copies of a string in lowercase, uppercase, or title case (first letter of each word capitalized). Converting to one case before comparing makes "Yes" and "YES" count as the same answer.
name = "aLiCe johnson"
print(name.lower()) ### 'alice johnson'
print(name.upper()) ### 'ALICE JOHNSON'
print(name.title()) ### 'Alice Johnson'
print("YES" == "yes") ### False: case matters
print("YES".lower() == "yes") ### True after converting the case
alice johnson
ALICE JOHNSON
Alice Johnson
False
True
2.4.2.2. Removing Extra Spaces#
strip() removes whitespace (spaces, tabs, and newline characters) from both ends of a string; lstrip() and rstrip() remove it from only the left or right end. Spaces in the middle are kept. Square brackets in the output below show where each string starts and ends.
response = " Yes \n"
print(f"[{response}]") ### spaces and a newline around Yes
print(f"[{response.strip()}]") ### [Yes]
print(f"[{response.lstrip()}]") ### only the left side is removed
[ Yes
]
[Yes]
[Yes
]
Methods can be chained: each method runs on the result of the one before it. strip().lower() is a common first step for cleaning survey answers.
response = " Yes \n"
clean = response.strip().lower()
print(clean) ### 'yes'
yes
2.4.2.3. Replacing Text#
replace(old, new) returns a copy with every occurrence of old replaced by new. Replacing with the empty string "" deletes characters. A price exported as text, such as "$1,250.00", must lose its $ and , before type conversion with float().
price_text = "$1,250.00"
digits = price_text.replace("$", "").replace(",", "")
price = float(digits)
print(digits) ### '1250.00', still a string
print(price * 2) ### 2500.0, now a number
1250.00
2500.0
2.4.2.4. Searching Text#
Several tools answer questions about what a string contains:
Tool |
Returns |
Example |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
index of the first match, or |
|
email = "pat.lee@example.com"
print("@" in email) ### True
print(email.endswith(".com")) ### True
print(email.startswith("sales")) ### False
print(email.find("@")) ### 7, the index of "@"
print(email.find("#")) ### -1, not found
True
True
False
7
-1
Because find() returns a position, you can combine it with slicing. Everything after the @ is the email domain.
at = email.find("@")
domain = email[at + 1:]
print(domain) ### 'example.com'
example.com
### Exercise: Clean Survey Responses
# 1. Create an empty list named cleaned.
# 2. Loop over responses; strip spaces and convert each answer to lowercase.
# 3. Append each cleaned answer to cleaned.
# 4. Count how many cleaned answers equal "yes".
# Print exactly:
# Cleaned: ['yes', 'no', 'yes', 'maybe', 'yes']
# Yes count: 3
responses = [" Yes", "no ", "YES", " Maybe ", "yes"]
### Your code starts here.
### Your code ends here.
Cleaned: ['yes', 'no', 'yes', 'maybe', 'yes']
Yes count: 3
2.4.3. Splitting and Joining#
Lists introduced split(), which breaks a string into a list, and join(), which combines a list of strings into one string. The character that marks the boundary between pieces is called the delimiter. With tuple unpacking, one line can split a product code and name each part.
code = "SKU-1024-BLUE"
parts = code.split("-")
print(parts) ### ['SKU', '1024', 'BLUE']
prefix, number, color = code.split("-")
print("Number:", number)
print("Color:", color)
['SKU', '1024', 'BLUE']
Number: 1024
Color: BLUE
The same pattern parses an email address or one line of comma-separated data. Every piece that split() returns is a string, so convert numeric pieces with int() or float() before doing arithmetic.
user, domain = "pat.lee@example.com".split("@")
print(user, "|", domain)
line = "Alice,Sales,52000"
name, department, salary = line.split(",")
salary = int(salary) ### '52000' becomes 52000
print(f"{name} works in {department}; a 3% raise is {salary * 0.03}")
pat.lee | example.com
Alice works in Sales; a 3% raise is 1560.0
join() is called on the delimiter, and its argument is the list of strings to combine. It is the reverse of split().
first_names = ["Ana", "Ben", "Chloe"]
print(", ".join(first_names)) ### Ana, Ben, Chloe
print(" | ".join(first_names)) ### Ana | Ben | Chloe
email = ".".join(["pat", "lee"]) + "@example.com"
print(email) ### pat.lee@example.com
Ana, Ben, Chloe
Ana | Ben | Chloe
pat.lee@example.com
join() accepts only strings. To join numbers, convert each one with str() first.
### Exercise: Parse Product Codes
# 1. Create an empty list named numbers.
# 2. Loop over codes; split each code on "-" and unpack it into prefix, number, and color.
# 3. Print the number and color, and append the number to numbers.
# 4. Join numbers with " | " and print the result.
# Print exactly:
# 1024: BLUE
# 2048: RED
# 4096: BLUE
# Product numbers: 1024 | 2048 | 4096
codes = ["SKU-1024-BLUE", "SKU-2048-RED", "SKU-4096-BLUE"]
### Your code starts here.
### Your code ends here.
1024: BLUE
2048: RED
4096: BLUE
Product numbers: 1024 | 2048 | 4096
2.4.4. Formatting Numbers with f-strings#
Python Syntax used f-strings such as f"Total: {total}" to place values inside text. A business report also needs numbers to look right: two decimal places for money, thousands separators, and percent signs. Add a format specifier after a colon inside the braces: {value:spec}.
revenue = 1234567.891
print(f"{revenue}") ### raw value
print(f"{revenue:.2f}") ### 2 decimal places
print(f"{revenue:,.2f}") ### thousands separator + 2 decimals
print(f"${revenue:,.2f}") ### currency: $ is plain text before the braces
print(f"{revenue:,.0f}") ### rounded to whole dollars
1234567.891
1234567.89
1,234,567.89
$1,234,567.89
1,234,568
The % specifier multiplies a number by 100 and adds a percent sign, so store rates as decimals (0.084, not 8.4).
growth = 0.084
print(f"{growth:.1%}") ### 8.4%
print(f"{growth:.0%}") ### 8%
8.4%
8%
The most useful specifiers are summarized below.
Specifier |
Meaning |
Example |
Result |
|---|---|---|---|
|
fixed point, 2 decimals |
|
|
|
thousands separator, 2 decimals |
|
|
|
percent, 1 decimal |
|
|
|
left-align in 10 characters |
|
|
|
right-align in 10 characters |
|
|
|
right-align and format |
|
|
A width and an alignment (< left, > right) line values up in columns, which turns a few print() calls into a small report. Text columns are usually left-aligned and number columns right-aligned. This example uses zip() from Lists to walk through regions and sales together.
regions = ["North", "South", "East", "West"]
sales = [125000, 98500.5, 143250.75, 87000]
print(f"{'Region':<10}{'Sales':>14}")
for region, amount in zip(regions, sales):
print(f"{region:<10}{amount:>14,.2f}")
print(f"{'Total':<10}{sum(sales):>14,.2f}")
Region Sales
North 125,000.00
South 98,500.50
East 143,250.75
West 87,000.00
Total 453,751.25
### Exercise: Format a Sales Summary
# 1. Use an f-string with a currency format for sales.
# 2. Use an f-string with a percent format (1 decimal) for margin.
# Print exactly:
# Region: Midwest
# Sales: $48,250.50
# Margin: 18.3%
region = "Midwest"
sales = 48250.5
margin = 0.183
### Your code starts here.
### Your code ends here.
Region: Midwest
Sales: $48,250.50
Margin: 18.3%
2.4.5. Strings Are Immutable#
Strings are immutable, like tuples: once a string exists, its characters cannot be changed. Assigning to an index raises a TypeError.
product = "apple"
%%expect TypeError
product[0] = "A"
TypeError: 'str' object does not support item assignment
To get a different string, build a new one and assign it to a name.
product = "A" + product[1:] ### a new string from pieces
print(product) ### 'Apple'
Apple
This is why every string method returns a new string. A common mistake is to call a method and forget to save the result: the original string stays the same.
name = " Ana Lopez "
name.strip() ### result is computed, then thrown away
print(f"[{name}]") ### still has the spaces
name = name.strip() ### save the new string under the same name
print(f"[{name}]")
[ Ana Lopez ]
[Ana Lopez]
### Exercise: Save the Cleaned Value
# The code below calls upper() but does not save the result.
# 1. Fix it so that region_code refers to the uppercase string.
# Print exactly:
# Region code: MW
region_code = "mw"
### Your code starts here.
region_code.upper()
### Your code ends here.
print("Region code:", region_code)
Region code: mw
Region code: MW
2.4.6. Looping Over Characters and Words#
A for loop over a string visits one character at a time. String methods such as isdigit() test a single character, so a loop can count or collect characters of one kind. Functions returns to character loops.
phone = "(555) 010-4477"
digits = 0
for character in phone:
if character.isdigit():
digits += 1
print("Digits:", digits) ### 10
Digits: 10
To work with words instead of characters, split() the text first and loop over the list. Cleaning the text before splitting, with lower() and replace(), makes "Great" and "great," count as the same word.
review = "Great product, fast shipping, great price"
words = review.lower().replace(",", "").split()
print(words)
print("Word count:", len(words))
great_count = 0
for word in words:
if word == "great":
great_count += 1
print("Times 'great' appears:", great_count)
['great', 'product', 'fast', 'shipping', 'great', 'price']
Word count: 6
Times 'great' appears: 2
Counting one word at a time works for a single word. To count every word in a collection of reviews, the next section, Dictionaries, stores one counter per word.
### Exercise: Count Words in Reviews
# 1. Loop over reviews with enumerate(reviews, start=1) and print each word count.
# 2. Count the reviews that mention "shipping" (ignore upper and lower case).
# Print exactly:
# Review 1: 5 words
# Review 2: 2 words
# Review 3: 5 words
# Reviews mentioning shipping: 2
reviews = [
"Fast shipping and great price",
"Great quality",
"Slow SHIPPING but great support",
]
### Your code starts here.
### Your code ends here.
Review 1: 5 words
Review 2: 2 words
Review 3: 5 words
Reviews mentioning shipping: 2
2.4.7. Summary#
Strings are sequences, so indexing and slicing work as they do for lists.
String methods such as
strip(),lower(),title(), andreplace()return new, cleaned strings;in,startswith(),endswith(), andfind()search text.split()breaks a string into a list at a delimiter, andjoin()combines a list of strings into one string.Format specifiers such as
:,.2f,:.1%,<10, and>12make numbers readable in reports.Strings are immutable: save the result of a method if you want to keep it.
The pandas
.strmethods in Chapter 4 apply these same operations to whole columns of text.