Python Strings: Slicing, Indexing, f-Strings, String Methods, Encoding, Escape Characters, Multiline Strings, and Why Strings Are Immutable

Python Strings: Slicing, Indexing, f-Strings, String Methods, Encoding, Escape Characters, Multiline Strings, and Why Strings Are Immutable

Strings are everywhere in data engineering — column names, file paths, SQL queries, API responses, log messages, error messages. You will work with strings more than any other data type. Yet most tutorials cover strings in 5 minutes: “use quotes, call .upper(), done.” That is not enough.

This post covers strings from the ground up — how Python stores them in memory (they are immutable sequences of Unicode characters), every way to create and format them, slicing with the three-parameter syntax, all the methods you will actually use in production, and the encoding concepts that matter when reading files from different systems.

Think of a string like a necklace of beads. Each bead is a character. The necklace has a fixed order — you can look at bead #3 (indexing), take a section of beads #2 through #5 (slicing), or create an entirely new necklace by rearranging beads (concatenation). But you cannot change a bead on the existing necklace — if you want a different color at position #3, you make a new necklace (immutability).

Table of Contents

  • Creating Strings
  • Single, Double, and Triple Quotes
  • Raw Strings
  • Strings in Memory: Immutability
  • Why Immutability Matters
  • Indexing
  • Positive and Negative Indexing
  • IndexError and Safe Access
  • Slicing
  • Basic Slicing [start:stop]
  • Slicing with Step [start:stop:step]
  • Common Slicing Patterns
  • Escape Characters
  • String Formatting
  • f-Strings (Recommended)
  • f-String Advanced Features
  • .format() Method
  • % Formatting (Legacy)
  • String Methods — Searching
  • find() and index()
  • count()
  • startswith() and endswith()
  • in Operator
  • String Methods — Modifying (Returns New String)
  • upper(), lower(), title(), capitalize(), swapcase()
  • strip(), lstrip(), rstrip()
  • replace()
  • center(), ljust(), rjust(), zfill()
  • String Methods — Splitting and Joining
  • split() and rsplit()
  • splitlines()
  • join()
  • partition() and rpartition()
  • String Methods — Checking
  • isdigit(), isnumeric(), isdecimal()
  • isalpha(), isalnum(), isspace()
  • isupper(), islower(), istitle()
  • String Concatenation and Repetition
  • Performance: Why + in a Loop Is Bad
  • Strings and Unicode (Encoding)
  • UTF-8 and Why It Matters for Data Engineers
  • encode() and decode()
  • Strings in Data Engineering
  • Building SQL Queries
  • Parsing File Paths
  • Cleaning Data Strings
  • Common Mistakes
  • Interview Questions
  • Wrapping Up

Creating Strings

Single, Double, and Triple Quotes

# Single and double quotes — identical behavior
name = 'Naveen'
name = "Naveen"        # No difference — pick one style and be consistent

# Use the OTHER quote when your string CONTAINS quotes
message = "It's a beautiful day"       # Single quote inside double quotes
html = '<div class="header">Hello</div>'  # Double quotes inside single quotes

# Triple quotes — multiline strings
bio = """Naveen Vuppula is a
Senior Data Engineering Consultant
based in Ontario, Canada."""

# Triple quotes preserve newlines and indentation exactly as written
sql = """
SELECT customer_id, first_name, last_name
FROM silver.customers
WHERE city = 'Toronto'
ORDER BY last_name
"""
print(sql)   # Prints with all newlines and indentation

# Triple quotes are also used for docstrings (function documentation)
def process_data(df):
    """
    Clean and transform the input DataFrame.

    Args:
        df: Raw PySpark DataFrame from Bronze layer

    Returns:
        Cleaned DataFrame ready for Silver layer
    """
    pass

Raw Strings

# Raw strings (prefix with r) treat backslashes as literal characters
# Essential for: file paths (Windows), regex patterns

# WITHOUT raw string — backslash is an escape character
path = "C:
ew_folder   est"
print(path)  # C:
             # ew_folder    est   ← 
 and     are interpreted as newline and tab!

