String Manipulation & Formatting

Master string operations, slicing, and modern formatting techniques.

Introduction

Strings are everywhere in programming, from user input to file processing to web scraping. Mastering string manipulation is essential for building real-world applications. In this lesson, you'll learn how to slice strings like a pro, format output beautifully with f-strings, and use powerful built-in methods to transform and validate text data. These skills form the foundation for working with files, APIs, and data processing, skills you'll use in every Python project.

Understanding Strings

Strings are sequences of characters. Each character has a position (called an index), making strings similar to lists but for text data. Understanding this concept is key to mastering string manipulation.

text = "Python"
# A string is a sequence: P-y-t-h-o-n
#                         0 1 2 3 4 5  (positive indices)
#                        -6-5-4-3-2-1  (negative indices)

# Get the length of a string
print(len(text))

# Strings are immutable (cannot be changed after creation)
try:
    text[0] = "J"
except TypeError as e:
    print(f"TypeError: {e}")

# But you can create NEW strings based on existing ones
new_text = "J" + text[1:]
print(new_text)
Expected Output:
6
TypeError: 'str' object does not support item assignment
Jython

String Indexing & Slicing

Access individual characters or extract substrings using indices and slicing. This is one of the most powerful features for string manipulation.

Basic Indexing

Access individual characters using square brackets and an index. Python uses 0-based indexing (the first character is at index 0).

text = "Python"

# Positive indexing (0-based, from left)
print(text[0])
print(text[1])
print(text[5])

# Negative indexing (from right)
print(text[-1])   # last character
print(text[-2])
print(text[-6])   # same as text[0]

# IndexError if out of range
try:
    text[10]
except IndexError as e:
    print(f"IndexError: {e}")
Expected Output:
P
y
n
n
o
P
IndexError: string index out of range

Positive and negative indices for text = "Python"

0
P
-6
1
y
-5
2
t
-4
3
h
-3
4
o
-2
5
n
-1

Green = positive index (counting from the left, starting at 0). Red = negative index (counting from the right, starting at -1). So text[0] and text[-6] both give "P".

Slicing Syntax

Extract portions of strings using [start:end:step] syntax. The end index is exclusive (not included in the result).

text = "Python Programming"
#       0123456789...

# Basic slicing [start:end] (end is exclusive)
print(text[0:6])
print(text[7:18])

# Omit start or end
print(text[:6])     # start defaults to 0
print(text[7:])     # end defaults to length
print(text[:])      # full copy

# Negative indices in slicing
print(text[-11:])   # last 11 characters
print(text[:-12])   # everything except last 12
Expected Output:
Python
Programming
Python
Programming
Python Programming
Programming
Python

Step Parameter

The optional step parameter controls which characters to include. A negative step reverses the string!

text = "Python Programming"

# Step parameter [start:end:step]
print(text[::2])    # every 2nd character
print(text[::3])    # every 3rd character
print(text[1::2])   # start at index 1, every 2nd

# Reverse string with negative step
print(text[::-1])   # reverse entire string
print(text[::-2])   # reverse, every 2nd char

# Combine start, end, and step
print(text[7::2])   # start at 7, every 2nd char
Expected Output:
Pto rgamn
Ph oai
yhnPormig
gnimmargorP nohtyP
gimroPnhy
Pormig

Common Slicing Patterns

Here are practical slicing patterns you'll use frequently in real projects.

text = "Hello, World!"

# First n characters
print(text[:5])
print(text[:3])

# Last n characters
print(text[-6:])
print(text[-1:])

# Remove first n characters
print(text[7:])     # skip first 7
print(text[2:])     # skip first 2

# Remove last n characters
print(text[:-7])    # remove last 7
print(text[:-1])    # remove last 1

# Reverse a string
print(text[::-1])

# Every other character
print(text[::2])

# Middle portion
print(text[2:10])
Expected Output:
Hello
Hel
World!
!
World!
llo, World!
Hello,
Hello, World
!dlroW ,olleH
Hlo ol!
llo, Wor

Remember: Slicing never modifies the original string. It always creates a new string. Strings are immutable in Python, once created, they cannot be changed.

String Concatenation & Repetition

Combine strings together or repeat them multiple times using operators.

# Concatenation (joining strings with +)
first = "Hello"
last = "World"
print(first + " " + last)

# Concatenation with variables
name = "Alice"
print("Hello, " + name + "!")

# Repetition (repeating strings with *)
print("*" * 5)
print("-" * 20)
print("=" * 10)

# Practical use cases
print("+" + "-" * 10 + "+")
print("!" * 3 + " ALERT " + "!" * 3)

