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
len(text)  # 6

# Strings are immutable (cannot be changed after creation)
# text[0] = "J"  # This would raise TypeError!

# But you can create NEW strings based on existing ones
new_text = "J" + text[1:]  # "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)
first = text[0]      # "P"
second = text[1]     # "y"
last = text[5]       # "n"

# Negative indexing (from right)
last = text[-1]      # "n" (last character)
second_last = text[-2]  # "o"
first = text[-6]     # "P" (same as text[0])

# IndexError if out of range
# text[10]  # IndexError: string index out of range
# text[-7]  # IndexError: string index out of range

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)
text[0:6]       # "Python"
text[7:18]      # "Programming"

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

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

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]
text[::2]       # "Pto rgamn" (every 2nd character)
text[::3]       # "Ph oai" (every 3rd character)
text[1::2]      # "yhnPormig" (start at index 1, every 2nd)

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

# Combine start, end, and step
text[7::2]      # "Pormig" (start at 7, every 2nd char)

Common Slicing Patterns

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

text = "Hello, World!"

# First n characters
text[:5]        # "Hello"
text[:3]        # "Hel"

# Last n characters
text[-6:]       # "World!"
text[-1:]       # "!"

# Remove first n characters
text[7:]        # "World!" (skip first 7)
text[2:]        # "llo, World!" (skip first 2)

# Remove last n characters
text[:-7]       # "Hello," (remove last 7)
text[:-1]       # "Hello, World" (remove last 1)

# Reverse a string
text[::-1]      # "!dlroW ,olleH"

# Every other character
text[::2]       # "Hlo ol!"

# Middle portion
text[2:10]      # "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"
greeting = first + " " + last  # "Hello World"

# Concatenation with variables
name = "Alice"
message = "Hello, " + name + "!"  # "Hello, Alice!"

# Repetition (repeating strings with *)
stars = "*" * 5      # "*****"
line = "-" * 20      # "--------------------"
separator = "=" * 10 # "=========="

# Practical use cases
border = "+" + "-" * 10 + "+"  # "+----------+"
emphasis = "!" * 3 + " ALERT " + "!" * 3  # "!!! ALERT !!!"

# Combine concatenation and repetition
header = "=" * 5 + " TITLE " + "=" * 5  # "===== TITLE ====="

# Multiple lines
divider = "-" * 40
print(divider)
print("Important Information")
print(divider)

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
height = 5.6

# Basic f-string
message = f"My name is {name}"
# "My name is Alice"

# Multiple variables
info = f"I'm {name}, {age} years old"
# "I'm Alice, 30 years old"

# Expressions inside f-strings
next_year = f"Next year I'll be {age + 1}"
# "Next year I'll be 31"

calculation = f"2 + 2 = {2 + 2}"
# "2 + 2 = 4"

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

# Formatting numbers
price = 19.99
formatted = f"Price: ${price:.2f}"
# "Price: $19.99"

percentage = 0.156
score = f"Score: {percentage:.1%}"
# "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
f"{pi:.2f}"       # "3.14" (2 decimal places)
f"{pi:.4f}"       # "3.1416" (4 decimal places)
f"{pi:.0f}"       # "3" (no decimal places, rounds)

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

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

# Thousands separator
big_num = 1000000
f"{big_num:,}"    # "1,000,000"

big_num = 1234567.89
f"{big_num:,.2f}" # "1,234,567.89"

# Percentage formatting
ratio = 0.75
f"{ratio:.1%}"    # "75.0%"
f"{ratio:.0%}"    # "75%"

# Binary, octal, hexadecimal
num = 42
f"{num:b}"        # "101010" (binary)
f"{num:o}"        # "52" (octal)
f"{num:x}"        # "2a" (hexadecimal)
f"{num:X}"        # "2A" (uppercase hex)

.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
"My name is {} and I'm {} years old".format(name, age)
# "My name is Alice and I'm 30 years old"

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

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

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

# Reusable format strings
template = "Hello, {name}! Your score is {score:.1f}."
template.format(name="Bob", score=95.6)
# "Hello, Bob! Your score is 95.6."
template.format(name="Alice", score=87.3)
# "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)
"My name is %s and I'm %d years old" % (name, age)
# "My name is Alice and I'm 30 years old"

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

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