# WITH raw string — backslashes are literal
path = r"C:
ew_folder   est"
print(path)  # C:
ew_folder   est  ← Correct!

# Regex patterns should ALWAYS use raw strings
import re
pattern = r"\d{3}-\d{3}-\d{4}"    # Matches: 416-555-1234
# Without r: "\d{3}-\d{3}-\d{4}" — messy double backslashes

Strings in Memory: Immutability

Why Immutability Matters

# Strings are IMMUTABLE — you cannot change characters in place
name = "Naveen"
# name[0] = "D"   # TypeError: 'str' object does not support item assignment

# Every "modification" creates a NEW string object
name = "naveen"
print(id(name))          # address A
upper_name = name.upper()
print(id(upper_name))    # address B — different object!
print(name)              # "naveen" — original is UNCHANGED

# Even += creates a new string
greeting = "Hello"
print(id(greeting))      # address A
greeting += " World"
print(id(greeting))      # address B — new string, new object

# Why immutable?
# 1. SAFETY: strings used as dict keys must not change
# 2. MEMORY: Python can intern (reuse) identical strings
# 3. THREADING: immutable objects are thread-safe (no locks needed)
# 4. HASHING: immutable objects can be hashed (needed for dict keys and sets)

Real-life analogy: A string is like a printed page. You cannot erase and rewrite one word on the page. To “change” it, you print a new page with the correction. The original page still exists unchanged until nobody needs it anymore (garbage collection).

Indexing

Positive and Negative Indexing

String:     P   y   t   h   o   n
Positive:   0   1   2   3   4   5
Negative:  -6  -5  -4  -3  -2  -1

Positive index: counts from the LEFT, starting at 0
Negative index: counts from the RIGHT, starting at -1
text = "Python"
print(text[0])     # 'P' — first character
print(text[5])     # 'n' — last character (index 5)
print(text[-1])    # 'n' — last character (negative index)
print(text[-2])    # 'o' — second to last
print(text[-6])    # 'P' — first character via negative index

# Practical use: get file extension
filename = "sales_data_2026.csv"
print(filename[-3:])   # "csv" — last 3 characters

IndexError and Safe Access

text = "Python"
# print(text[10])   # IndexError: string index out of range

# Safe access patterns:
# 1. Check length first
if len(text) > 10:
    print(text[10])

# 2. Use try/except
try:
    char = text[10]
except IndexError:
    char = ""   # Default empty string

# 3. Use slicing (never raises IndexError!)
print(text[10:11])  # "" — empty string, no error
# Slicing is forgiving — out-of-range indices return empty, not errors

Slicing

Basic Slicing [start:stop]

text = "Python Programming"

# [start:stop] — includes start, EXCLUDES stop
print(text[0:6])    # "Python"    (chars at index 0,1,2,3,4,5)
print(text[7:18])   # "Programming"
print(text[7:])     # "Programming" (omit stop = go to end)
print(text[:6])     # "Python"     (omit start = start from 0)
print(text[:])      # "Python Programming" (copy entire string)

# Think of indices as BETWEEN characters:
#  | P | y | t | h | o | n |   | P | r | o | ...
#  0   1   2   3   4   5   6   7   8   9   10
# text[0:6] = everything between position 0 and position 6

Slicing with Step [start:stop:step]

text = "Python Programming"

# [start:stop:step] — take every Nth character
print(text[0:6:2])    # "Pto"   (every 2nd char: P, t, o)
print(text[::2])      # "Pto rgamn"  (every 2nd char, full string)
print(text[::3])      # "Ph rmig"    (every 3rd char)

# Negative step — goes RIGHT to LEFT
print(text[::-1])     # "gnimmargorP nohtyP"  (reversed string!)
print(text[::-2])     # "gimroP oty"  (every 2nd char, reversed)
print(text[5::-1])    # "nohtyP"  (from index 5, go backwards to start)

# Common patterns:
text = "Hello World"
print(text[::-1])          # "dlroW olleH"  (reverse)
print(text[:5])            # "Hello"         (first 5 chars)
print(text[-5:])           # "World"         (last 5 chars)
print(text[::2])           # "HloWrd"        (every other char)