# Combine concatenation and repetition
print("=" * 5 + " TITLE " + "=" * 5)

# Multiple lines
divider = "-" * 40
print(divider)
print("Important Information")
print(divider)
Expected Output:
Hello World
Hello, Alice!
*****
--------------------
==========
+----------+
!!! ALERT !!!
===== TITLE =====
----------------------------------------
Important Information
----------------------------------------

Performance Note: While + works for concatenation, it's not the most efficient for combining many strings. For multiple strings, use f-strings or the .join() method (covered later).

String Formatting

Python offers multiple ways to format strings. F-strings (formatted string literals) are the modern, recommended approach introduced in Python 3.6+.

F-Strings (Recommended)

F-strings are the most readable and fastest way to format strings. Prefix your string with f and use curly braces{} to embed expressions.

name = "Alice"
age = 30

# Basic f-string
print(f"My name is {name}")

# Multiple variables
print(f"I'm {name}, {age} years old")

# Expressions inside f-strings
print(f"Next year I'll be {age + 1}")
print(f"2 + 2 = {2 + 2}")

# Method calls inside f-strings
print(f"Hello, {name.upper()}!")

# Formatting numbers
price = 19.99
print(f"Price: ${price:.2f}")

percentage = 0.156
print(f"Score: {percentage:.1%}")
Expected Output:
My name is Alice
I'm Alice, 30 years old
Next year I'll be 31
2 + 2 = 4
Hello, ALICE!
Price: $19.99
Score: 15.6%

Format Specifiers

Control how values are displayed using format specifiers after a colon. This gives you precise control over alignment, padding, and number formatting.

# Decimal places for floats
pi = 3.14159
print(f"{pi:.2f}")       # 2 decimal places
print(f"{pi:.4f}")       # 4 decimal places
print(f"{pi:.0f}")       # no decimal places, rounds

# Padding and alignment (the pipes make the padding visible)
name = "Bob"
print(f"|{name:>10}|")   # right align, width 10
print(f"|{name:<10}|")   # left align, width 10
print(f"|{name:^10}|")   # center, width 10
print(f"|{name:*^10}|")  # center with * padding

# Numbers with padding
num = 42
print(f"|{num:05d}|")    # pad with zeros, width 5
print(f"|{num:5d}|")     # pad with spaces, width 5

# Thousands separator
print(f"{1000000:,}")
print(f"{1234567.89:,.2f}")

# Percentage formatting
ratio = 0.75
print(f"{ratio:.1%}")
print(f"{ratio:.0%}")

# Binary, octal, hexadecimal
print(f"{num:b}")        # binary
print(f"{num:o}")        # octal
print(f"{num:x}")        # hexadecimal
print(f"{num:X}")        # uppercase hex
Expected Output:
3.14
3.1416
3
|       Bob|
|Bob       |
|   Bob    |
|***Bob****|
|00042|
|   42|
1,000,000
1,234,567.89
75.0%
75%
101010
52
2a
2A

.format() Method

The older .format() method is still widely used, especially in older codebases or when you need reusable format strings.

name = "Alice"
age = 30

# Positional arguments
print("My name is {} and I'm {} years old".format(name, age))

# Named arguments
print("My name is {n} and I'm {a} years old".format(n=name, a=age))

# Index-based (reuse arguments)
print("{0} is {1} years old. {0} is a developer.".format(name, age))

# With format specifiers
print("Price: ${:.2f}".format(19.99))
print("{:<10} | {:>10}".format("Name", "Score"))

# Reusable format strings
template = "Hello, {name}! Your score is {score:.1f}."
print(template.format(name="Bob", score=95.6))
print(template.format(name="Alice", score=87.3))
Expected Output:
My name is Alice and I'm 30 years old
My name is Alice and I'm 30 years old
Alice is 30 years old. Alice is a developer.
Price: $19.99
Name       |      Score
Hello, Bob! Your score is 95.6.
Hello, Alice! Your score is 87.3.

% Formatting (Legacy)

Old-style formatting using the % operator. Still found in older code, but use f-strings in new projects!

name = "Alice"
age = 30

# Old style (C-like formatting)
print("My name is %s and I'm %d years old" % (name, age))

# Format specifiers:
# %s = string
# %d = integer
# %f = float
# %.<precision>f = float with decimal places

print("Price: $%.2f" % 19.99)
print("Score: %d%%" % 85)   # need %% for a literal %

# With padding
print("%-10s | %10s" % ("Name", "Score"))
Expected Output:
My name is Alice and I'm 30 years old
Price: $19.99
Score: 85%
Name       |      Score