# With padding
"%-10s | %10s" % ("Name", "Score")
# "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!"

text.upper()        # "HELLO, WORLD!"
text.lower()        # "hello, world!"
text.capitalize()   # "Hello, world!" (first letter only)
text.title()        # "Hello, World!" (each word capitalized)
text.swapcase()     # "HELLO, wORLD!" (swap case)

# Practical use cases
email = "Alice.Smith@EXAMPLE.com"
normalized = email.lower()  # "alice.smith@example.com"

title = "the lord of the rings"
formatted_title = title.title()  # "The Lord Of The Rings"

# Check case
"HELLO".isupper()   # True
"hello".islower()   # True
"Hello World".istitle()  # True

Whitespace Handling

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

text = "  Hello, World!  "

# Remove whitespace from both ends
text.strip()        # "Hello, World!"

# Remove from left only
text.lstrip()       # "Hello, World!  "

# Remove from right only
text.rstrip()       # "  Hello, World!"

# Remove specific characters
text = "...Hello..."
text.strip(".")     # "Hello"

phone = "---555-1234---"
phone.strip("-")    # "555-1234"

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

# Check for whitespace
"   ".isspace()     # True (all whitespace)
"abc".isspace()     # False
"".isspace()        # False (empty string)

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)
text.find("World")      # 7
text.find("Python")     # 21
text.find("Java")       # -1 (not found)
text.find("Hello", 1)   # 14 (search starting at index 1)

# Index (like find, but raises ValueError if not found)
text.index("World")     # 7
# text.index("Java")    # ValueError!

# Count occurrences
text.count("Hello")     # 2
text.count("o")         # 4
text.count("z")         # 0

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

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

# Case-insensitive search
text.lower().find("hello")  # 0 (found)
"hello" in text.lower()     # True

String Modification

Transform strings by replacing, splitting, or joining content.

text = "Hello, World!"

# Replace substring
text.replace("World", "Python")  # "Hello, Python!"
text.replace("l", "L")           # "HeLLo, WorLd!"
text.replace("l", "L", 2)        # "HeLLo, World!" (max 2 replacements)

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

# Split into list
text.split(",")         # ["Hello", " World!"]
"a-b-c".split("-")      # ["a", "b", "c"]
"one two three".split() # ["one", "two", "three"] (split on any whitespace)

csv = "Alice,30,Engineer"
parts = csv.split(",")  # ["Alice", "30", "Engineer"]
name, age, job = csv.split(",")  # Unpack directly

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

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

# Practical: build CSV
data = ["Alice", "30", "Engineer"]
",".join(data)          # "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
"123".isdigit()         # True (all digits)
"abc".isalpha()         # True (all letters)
"abc123".isalnum()      # True (letters or digits)
"   ".isspace()         # True (all whitespace)
"".isspace()            # False (empty string)

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

# Check case
"HELLO".isupper()       # True
"hello".islower()       # True
"Hello".istitle()       # True
"HeLLo".istitle()       # False

# 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
"abc".isascii()         # True (all ASCII characters)
"123.45".isdecimal()    # False (has decimal point)
"Hello".isprintable()   # True (all printable characters)

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}")  # "alice.smith"
    print(f"Domain: {domain}")      # "example.com"

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

result = clean_email("  BOB@EXAMPLE.COM  ")
# "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)    # "Alice Marie Smith"
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  "))  # "John Doe"
print(format_name("mary-jane watson"))  # "Mary-jane Watson"

# 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"))  # "A.M.S."
print(get_initials("john doe"))           # "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)

# Output:
# =============Sales Report==============
# ========================================
# Salesperson: Alice
# Items Sold:           47
# Revenue:       $2,350.75
# Target:        $2,000.00
# Performance:      117.5%
# ========================================
# Status:      EXCEEDS TARGET

# 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))

# Output:
# +---------+-------+-------+
# | Name    | Score | Grade |
# +---------+-------+-------+
# | Alice   | 95    | A     |
# | Bob     | 87    | B     |
# | Charlie | 92    | A     |
# +---------+-------+-------+

Example 4: 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}")

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