Common Slicing Patterns

# Data engineering slicing patterns
filename = "sales_report_2026_Q2.csv"
print(filename[-4:])          # ".csv" — file extension
print(filename[:-4])          # "sales_report_2026_Q2" — name without extension
print(filename.split("_")[-1].split(".")[0])  # "Q2" — extract quarter

# Extract parts of a date string
date_str = "2026-06-15"
year = date_str[:4]       # "2026"
month = date_str[5:7]     # "06"
day = date_str[8:10]      # "15"

# Check palindrome
word = "racecar"
print(word == word[::-1])  # True — it is a palindrome

Escape Characters

Escape Meaning Example Output
Newline"Line1 Line2"Line1
Line2
Tab"Col1 Col2"Col1 Col2
\Literal backslash"C:\folder"C: older
'Single quote'It's here'It’s here
"Double quote"She said "hi""She said “hi”
Carriage return"Hello World"World (overwrites)
Null character"HelloWorld"Hello(null)World

# Common in data engineering:
print("Name Age City")    # Tab-separated header
print("Naveen   30  Toronto")

# File paths on Windows — use raw strings to avoid escape issues
path = r"C:\Users\naveen\data\sales.csv"  # Raw string — safe
path = "C:\\Users\\naveen\\data\\sales.csv"  # Escaped — works but messy

# Or use forward slashes (Python handles this on all OS)
path = "C:/Users/naveen/data/sales.csv"  # Works everywhere!

String Formatting

# f-strings (Python 3.6+) — the modern, recommended way
name = "Naveen"
age = 30
city = "Toronto"

# Basic usage
print(f"My name is {name}")                    # "My name is Naveen"
print(f"{name} is {age} years old")            # "Naveen is 30 years old"

# Expressions inside {}
print(f"Next year: {age + 1}")                 # "Next year: 31"
print(f"Uppercase: {name.upper()}")            # "Uppercase: NAVEEN"
print(f"Length: {len(name)}")                   # "Length: 6"
print(f"Even? {age % 2 == 0}")                # "Even? True"

# Multiline f-strings
message = (
    f"Name: {name}
"
    f"Age: {age}
"
    f"City: {city}"
)

f-String Advanced Features

# Number formatting
price = 1234567.891
print(f"${price:,.2f}")          # "$1,234,567.89" — commas + 2 decimals
print(f"{price:.0f}")            # "1234568" — no decimals (rounded)
print(f"{price:>20,.2f}")        # "    1,234,567.89" — right-aligned in 20 chars

# Percentage
ratio = 0.8542
print(f"{ratio:.1%}")            # "85.4%" — percentage with 1 decimal

# Padding and alignment
name = "Naveen"
print(f"{name:<20}")             # "Naveen              " — left-aligned
print(f"{name:>20}")             # "              Naveen" — right-aligned
print(f"{name:^20}")             # "       Naveen       " — centered
print(f"{name:*^20}")            # "*******Naveen*******" — fill with *

# Integer formatting
num = 42
print(f"{num:05d}")              # "00042" — zero-padded to 5 digits
print(f"{num:b}")                # "101010" — binary
print(f"{num:x}")                # "2a" — hexadecimal
print(f"{num:o}")                # "52" — octal

# Date formatting
from datetime import datetime
now = datetime.now()
print(f"{now:%Y-%m-%d %H:%M}")  # "2026-06-15 14:30"
print(f"{now:%B %d, %Y}")        # "June 15, 2026"

# Debugging (Python 3.8+) — shows variable name AND value
x = 42
print(f"{x = }")                 # "x = 42"
print(f"{name = }, {age = }")    # "name = 'Naveen', age = 30"

.format() Method

# Older style — still used in some codebases
print("Hello, {}!".format(name))                    # "Hello, Naveen!"
print("{0} is {1} years old".format(name, age))     # Positional
print("{n} is {a} years old".format(n=name, a=age)) # Named

# Useful when the template is defined separately (e.g., loaded from config)
template = "Dear {name}, your balance is ${amount:,.2f}"
print(template.format(name="Naveen", amount=12345.6))

% Formatting (Legacy)