Comparison: Which One to Use?

✓ F-Strings
  • Most readable
  • Fastest performance
  • Concise syntax
  • Python 3.6+
  • Recommended!
.format()
  • Good for templates
  • Python 2.7+
  • Reusable format strings
  • Still widely used
  • Slightly more verbose
% Formatting
  • Legacy method
  • Harder to read
  • Limited features
  • C-style syntax
  • Avoid in new code

Essential String Methods

Python provides many built-in methods to manipulate and analyze strings. Here are the essential ones organized by category.

Case Transformation

Methods to change the case of characters in strings.

text = "hello, World!"

print(text.upper())
print(text.lower())
print(text.capitalize())   # first letter only
print(text.title())        # each word capitalized
print(text.swapcase())     # swap case

# Practical use cases
email = "Alice.Smith@EXAMPLE.com"
print(email.lower())

title = "the lord of the rings"
print(title.title())

# Check case
print("HELLO".isupper())
print("hello".islower())
print("Hello World".istitle())
Expected Output:
HELLO, WORLD!
hello, world!
Hello, world!
Hello, World!
HELLO, wORLD!
alice.smith@example.com
The Lord Of The Rings
True
True
True

Whitespace Handling

Remove or manage whitespace from strings, essential for cleaning user input.

text = "  Hello, World!  "

# repr() shows the quotes, so you can see exactly which spaces survive
print(repr(text.strip()))    # both ends
print(repr(text.lstrip()))   # left only
print(repr(text.rstrip()))   # right only

# Remove specific characters
print(repr("...Hello...".strip(".")))
print(repr("---555-1234---".strip("-")))

# Practical use case: cleaning user input
user_input = "  alice@example.com  "
print(repr(user_input.strip()))

# Check for whitespace
print("   ".isspace())   # all whitespace
print("abc".isspace())
print("".isspace())      # empty string
Expected Output:
'Hello, World!'
'Hello, World!  '
'  Hello, World!'
'Hello'
'555-1234'
'alice@example.com'
True
False
False

Searching & Finding

Search for substrings and get information about their location.

text = "Hello, World! Hello, Python!"

# Find substring (returns index or -1 if not found)
print(text.find("World"))
print(text.find("Python"))
print(text.find("Java"))       # not found
print(text.find("Hello", 1))   # search starting at index 1

# Index (like find, but raises ValueError if not found)
print(text.index("World"))
try:
    text.index("Java")
except ValueError as e:
    print(f"ValueError: {e}")

# Count occurrences
print(text.count("Hello"))
print(text.count("o"))
print(text.count("z"))

# Check start/end
print(text.startswith("Hello"))
print(text.startswith("Hi"))
print(text.endswith("Python!"))
print(text.endswith("World"))

# Check if substring exists (using 'in')
print("World" in text)
print("Java" in text)

# Case-insensitive search
print(text.lower().find("hello"))
print("hello" in text.lower())
Expected Output:
7
21
-1
14
7
ValueError: substring not found
2
4
0
True
False
True
False
True
False
0
True

String Modification

Transform strings by replacing, splitting, or joining content.

text = "Hello, World!"

# Replace substring
print(text.replace("World", "Python"))
print(text.replace("l", "L"))
print(text.replace("l", "L", 2))     # max 2 replacements

# Remove substring (replace with empty string)
print(text.replace("Hello, ", ""))

# Split into list
print(text.split(","))
print("a-b-c".split("-"))
print("one two three".split())       # split on any whitespace

csv = "Alice,30,Engineer"
name, age, job = csv.split(",")      # unpack directly
print(name, age, job)

# Join list into string
words = ["Hello", "World"]
print(" ".join(words))
print("-".join(words))
print("".join(words))

# Join with newlines
lines = ["Line 1", "Line 2", "Line 3"]
print("\n".join(lines))

# Practical: build CSV
data = ["Alice", "30", "Engineer"]
print(",".join(data))
Expected Output:
Hello, Python!
HeLLo, WorLd!
HeLLo, World!
World!
['Hello', ' World!']
['a', 'b', 'c']
['one', 'two', 'three']
Alice 30 Engineer
Hello World
Hello-World
HelloWorld
Line 1
Line 2
Line 3
Alice,30,Engineer

Important: String methods don't modify the original string (strings are immutable). They return a new string with the changes. Always assign the result to a variable if you want to keep it.

String Validation

Check the content and characteristics of strings.

