What is a string?
A string is an ordered, immutable sequence of Unicode code points. Each character has a position, and the whole thing is locked down the moment it's created. That immutability shapes everything about how you use strings in Python.
Think of a string like a printed book page. You can read any word by its position, but you can't erase and rewrite a single letter. You'd have to reprint the whole page. Python strings work the same way.
This has a real consequence: every time you “change” a string, Python creates an entirely new string object. The original is untouched.
1s = "hello"2print(id(s)) # memory address, e.g. 14023456834s = s + " world" # creates a new string object5print(id(s)) # different address, new object6print(s) # "hello world"
Try to mutate a character directly and Python refuses:
1s = "hello"2s[0] = "H" # Python won't allow this
Strings are also sequences, so everything you know about Python sequences applies: len(), indexing, slicing, iteration, and the in operator all work out of the box.
1s = "Python"23print(len(s)) # 64print("y" in s) # True5print("x" in s) # False67for char in s:8 print(char, end=" ") # P y t h o n
Creating strings
Python gives you four ways to write string literals, each with a practical reason to exist.
Single and double quotes
They're interchangeable. Pick one style and be consistent. The only reason to mix them is to avoid backslash escapes:
1name = 'Alice'2msg = "Hello, world!"34# Avoid escaping by choosing the right outer quote5quote = "It's a beautiful day" # single quote inside, no escape6html = 'She said "hello"' # double quote inside, no escape
Triple quotes
For multi-line strings. Newlines and indentation are preserved literally. Useful for SQL queries, HTML templates, and docstrings.
1sql = """2 SELECT id, name3 FROM users4 WHERE active = true5 ORDER BY name6"""78haiku = '''9An old silent pond...10A frog jumps into the pond.11Splash! Silence again.12'''
Raw strings
Prefix with r to treat backslashes literally. Essential for Windows file paths and regex patterns.
1# Without raw, backslash is an escape sequence2path = "C:\Users\alice\Documents" # ugly, error-prone34# With raw, backslash is just a backslash5path = r"C:\Users\alice\Documents" # clean67# Regex without raw requires four backslashes for \d8import re9pattern = r"\d+" # matches one or more digits
f-strings (Python 3.6+)
If you're building strings with dynamic content and you're not using f-strings, you're doing it the hard way. They're the clearest and fastest option Python has.
1name = "Alice"2age = 3034# Old approaches (avoid these)5greeting = "Hello, " + name + ". You are " + str(age) + " years old."6greeting = "Hello, %s. You are %d years old." % (name, age)78# f-string (use this)9greeting = f"Hello, {name}. You are {age} years old."1011# Expressions work inside {}12print(f"Next year: {age + 1}")13print(f"Uppercase: {name.upper()}")14print(f"Pi: {3.14159:.2f}") # format spec inside the braces
str() constructor
Converts any object to its string representation. You'll use this when joining non-string values into a string.
1str(42) # "42"2str(3.14) # "3.14"3str(True) # "True"4str([1, 2, 3]) # "[1, 2, 3]"5str(None) # "None"67# join() requires strings, so convert first8nums = [1, 2, 3, 4, 5]9print(", ".join(str(n) for n in nums))
Indexing & slicing
Every character has two addresses: a positive index from the front (starting at 0) and a negative index from the back (starting at -1). Slicing lets you extract any subsequence with [start:stop:step].
Adjust the sliders to see how start, stop, and step interact. Selected characters highlight in real time.
string[0:5]startdefaults to 0 (beginning of string)stopdefaults to end, excluded from the resultstepdefaults to 1, use -1 to reverseGotcha
s[start:stop] includes start but excludes stop. So s[0:5] gives characters at indices 0, 1, 2, 3, 4. Never 5.
Patterns you'll use constantly
1s = "Hello, World!"23s[:5] # "Hello" first 5 characters4s[-6:] # "orld!" last 6 characters5s[7:12] # "World" middle slice6s[::-1] # "!dlroW ,olleH" reverse the string7s[::2] # "Hlo ol!" every other character8s[1:-1] # "ello, World" strip first and last910# Negative indices count from the end11s[-1] # "!" last character12s[-3] # "l" third from end
Out-of-range slice indices are silently clamped, no IndexError. But out-of-range single indices (like s[99] on a short string) do raise IndexError.
String methods
Python ships 40+ string methods. Below are the ones you'll actually reach for in production, organized by purpose, with examples and the gotchas that trip people up.
str.find(sub[, start[, end]]) -> intReturns the lowest index where sub is found, or -1 if not found. Optional start/end limit the search range.
Like Ctrl+F in your browser. Find the first occurrence and return its position.
Gotcha
Use find() when not finding is acceptable. Use index() when the value must exist. index() raises ValueError if not found.
Remember: strings are immutable. Every method returns a new string. None modify in place. Writing s.upper() without assigning the result does nothing to s.
Real-world patterns
The string operations you'll reach for constantly in production.
Clean and normalize user input
User-submitted text arrives dirty. Strip whitespace and normalize case before storing or comparing.
1raw_username = " Alice "2raw_email = "Alice@EXAMPLE.COM "34username = raw_username.strip().lower() # "alice"5email = raw_email.strip().lower() # "alice@example.com"67# Always normalize before comparing8if username == "alice":9 print("Found user")
Parse delimited data
When you receive a flat string of comma-separated values, split and strip each field.
1line = "Alice, 30, engineer , New York"23fields = [f.strip() for f in line.split(",")]4# ['Alice', '30', 'engineer', 'New York']56name, age, role, city = fields7print(f"{name} is a {role} in {city}")
Build URLs and paths safely
1base = "https://api.example.com/"2endpoint = "/users/profile/"34# Strip slashes before joining to avoid double-slash bugs5url = f"{base.rstrip('/')}/{endpoint.strip('/')}"6# "https://api.example.com/users/profile/"78# For file paths use pathlib, not string joins9from pathlib import Path10path = Path("/home/alice") / "documents" / "report.pdf"11print(path) # /home/alice/documents/report.pdf
Truncate text for display
1def truncate(text: str, max_len: int = 100) -> str:2 if len(text) <= max_len:3 return text4 return text[:max_len - 3].rstrip() + "..."56print(truncate("The quick brown fox jumped over the lazy dog", 25))7# "The quick brown fox..."89# Word-aware wrapping from the standard library10import textwrap11print(textwrap.fill("The quick brown fox jumped over the lazy dog", width=25))
Filter files by extension
1filenames = ["report.pdf", "data.csv", "photo.png", "notes.txt", "image.jpg"]23# endswith() with a tuple checks any of several suffixes at once4images = [f for f in filenames if f.endswith((".jpg", ".png", ".gif", ".webp"))]5# ['photo.png', 'image.jpg']67urls = ["https://example.com", "http://old.com", "ftp://files.com"]8secure = [u for u in urls if u.startswith("https://")]9# ['https://example.com']
Performance
There's one performance trap that catches every Python developer at least once: building a string by concatenating inside a loop.
1# Don't do this2result = ""3for word in words:4 result += word + " " # creates a brand new string every time
Because strings are immutable, each += copies the entire existing string plus the new piece into a new object. For a loop over n words, you copy 1 char, then 2, then 3, and so on. The total work is proportional to n². Fine for 10 items, genuinely painful at 10,000.
O(n²): copies the entire string on every iteration
O(n): builds the string once at the end
The fix is str.join(). Collect all pieces in a list, then join once. Python pre-calculates the total length and allocates memory in a single pass.
1# Do this instead (O(n))2parts = []3for word in words:4 parts.append(word)5result = " ".join(parts)67# Even better: list comprehension + join8result = " ".join(word.strip() for word in words)910# For large-scale text assembly: io.StringIO11from io import StringIO12buf = StringIO()13for chunk in data_stream:14 buf.write(chunk)15result = buf.getvalue()
Performance note
CPython has an optimization that sometimes makes +=reuse the buffer when the string has only one reference. Don't rely on it. The optimization disappears with aliasing and isn't guaranteed. Use join() and never think about it again.