# Old C-style formatting — avoid in new code, but know it for reading legacy code
print("Hello, %s! You are %d years old." % (name, age))
print("Price: %.2f" % price)
# Use f-strings instead — more readable, more powerful, faster

String Methods — Searching

find() and index()

text = "Hello World, Hello Python"

# find() — returns index of first occurrence, or -1 if not found
print(text.find("Hello"))       # 0
print(text.find("Hello", 5))    # 13 (start search from index 5)
print(text.find("Java"))        # -1 (not found — no error)

# index() — same as find(), but raises ValueError if not found
print(text.index("Hello"))      # 0
# text.index("Java")            # ValueError: substring not found

# rfind() / rindex() — search from the RIGHT
print(text.rfind("Hello"))      # 13 (last occurrence)

# Rule: Use find() when "not found" is a normal case (returns -1)
#        Use index() when "not found" is an error (raises exception)

count()

text = "banana"
print(text.count("a"))        # 3
print(text.count("an"))       # 2
print(text.count("z"))        # 0

# Count occurrences of a word in a log file
log = "ERROR: disk full. ERROR: timeout. WARNING: slow query. ERROR: OOM"
print(log.count("ERROR"))     # 3

startswith() and endswith()

filename = "sales_report_2026.csv"
print(filename.endswith(".csv"))              # True
print(filename.endswith((".csv", ".tsv")))    # True (check multiple)
print(filename.startswith("sales"))           # True
print(filename.startswith(("sales", "finance")))  # True (check multiple)

# Practical: filter files by extension
files = ["data.csv", "report.pdf", "sales.csv", "image.png"]
csv_files = [f for f in files if f.endswith(".csv")]
print(csv_files)  # ['data.csv', 'sales.csv']

in Operator

# "in" checks if a substring exists (returns True/False)
print("Python" in "I love Python")    # True
print("Java" in "I love Python")      # False
print("error" in "No errors found".lower())  # True (case-insensitive check)

# Most readable way to check for substring presence
if "ERROR" in log_line:
    print("Found an error!")

String Methods — Modifying (Returns New String)

Remember: strings are immutable. Every “modifying” method returns a new string. The original is unchanged.

upper(), lower(), title(), capitalize(), swapcase()

text = "hello WORLD"
print(text.upper())        # "HELLO WORLD"
print(text.lower())        # "hello world"
print(text.title())        # "Hello World"
print(text.capitalize())   # "Hello world" (only first char uppercased)
print(text.swapcase())     # "HELLO world"
print(text)                # "hello WORLD" — original unchanged!

# Data engineering: case-insensitive comparison
user_input = "  Toronto  "
if user_input.strip().lower() == "toronto":
    print("Match!")

strip(), lstrip(), rstrip()

# Remove whitespace (spaces, tabs, newlines) from edges
text = "   hello world   
"
print(text.strip())        # "hello world"
print(text.lstrip())       # "hello world   
"  (left only)
print(text.rstrip())       # "   hello world"     (right only)

# Remove specific characters
text = "###hello###"
print(text.strip("#"))     # "hello"

# Critical for data cleaning — CSV fields often have trailing spaces
name = "  Naveen  "
clean_name = name.strip()  # "Naveen"

replace()

text = "Hello World"
print(text.replace("World", "Python"))   # "Hello Python"
print(text.replace("l", "r"))            # "Herro Worrd" (replaces ALL occurrences)
print(text.replace("l", "r", 1))         # "Herlo World" (replace only first occurrence)

# Chain replacements for data cleaning
dirty = "  John   Doe  "
clean = dirty.strip().replace("   ", " ")  # "John Doe"

# Remove characters
phone = "(416) 555-1234"
digits = phone.replace("(", "").replace(")", "").replace(" ", "").replace("-", "")
print(digits)  # "4165551234"

center(), ljust(), rjust(), zfill()

# Padding for formatted output
print("Name".ljust(20) + "Age".ljust(10) + "City")
print("Naveen".ljust(20) + "30".ljust(10) + "Toronto")
# Name                Age       City
# Naveen              30        Toronto