# Check string content type
print("123".isdigit())          # all digits
print("abc".isalpha())          # all letters
print("abc123".isalnum())       # letters or digits
print("   ".isspace())          # all whitespace
print("".isspace())             # empty string

print("Hello123".isdigit())     # has letters
print("Hello World".isalpha())  # has space

# Check case
print("Hello".istitle())
print("HeLLo".istitle())

# Practical validation examples
age_input = "25"
if age_input.isdigit():
    age = int(age_input)
    print(f"Age: {age}")
else:
    print("Invalid age - must be digits only")

username = "alice123"
if username.isalnum():
    print("Valid username")
else:
    print("Username must be alphanumeric")

# Other useful checks
print("abc".isascii())          # all ASCII characters
print("123.45".isdecimal())     # the dot is not a decimal digit
print("Hello".isprintable())
Expected Output:
True
True
True
True
False
False
False
True
False
Age: 25
Valid username
True
False
True

Practical String Examples

Now let's combine what we've learned to solve real-world problems. These examples show how string methods work together in practical scenarios.

Example 1: Email Validation & Formatting

Clean and validate email input by combining multiple string methods.

# Clean and validate email input
email = "  Alice.Smith@EXAMPLE.COM  "

# Step 1: Remove whitespace
email = email.strip()  # "Alice.Smith@EXAMPLE.COM"

# Step 2: Convert to lowercase (emails are case-insensitive)
email = email.lower()  # "alice.smith@example.com"

# Step 3: Validate structure
has_at = "@" in email
only_one_at = email.count("@") == 1

# Check domain has a dot
domain = email.split("@")[1] if has_at else ""
has_dot_in_domain = "." in domain

if has_at and only_one_at and has_dot_in_domain:
    print(f"Valid email: {email}")

    # Extract parts
    username, domain = email.split("@")
    print(f"Username: {username}")
    print(f"Domain: {domain}")

    # Check domain
    if domain.endswith(".edu"):
        print("Educational institution")
    elif domain.endswith(".com"):
        print("Commercial domain")
else:
    print("Invalid email format")

# Chain all cleaning steps
def clean_email(email):
    return email.strip().lower()

print(clean_email("  BOB@EXAMPLE.COM  "))
Expected Output:
Valid email: alice.smith@example.com
Username: alice.smith
Domain: example.com
Commercial domain
bob@example.com

Example 2: Name Formatting

Format names consistently by handling extra whitespace and proper capitalization.

# Format names consistently
full_name = "  alice   marie   SMITH  "

# Step 1: Strip outer whitespace
full_name = full_name.strip()  # "alice   marie   SMITH"

# Step 2: Split by whitespace (handles multiple spaces)
parts = full_name.split()      # ["alice", "marie", "SMITH"]

# Step 3: Capitalize each part
parts = [part.capitalize() for part in parts]
# ["Alice", "Marie", "Smith"]

# Step 4: Join back together
formatted = " ".join(parts)
print(f"Formatted name: {formatted}")

# As a reusable function
def format_name(name):
    """Format a name with proper capitalization and spacing."""
    return " ".join(part.capitalize() for part in name.strip().split())

# Test the function
print(format_name("  john    DOE  "))
print(format_name("mary-jane watson"))  # capitalize() lowercases the rest

# Handle different name formats
def get_initials(name):
    """Get initials from a name."""
    parts = name.strip().split()
    initials = [part[0].upper() for part in parts]
    return ".".join(initials) + "."

print(get_initials("Alice Marie Smith"))
print(get_initials("john doe"))
Expected Output:
Formatted name: Alice Marie Smith
John Doe
Mary-jane Watson
A.M.S.
J.D.

Example 3: Creating a Text Report

Generate formatted reports with alignment and number formatting.

# Generate a formatted sales report
name = "Alice"
items_sold = 47
revenue = 2350.75
target = 2000

# Create report with formatting
header = "Sales Report".center(40, "=")
separator = "=" * 40

report = f"""
{header}
{separator}
Salesperson: {name}
Items Sold:  {items_sold:>10}
Revenue:     ${revenue:>10,.2f}
Target:      ${target:>10,.2f}
Performance: {(revenue/target):>10.1%}
{separator}
Status:      {"EXCEEDS TARGET" if revenue > target else "BELOW TARGET"}
"""

print(report)
Expected Output:
==============Sales Report==============
========================================
Salesperson: Alice
Items Sold:          47
Revenue:     $  2,350.75
Target:      $  2,000.00
Performance:     117.5%
========================================
Status:      EXCEEDS TARGET

