All 57 lessons

Lesson 5 of 57 · Getting Started

Strings in Python: Text Handling for Analysts

By Skillancy Editorial Team

Run the code in the free Python Compiler or on your own computer. Lessons that read files use the FreshBasket practice data: download the dataset (ZIP).

Numbers are only half of analytics. Customer names, city spellings, product descriptions, email addresses, order notes and survey comments are all text, and real data is full of text that is messy: extra spaces, inconsistent capitals, strange separators. A large part of an analyst's time is spent turning that text into something usable.

In Python, text is handled by the str (string) type. Strings come with dozens of built-in tools, called methods, for searching, cleaning, splitting and formatting. This lesson teaches the ones you will use every week, with FreshBasket examples. The same ideas return in pandas, where the .str accessor applies them to a whole column at once.

Creating strings

A string is a sequence of characters wrapped in quotes. Single quotes and double quotes are equivalent, so choose whichever avoids escaping:

city = "Bengaluru"
shop = 'FreshBasket'
quote = "It's fresh"          # a double-quoted string can contain an apostrophe
note = 'He said "hurry"'      # and vice versa

Triple quotes (""" or ''') create a string that spans several lines, which is handy for messages and SQL queries. A backslash starts an escape sequence: \n is a new line, \t is a tab, and \\ is a literal backslash.

Indexing and slicing

Every character has a position called an index, starting at 0, not 1. Negative indexes count from the end: -1 is the last character.

 F  r  e  s  h  B  a  s  k  e  t
 0  1  2  3  4  5  6  7  8  9  10
-11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1

A slice text[start:stop] takes characters from start up to, but not including, stop. Leave out start to begin at the start, leave out stop to go to the end, and add a third number for a step: text[::2] takes every second character, and text[::-1] reverses the string.

Worked examples

Example 1: Length, indexing and slicing

Question: For the order id O02299, extract the prefix letter and the numeric part, and check the length.

order_id = "O02299"

print(len(order_id))
print(order_id[0])
print(order_id[1:])
print(order_id[-2:])
print(order_id[::-1])

Output

6
O
02299
99
99220O

How it works: len() counts characters. order_id[0] is the first character. order_id[1:] starts at index 1 and runs to the end, which drops the letter. order_id[-2:] starts two from the end. order_id[::-1] reads the string backwards by stepping by -1.

Reading the result: The id has 6 characters, begins with O, and its number part is 02299. Slicing never raises an error for out-of-range ends; it simply returns what exists. Indexing a single position that does not exist, such as order_id[10], does raise an IndexError.

Example 2: Changing case

Question: Names arrive as "aarav SHARMA". Make them consistent.

raw_name = "aarav SHARMA"

print(raw_name.upper())
print(raw_name.lower())
print(raw_name.title())
print(raw_name.capitalize())
print(raw_name.swapcase())

Output

AARAV SHARMA
aarav sharma
Aarav Sharma
Aarav sharma
AARAV sharma

How it works: Each method returns a new string and leaves the original untouched. title() capitalises the first letter of every word, which is what you want for names and cities, whereas capitalize() only changes the very first letter of the whole string.

Reading the result: title() gives Aarav Sharma, the clean form. Case normalisation is also the first step when comparing text, because "Delhi" and "delhi" are different strings to Python.

Example 3: Removing extra spaces

Question: A city field arrives as " Mumbai \n". How do I clean it?

messy_city = "  Mumbai \n"

print(repr(messy_city))
print(repr(messy_city.strip()))
print(repr(messy_city.lstrip()))
print(repr(messy_city.rstrip()))
print("--Pune--".strip("-"))

Output

'  Mumbai \n'
'Mumbai'
'Mumbai \n'
'  Mumbai'
Pune

How it works: strip() removes whitespace (spaces, tabs and new lines) from both ends. lstrip() and rstrip() clean only the left or right. Giving strip a string of characters removes those characters instead. We wrapped each result in repr() so that the hidden spaces and the \n are visible.

Reading the result: The original has leading spaces and a trailing newline; after strip() we have just 'Mumbai'. Stripping is usually the first cleaning step for any text column, because invisible whitespace is a classic reason why two "identical" values do not match.

Example 4: Replacing and standardising

Question: Standardise city variants "Bangalore" and "Bengalore" to "Bengaluru", and turn phone numbers into plain digits.

city = "Bangalore"
fixed_city = city.replace("Bangalore", "Bengaluru")
print(fixed_city)

phone = "+91 98765-43210"
digits = phone.replace("+91", "").replace(" ", "").replace("-", "")
print(digits, len(digits))

Output

Bengaluru
9876543210 10

How it works: replace(old, new) swaps every occurrence of old with new. Methods can be chained: replace(...).replace(...) applies each replacement in order to the result of the previous one.

Reading the result: The city is now Bengaluru, and the phone number is the 10-digit string 9876543210. For many replacements a loop or a dictionary is neater, and for pattern-based cleaning you will use regular expressions later in the tutorial.

Example 5: Splitting and joining

Question: An address line is "Koramangala, Bengaluru, Karnataka, 560034". Split it into parts, then rebuild it with a different separator.

address = "Koramangala, Bengaluru, Karnataka, 560034"

parts = address.split(", ")
print(parts)
print(len(parts), parts[-1])

print(" | ".join(parts))
print("a-b-c".split("-", 1))

Output

['Koramangala', 'Bengaluru', 'Karnataka', '560034']
4 560034
Koramangala | Bengaluru | Karnataka | 560034
['a', 'b-c']

How it works: split(separator) cuts a string into a list of pieces wherever the separator occurs, and separator.join(list) does the reverse, gluing pieces together with the separator between them. An optional second argument limits the number of splits.

Reading the result: The address broke into 4 parts, and the last is the pincode 560034. Notice that join is called on the separator, not on the list, which surprises most beginners. Called with no argument, split() splits on any whitespace and ignores repeated spaces, which makes it ideal for cleaning.

Example 6: Searching inside text

Question: From the email "priya.sharma42@example.com", find the position of @, check the domain, and count the dots.

email = "priya.sharma42@example.com"

print(email.find("@"))
print(email.find("#"))
print(email.startswith("priya"), email.endswith(".com"))
print(email.count("."))
print("sharma" in email, "gmail" in email)

username, domain = email.split("@")
print(username, "|", domain)

Output

14
-1
True True
2
True False
priya.sharma42 | example.com

How it works: find() returns the index of the first match, or -1 if there is none. startswith and endswith test the beginning and end. count counts non-overlapping occurrences, and the in operator checks whether one string appears anywhere inside another. Splitting on @ and unpacking the two parts is a quick way to separate the username from the domain.

Reading the result: The @ sits at index 14, the address ends with .com, and the domain is example.com. A missing substring gives -1 from find, so always test the result before using it as an index.

Example 7: Strings are immutable

Question: Can I change one character of a string in place?

code = "FB-1042"
code[0] = "X"

Output

Traceback (most recent call last):
  File "example.py", line 2, in <module>
    code[0] = "X"
    ~~~~^^^
TypeError: 'str' object does not support item assignment

How it works: Strings are immutable: once created they cannot be changed. Every method that seems to modify a string actually builds a new one. To change a string you assign the new version back to a name.

Reading the result: Python raises TypeError: 'str' object does not support item assignment. The fix is to build a new string and keep it:

code = "FB-1042"
code = "X" + code[1:]
print(code)

Output

XB-1042

Here we glued X onto everything after the first character, and re-pointed code at the result.

Example 8: Formatting with f-strings

Question: Print a neat order line: id, customer, amount with commas, and discount as a percentage, aligned in columns.

order = "O02299"
customer = "Aarav Sharma"
amount = 12499.5
discount = 0.125

print(f"{order} | {customer:<14} | Rs. {amount:>10,.2f} | {discount:.1%}")
print(f"{'ID':<8}{'Name':<16}{'Amount':>12}")
print(f"{order:<8}{customer:<16}{amount:>12,.1f}")

Output

O02299 | Aarav Sharma   | Rs.  12,499.50 | 12.5%
ID      Name                  Amount
O02299  Aarav Sharma        12,499.5

How it works: Inside an f-string, {value:format} applies a format specification. <14 left-aligns in 14 characters, >10 right-aligns in 10, , adds thousand separators, .2f shows two decimals, and .1% multiplies by 100 and adds a percent sign. You can also put short expressions inside the braces.

Reading the result: The columns line up, the amount reads 12,499.50, and the discount shows as 12.5%. Learn this mini-language once and every report you print will look professional.

Example 9: Multi-line strings and a mini message template

Question: Produce a customer notification that spans several lines.

name = "Aarav"
eta = 18

message = f"""Hi {name},
Your FreshBasket order is on its way.
Estimated arrival: {eta} minutes.
Thank you!"""

print(message)
print(message.count("\n") + 1, "lines")

Output

Hi Aarav,
Your FreshBasket order is on its way.
Estimated arrival: 18 minutes.
Thank you!
4 lines

How it works: Triple quotes keep line breaks exactly as typed, and the f prefix still substitutes variables. count("\n") counts the line breaks, so adding 1 gives the number of lines.

Reading the result: The message prints on four lines, matching the count. Templates like this are the starting point for automated emails and WhatsApp notifications.

Common mistakes

1. Thinking methods change the original. name.upper() does nothing useful on its own line: you must assign it, as in name = name.upper().

2. Off-by-one slicing. "Bengaluru"[0:3] is "Ben", not "Beng". The stop index is excluded.

3. Forgetting that find can return -1.

text = "Mumbai"
position = text.find("x")
print(position, text[position])

Output

-1 i

The last character was printed, not an error, because -1 is a valid index. This silent bug is why you should check if position != -1 first.

4. Comparing without normalising. "UPI" == "upi" is False. Lowercase and strip both sides first.

5. Concatenating strings with numbers. "Orders: " + 5 fails. Use an f-string, f"Orders: {5}", or str(5).

Practice questions

Practice 1 (Easy): Clean a name

Convert " rAHUL verma " to the properly cased "Rahul Verma", with single spaces between words and no spaces at the ends.

Show solution
raw = "  rAHUL   verma "
clean = " ".join(raw.split()).title()
print(repr(clean))

Output

'Rahul Verma'

raw.split() with no argument splits on any run of whitespace and drops empty pieces, " ".join(...) glues the words back with one space, and .title() fixes the capitals.

Practice 2 (Medium): Build a username

From the email "Vikram.Nair17@Example.COM", create the lowercase username and domain, and print them like vikram.nair17 at example.com.

Show solution
email = "Vikram.Nair17@Example.COM"
username, domain = email.lower().split("@")
print(f"{username} at {domain}")

Output

vikram.nair17 at example.com

Lowercasing first means both parts are normalised, and unpacking the result of split("@") assigns them to two names at once.

Practice 3 (Hard): Parse a feedback line

A feedback record looks like "2025-12-12 | O04344 | rating:3 | Late delivery by almost 50 minutes.". Extract the date, the order id, the rating as an integer, and the comment, without any regular expressions.

Show solution
line = "2025-12-12 | O04344 | rating:3 | Late delivery by almost 50 minutes."

date, order_id, rating_part, comment = [p.strip() for p in line.split("|")]
rating = int(rating_part.split(":")[1])

print(date)
print(order_id)
print(rating, type(rating))
print(comment)

Output

2025-12-12
O04344
3 <class 'int'>
Late delivery by almost 50 minutes.

We split on the pipe, then used a list comprehension (covered later) to strip each piece; you can read it as "for each part, strip it". The rating part rating:3 splits on the colon, and the second piece "3" is converted to an integer with int().

Key takeaways

  • A string is an immutable sequence of characters; indexes start at 0, negative indexes count from the end, and slices exclude the stop index.
  • Methods such as strip, lower, title, replace, split and join return new strings, so assign the result.
  • split turns text into a list, join turns a list back into text, and in, find, startswith and endswith search.
  • f-strings with format specifications (:,.2f, :<12, :.1%) produce professional, aligned output.
  • Always clean before comparing: strip whitespace and normalise case.
  • These same methods reappear in pandas as .str.strip(), .str.lower() and friends, so everything you learn here scales to millions of rows.

Frequently asked questions

What is the difference between a function and a method?

A function stands alone, like len(text). A method belongs to an object and is called with a dot, like text.upper(). Both do jobs, but methods act on the object they are attached to.

Single quotes or double quotes?

No difference in meaning. Pick one style for your project and switch only when the text contains the other kind of quote. Many teams follow the Black code formatter, which prefers double quotes.

How do I include a quote or backslash inside a string?

Use the other kind of quote around it, or escape with a backslash: "She said \"hi\"" and "C:\\data". A raw string, written r"C:\data", treats backslashes literally and is useful for file paths and regular expressions.

Why does "10" + "5" give "105"?

Because + joins strings. To add the numbers, convert them first: int("10") + int("5") is 15.

How can I tell whether a string is all digits or all letters?

Use the test methods isdigit(), isalpha(), isalnum() and isspace(). For example "560034".isdigit() is True. They return False for an empty string.

Are strings case-sensitive when I use in or ==?

Yes. "fresh" in "FreshBasket" is False. Convert both sides with .lower() for case-insensitive checks.

Newsletter

Keep learning Python

One email a week with Python and analytics guides, new practice questions and cohort dates. No spam, unsubscribe anytime.

Ready to practise? Try the Python interview questions