# Zero-fill (useful for IDs, dates)
print("42".zfill(5))       # "00042"
print("7".zfill(3))        # "007"

# Center
print("REPORT".center(40, "="))  # "=================REPORT================="

String Methods — Splitting and Joining

split() and rsplit()

# split() — break string into a list
text = "apple,banana,cherry"
print(text.split(","))           # ['apple', 'banana', 'cherry']

text = "Hello   World   Python"
print(text.split())              # ['Hello', 'World', 'Python'] (splits on any whitespace)
print(text.split(" "))           # ['Hello', '', '', 'World', '', '', 'Python'] (splits on each space)

# Limit splits
text = "one:two:three:four"
print(text.split(":", 2))        # ['one', 'two', 'three:four'] (max 2 splits)
print(text.rsplit(":", 1))       # ['one:two:three', 'four'] (split from right, once)

# Practical: parse a file path
path = "/data/bronze/customers/2026/06/data.parquet"
parts = path.split("/")
filename = parts[-1]            # "data.parquet"
year = parts[-3]                # "2026"

splitlines()

# Split on line breaks (
, 
, 
)
text = "Line 1
Line 2
Line 3"
print(text.splitlines())   # ['Line 1', 'Line 2', 'Line 3']

# Useful for processing multi-line API responses or log files
log_text = """2026-06-15 ERROR disk full
2026-06-15 WARNING slow query
2026-06-15 ERROR timeout"""
lines = log_text.splitlines()
errors = [line for line in lines if "ERROR" in line]
print(errors)  # ['2026-06-15 ERROR disk full', '2026-06-15 ERROR timeout']

join()

# join() — the REVERSE of split(). Combines a list into a string
words = ['Hello', 'World', 'Python']
print(" ".join(words))         # "Hello World Python"
print(", ".join(words))        # "Hello, World, Python"
print(" → ".join(words))       # "Hello → World → Python"
print("".join(words))          # "HelloWorldPython" (no separator)

# Practical: build a CSV row
values = ["Naveen", "30", "Toronto"]
csv_row = ",".join(values)     # "Naveen,30,Toronto"

# Build a SQL IN clause
ids = ["101", "102", "103"]
sql = f"SELECT * FROM customers WHERE id IN ({', '.join(ids)})"
print(sql)  # SELECT * FROM customers WHERE id IN (101, 102, 103)

partition() and rpartition()

# partition() — splits into exactly 3 parts: before, separator, after
text = "name=Naveen"
before, sep, after = text.partition("=")
print(before)   # "name"
print(after)     # "Naveen"

# Useful for parsing key=value pairs
config_line = "database_host=server.database.windows.net"
key, _, value = config_line.partition("=")
print(f"{key}: {value}")  # "database_host: server.database.windows.net"

# rpartition — splits from the RIGHT (useful for file extensions)
filename = "sales.report.2026.csv"
name, _, ext = filename.rpartition(".")
print(name)  # "sales.report.2026"
print(ext)   # "csv"

String Methods — Checking

isdigit(), isnumeric(), isdecimal()

# All three check for "number-like" strings, with subtle differences
print("42".isdigit())      # True
print("42".isnumeric())    # True
print("42".isdecimal())    # True

print("3.14".isdigit())    # False (has a decimal point)
print("-42".isdigit())     # False (has a minus sign)

# For data validation, the safest approach:
def is_valid_number(s):
    try:
        float(s)
        return True
    except ValueError:
        return False

print(is_valid_number("3.14"))   # True
print(is_valid_number("-42"))    # True
print(is_valid_number("abc"))    # False

isalpha(), isalnum(), isspace()

print("Hello".isalpha())     # True (only letters)
print("Hello1".isalpha())    # False (has a digit)
print("Hello1".isalnum())    # True (letters + digits)
print("   ".isspace())       # True (only whitespace)
print("Hello World".isalpha())  # False (space is not a letter)

isupper(), islower(), istitle()

print("HELLO".isupper())       # True
print("hello".islower())       # True
print("Hello World".istitle())  # True (Title Case)
print("Hello world".istitle())  # False (lowercase 'w')

String Concatenation and Repetition