Watch the currency column: the padding landsbetween the $ and the number. The$ is a literal character sitting before the field, so{revenue:>10,.2f} right-aligns within the 10 columns that follow it. To pad before the $ instead, build the string first and align the whole thing:{"$" + f"{revenue:,.2f}":>11}.

Example 4: Building a Text Table

The same tools scale up to a table: measure each column, then useljust() to pad every cell to that width.

# Create a table
def create_table(headers, rows):
    """Create a simple text table."""
    # Calculate column widths
    col_widths = [len(h) for h in headers]
    for row in rows:
        for i, cell in enumerate(row):
            col_widths[i] = max(col_widths[i], len(str(cell)))

    # Create separator
    separator = "+" + "+".join("-" * (w + 2) for w in col_widths) + "+"

    # Header row
    header_row = "| " + " | ".join(
        h.ljust(col_widths[i]) for i, h in enumerate(headers)
    ) + " |"

    # Data rows
    data_rows = []
    for row in rows:
        data_row = "| " + " | ".join(
            str(cell).ljust(col_widths[i]) for i, cell in enumerate(row)
        ) + " |"
        data_rows.append(data_row)

    # Combine all parts
    result = [separator, header_row, separator]
    result.extend(data_rows)
    result.append(separator)

    return "\n".join(result)

# Use the table function
headers = ["Name", "Score", "Grade"]
rows = [
    ["Alice", "95", "A"],
    ["Bob", "87", "B"],
    ["Charlie", "92", "A"],
]

print(create_table(headers, rows))
Expected Output:
+---------+-------+-------+
| Name    | Score | Grade |
+---------+-------+-------+
| Alice   | 95    | A     |
| Bob     | 87    | B     |
| Charlie | 92    | A     |
+---------+-------+-------+

Example 5: Password Validation

Validate password strength using string methods.

def validate_password(password):
    """
    Validate password meets security requirements:
    - At least 8 characters
    - Contains uppercase and lowercase
    - Contains at least one digit
    - Contains at least one special character
    """
    errors = []

    # Check length
    if len(password) < 8:
        errors.append("Password must be at least 8 characters")

    # Check for uppercase
    if not any(c.isupper() for c in password):
        errors.append("Password must contain at least one uppercase letter")

    # Check for lowercase
    if not any(c.islower() for c in password):
        errors.append("Password must contain at least one lowercase letter")

    # Check for digit
    if not any(c.isdigit() for c in password):
        errors.append("Password must contain at least one digit")

    # Check for special character
    special_chars = "!@#$%^&*()_+-=[]{}|;:,.<>?"
    if not any(c in special_chars for c in password):
        errors.append("Password must contain at least one special character")

    return errors

# Test the validator
password = "MyPass123!"
errors = validate_password(password)

if errors:
    print("Password validation failed:")
    for error in errors:
        print(f"  - {error}")
else:
    print("Password is strong!")

# Test various passwords
test_passwords = [
    "weak",           # Too short, no uppercase, no digit, no special
    "WeakPassword",   # No digit, no special
    "WeakPass123",    # No special
    "Strong@Pass1",   # Valid!
]

for pwd in test_passwords:
    errors = validate_password(pwd)
    status = "✓ Valid" if not errors else "✗ Invalid"
    print(f"{pwd:<20} {status}")
Expected Output:
Password is strong!
weak                 ✗ Invalid
WeakPassword         ✗ Invalid
WeakPass123          ✗ Invalid
Strong@Pass1         ✓ Valid

Key Takeaways

String Basics

  • Strings are immutable sequences
  • Use [index] to access characters
  • Use [start:end:step] for slicing
  • Negative indices count from the end
  • [::-1] reverses a string

Formatting

  • Use f-strings for modern formatting
  • Format: f"{variable:.2f}"
  • .format() for templates
  • Avoid % formatting in new code
  • Use format specifiers for alignment and padding

Essential Methods

  • .upper(), .lower() for case
  • .strip() removes whitespace
  • .split() and .join() for lists
  • .replace() for modifications
  • .find(), .startswith(), .endswith()

Best Practices

  • Clean user input with .strip()
  • Use .lower() for case-insensitive comparison
  • Validate with .isdigit(), .isalpha()
  • Methods return new strings (immutable)
  • Chain methods for complex transformations
What's Next?

You've mastered string manipulation, a fundamental skill for any Python developer! In the next lessons, we'll explore:

  • List comprehensions & lambda functions - Write cleaner, more Pythonic code with elegant one-liners
  • Standard library modules - Discover powerful built-in tools for dates, files, and more
  • Debugging basics - Learn to find and fix bugs like a professional developer