Codetail

Article 2 of 15

Strings

Text as a sequence. Every operation you'll ever need.

30 min read

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.

Python
1s = "hello"
2print(id(s)) # memory address, e.g. 140234568
3
4s = s + " world" # creates a new string object
5print(id(s)) # different address, new object
6print(s) # "hello world"

Try to mutate a character directly and Python refuses:

Python
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.

Python
1s = "Python"
2
3print(len(s)) # 6
4print("y" in s) # True
5print("x" in s) # False
6
7for 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:

Python
1name = 'Alice'
2msg = "Hello, world!"
3
4# Avoid escaping by choosing the right outer quote
5quote = "It's a beautiful day" # single quote inside, no escape
6html = '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.

Python
1sql = """
2 SELECT id, name
3 FROM users
4 WHERE active = true
5 ORDER BY name
6"""
7
8haiku = '''
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.

Python
1# Without raw, backslash is an escape sequence
2path = "C:\Users\alice\Documents" # ugly, error-prone
3
4# With raw, backslash is just a backslash
5path = r"C:\Users\alice\Documents" # clean
6
7# Regex without raw requires four backslashes for \d
8import re
9pattern = 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.

Python
1name = "Alice"
2age = 30
3
4# Old approaches (avoid these)
5greeting = "Hello, " + name + ". You are " + str(age) + " years old."
6greeting = "Hello, %s. You are %d years old." % (name, age)
7
8# f-string (use this)
9greeting = f"Hello, {name}. You are {age} years old."
10
11# 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.

Python
1str(42) # "42"
2str(3.14) # "3.14"
3str(True) # "True"
4str([1, 2, 3]) # "[1, 2, 3]"
5str(None) # "None"
6
7# join() requires strings, so convert first
8nums = [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.

Interactive Slicer
h
0
-11
e
1
-10
l
2
-9
l
3
-8
o
4
-7
5
-6
w
6
-5
o
7
-4
r
8
-3
l
9
-2
d
10
-1
0
5
1
Result
string[0:5]
hello
startdefaults to 0 (beginning of string)
stopdefaults to end, excluded from the result
stepdefaults to 1, use -1 to reverse

Gotcha

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

Python
1s = "Hello, World!"
2
3s[:5] # "Hello" first 5 characters
4s[-6:] # "orld!" last 6 characters
5s[7:12] # "World" middle slice
6s[::-1] # "!dlroW ,olleH" reverse the string
7s[::2] # "Hlo ol!" every other character
8s[1:-1] # "ello, World" strip first and last
9
10# Negative indices count from the end
11s[-1] # "!" last character
12s[-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.

Method Explorer
str.find(sub[, start[, end]]) -> int

Returns 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.

"hello world".find("world")
6

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.

Python
1raw_username = " Alice "
2raw_email = "Alice@EXAMPLE.COM "
3
4username = raw_username.strip().lower() # "alice"
5email = raw_email.strip().lower() # "alice@example.com"
6
7# Always normalize before comparing
8if username == "alice":
9 print("Found user")

Parse delimited data

When you receive a flat string of comma-separated values, split and strip each field.

Python
1line = "Alice, 30, engineer , New York"
2
3fields = [f.strip() for f in line.split(",")]
4# ['Alice', '30', 'engineer', 'New York']
5
6name, age, role, city = fields
7print(f"{name} is a {role} in {city}")

Build URLs and paths safely

Python
1base = "https://api.example.com/"
2endpoint = "/users/profile/"
3
4# Strip slashes before joining to avoid double-slash bugs
5url = f"{base.rstrip('/')}/{endpoint.strip('/')}"
6# "https://api.example.com/users/profile/"
7
8# For file paths use pathlib, not string joins
9from pathlib import Path
10path = Path("/home/alice") / "documents" / "report.pdf"
11print(path) # /home/alice/documents/report.pdf

Truncate text for display

Python
1def truncate(text: str, max_len: int = 100) -> str:
2 if len(text) <= max_len:
3 return text
4 return text[:max_len - 3].rstrip() + "..."
5
6print(truncate("The quick brown fox jumped over the lazy dog", 25))
7# "The quick brown fox..."
8
9# Word-aware wrapping from the standard library
10import textwrap
11print(textwrap.fill("The quick brown fox jumped over the lazy dog", width=25))

Filter files by extension

Python
1filenames = ["report.pdf", "data.csv", "photo.png", "notes.txt", "image.jpg"]
2
3# endswith() with a tuple checks any of several suffixes at once
4images = [f for f in filenames if f.endswith((".jpg", ".png", ".gif", ".webp"))]
5# ['photo.png', 'image.jpg']
6
7urls = ["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.

Python
1# Don't do this
2result = ""
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.

String building: "+=" loop vs join()
n (input size, growing right)operations
str += (loop)

O(n²): copies the entire string on every iteration

"".join()

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.

Python
1# Do this instead (O(n))
2parts = []
3for word in words:
4 parts.append(word)
5result = " ".join(parts)
6
7# Even better: list comprehension + join
8result = " ".join(word.strip() for word in words)
9
10# For large-scale text assembly: io.StringIO
11from io import StringIO
12buf = 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.