# Concatenation with +
first = "Hello"
second = "World"
result = first + " " + second    # "Hello World"

# Repetition with *
line = "=" * 40                  # "========================================"
print(line)
header = "-" * 20 + " REPORT " + "-" * 20
print(header)                    # "-------------------- REPORT --------------------"

Performance: Why + in a Loop Is Bad

# BAD — creates a new string object on EVERY iteration (O(n²))
result = ""
for i in range(10000):
    result += str(i) + ","    # 10,000 new string objects created!

# GOOD — use join() (O(n))
result = ",".join(str(i) for i in range(10000))    # ONE string created at the end

# GOOD — use a list, then join
parts = []
for i in range(10000):
    parts.append(str(i))
result = ",".join(parts)

# Why is + in a loop bad?
# Strings are immutable. Each += creates a NEW string:
#   Iteration 1: "" + "0," → new string "0,"
#   Iteration 2: "0," + "1," → new string "0,1,"
#   Iteration 3: "0,1," + "2," → new string "0,1,2,"
#   ... copying all previous characters every time → O(n²) time

Strings and Unicode (Encoding)

UTF-8 and Why It Matters for Data Engineers

# Python 3 strings are Unicode by default — they can hold ANY character
name_hindi = "नवीन"
name_japanese = "プログラミング"
emoji = "🐍"
print(f"{name_hindi} {name_japanese} {emoji}")  # Works perfectly

# But FILES on disk use ENCODINGS — how characters are stored as bytes
# UTF-8: most common, handles all Unicode, variable-length (1-4 bytes per char)
# ASCII: English only, 1 byte per char, subset of UTF-8
# Latin-1 (ISO-8859-1): Western European, 1 byte per char
# Windows-1252: Similar to Latin-1, common in legacy Windows systems

# Common data engineering problem:
# A CSV from a European client uses Latin-1 encoding, but your pipeline
# expects UTF-8 → UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9

# Fix: specify the correct encoding when reading
with open("european_data.csv", "r", encoding="latin-1") as f:
    content = f.read()

# Or in PySpark:
# df = spark.read.option("encoding", "ISO-8859-1").csv("european.csv")

encode() and decode()

# String → Bytes: encode()
text = "Café"
utf8_bytes = text.encode("utf-8")      # b'Café' (é = 2 bytes in UTF-8)
latin_bytes = text.encode("latin-1")   # b'Café' (é = 1 byte in Latin-1)
print(len(utf8_bytes))                  # 5 bytes
print(len(latin_bytes))                 # 4 bytes

# Bytes → String: decode()
recovered = utf8_bytes.decode("utf-8")   # "Café"
# utf8_bytes.decode("ascii")             # UnicodeDecodeError! é is not ASCII

# Handling errors
recovered = utf8_bytes.decode("ascii", errors="replace")   # "Caf??" — replaces bad chars
recovered = utf8_bytes.decode("ascii", errors="ignore")    # "Caf" — drops bad chars

Strings in Data Engineering

Building SQL Queries

# Use f-strings for dynamic SQL (but NEVER with user input — SQL injection risk!)
table = "silver.customers"
date_filter = "2026-01-01"
columns = ["customer_id", "first_name", "city"]

sql = f"""
SELECT {', '.join(columns)}
FROM {table}
WHERE created_date >= '{date_filter}'
ORDER BY customer_id
"""
print(sql)

# For user input, ALWAYS use parameterized queries:
# cursor.execute("SELECT * FROM users WHERE name = ?", (user_input,))

Parsing File Paths

# Use pathlib (modern) or os.path (legacy) for file paths — not string slicing
from pathlib import Path

path = Path("/data/bronze/customers/2026/06/data.parquet")
print(path.name)       # "data.parquet"
print(path.stem)       # "data"
print(path.suffix)     # ".parquet"
print(path.parent)     # PosixPath('/data/bronze/customers/2026/06')
print(path.parts)      # ('/', 'data', 'bronze', 'customers', '2026', '06', 'data.parquet')

# Build paths safely (handles OS-specific separators)
output = Path("/data") / "silver" / "customers" / "output.parquet"
print(output)  # /data/silver/customers/output.parquet

Cleaning Data Strings

# Common cleaning operations for data engineering
def clean_string(value):
    if value is None:
        return None
    return (value
        .strip()                          # Remove whitespace
        .replace(" ", " ")             # Replace non-breaking space
        .replace("  ", " ")               # Replace tabs with space
        .replace("  ", " ")              # Collapse double spaces
    )

# Standardize phone numbers
def clean_phone(phone):
    if phone is None:
        return None
    digits = "".join(c for c in phone if c.isdigit())
    if len(digits) == 10:
        return f"({digits[:3]}) {digits[3:6]}-{digits[6:]}"
    return digits

print(clean_phone("(416) 555-1234"))   # "(416) 555-1234"
print(clean_phone("416.555.1234"))     # "(416) 555-1234"
print(clean_phone("416-555-1234"))     # "(416) 555-1234"

Common Mistakes

  1. Forgetting strings are immutablename.upper() returns a NEW string. If you do not assign it (name = name.upper()), the original is unchanged and the result is lost.
  2. Using + for string concatenation in loops — creates a new string every iteration (O(n²)). Use join() or a list of parts instead.
  3. Not using raw strings for file paths and regex"C: ew" contains a newline character. Use r"C: ew" or "C:\new".
  4. Confusing find() and index()find() returns -1 when not found (safe). index() raises ValueError (crashes). Use find() when “not found” is a normal case.
  5. Not specifying encoding when reading files — the default is UTF-8. If the file uses Latin-1 or Windows-1252, you get UnicodeDecodeError. Always know your source encoding.
  6. Using split(” “) instead of split()split() with no argument splits on ANY whitespace and handles multiple spaces. split(" ") splits on each single space, leaving empty strings for multiple spaces.

Interview Questions

Q: Why are strings immutable in Python? A: Immutability enables strings to be used as dictionary keys (they are hashable), allows Python to intern and reuse identical strings for memory efficiency, makes strings thread-safe without locks, and prevents accidental modification of shared string references.

Q: What is the difference between find() and index()? A: Both search for a substring and return its position. find() returns -1 when the substring is not found. index() raises a ValueError. Use find() when absence is a normal case and index() when absence is an error.

Q: How do f-strings work and why are they preferred? A: f-strings (prefix with f) embed Python expressions inside {} within a string. They are evaluated at runtime and support formatting specifiers (:.2f, :,, :>20). They are preferred over .format() and % because they are more readable, faster, and support any valid Python expression inline.

Q: What is string slicing and how does the step parameter work? A: Slicing extracts a portion of a string: text[start:stop:step]. start is inclusive, stop is exclusive, step controls direction and interval. text[::-1] reverses the string. text[::2] takes every other character. Slicing never raises IndexError — out-of-range indices are silently handled.

Q: Why is string concatenation with + slow in a loop? A: Strings are immutable, so each += creates a new string object, copying all previous characters. For N iterations, this copies 1 + 2 + 3 + … + N characters = O(n²) time. Using join() builds the final string in one operation = O(n) time.

Q: What is UTF-8 and why does encoding matter? A: UTF-8 is a variable-length encoding that represents all Unicode characters using 1-4 bytes. It matters because files from different systems use different encodings (UTF-8, Latin-1, Windows-1252). Reading a Latin-1 file as UTF-8 causes UnicodeDecodeError. Always specify the correct encoding when reading external files.

Wrapping Up

Strings are the most common data type in data engineering — every file path, SQL query, API response, and log message is a string. Mastering indexing, slicing, f-strings, and the essential methods (split, join, strip, replace, find) makes your code cleaner and faster. Understanding immutability prevents subtle bugs. And knowing about encoding prevents the dreaded UnicodeDecodeError that hits every data engineer eventually.

Previous in this series: Variables, Data Types, and Type Conversion

Next in this series: Lists — Indexing, Slicing, Sorting, and Methods


Naveen Vuppula is a Senior Data Engineering Consultant and app developer based in Ontario, Canada. He writes about Python, SQL, AWS, Azure, and everything data engineering at DriveDataScience.com.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top
Share via
Copy link