String Advanced
Lesson 06 covered the basics. Now let's dig deeper. What does immutability really mean? What lesser-known but powerful string operations exist? How are Chinese characters stored in computers? After this lesson, your understanding of strings will reach a new level.
1. String Immutability
We mentioned it briefly in Lesson 06's FAQ — once created, strings can't be modified. Let's see what this means concretely.
text = "Hello"
# ❌ Can't modify like this — this would error
# text[0] = "h" # TypeError: 'str' object does not support item assignment
To change the first letter to lowercase, create a new string:
text = "Hello"
new_text = "h" + text[1:] # Create a new string
print(new_text) # hello
print(text) # Hello — original unchanged
Benefits of Immutability
# Safe shared references
a = "Python"
b = a # b and a point to the same string
# No matter what you do to a, b is unaffected
a = a.upper()
print(a) # PYTHON
print(b) # Python — b still holds the original value
The Cost of Immutability
Each "modification" creates a new object. Concatenating many strings in a loop is slow:
# ❌ Not recommended: string concatenation in a loop
result = ""
for i in range(10000):
result += str(i) + ","
# This creates 10000 temporary string objects
# ✅ Recommended: collect in a list, then join()
parts = []
for i in range(10000):
parts.append(str(i))
result = ",".join(parts)
+= is perfectly fine.
Example: Simulating String Methods (Difficulty ⭐⭐)
# Implement a "capitalize" function
def my_capitalize(text):
if not text:
return text
first = text[0].upper()
rest = text[1:].lower()
return first + rest
print(my_capitalize("hello WORLD")) # Hello world
print(my_capitalize("python")) # Python
# Implement a "replace" function
def my_replace(text, old, new):
parts = text.split(old) # Split by old, get the parts
return new.join(parts) # Rejoin with new
print(my_replace("one, two, one", "one", "1")) # 1, two, 1
2. String Query Methods
Beyond find() and count() from Lesson 06, here are more useful methods.
startswith() and endswith()
# Check string start/end
url = "https://www.example.com"
print(url.startswith("https")) # True — HTTPS protocol
print(url.endswith(".com")) # True — .com domain
print(url.endswith(".org")) # False
# Can check multiple at once
print(url.endswith((".com", ".org", ".cn"))) # True — matches .com
Practical Uses
# File extension check
filename = "report.pdf"
if filename.endswith((".pdf", ".doc", ".docx")):
print("Document file")
elif filename.endswith((".jpg", ".png", ".gif")):
print("Image file")
elif filename.endswith((".py", ".js", ".html")):
print("Code file")
else:
print("Other file")
# URL protocol check
link = "ftp://files.example.com"
if link.startswith(("http://", "https://")):
print("Web link")
elif link.startswith("ftp://"):
print("FTP link")
Output:
Document file
FTP link
removeprefix() and removesuffix() (Python 3.9+)
These were added in Python 3.9 to cleanly remove prefixes or suffixes:
url = "https://www.example.com"
# Remove protocol prefix
domain = url.removeprefix("https://")
print(domain) # www.example.com
# Remove domain suffix
site_name = url.removesuffix(".com")
print(site_name) # https://www.example
# If the string doesn't start/end with the given text, return it unchanged
print(url.removeprefix("ftp://")) # https://www.example.com (unchanged)
url[len("https://"):] or url.split("://", 1)[1]. removeprefix() and removesuffix() make this more intuitive.
3. Advanced split() Usage
Lesson 06 covered basic splitting. Let's explore more powerful variants.
Limiting the Number of Splits
# Limit splits with the maxsplit parameter
text = "one,two,three,four,five"
print(text.split(",", 1)) # ['one', 'two,three,four,five']
print(text.split(",", 2)) # ['one', 'two', 'three,four,five']
print(text.split(",", 3)) # ['one', 'two', 'three', 'four,five']
rsplit(): Split from Right to Left
text = "one,two,three,four,five"
print(text.rsplit(",", 1)) # ['one,two,three,four', 'five']
print(text.rsplit(",", 2)) # ['one,two,three', 'four', 'five']
splitlines(): Split by Lines
multiline = """First line
Second line
Third line"""
lines = multiline.splitlines()
print(lines) # ['First line', 'Second line', 'Third line']
# Keep line breaks
lines = multiline.splitlines(True)
print(lines) # ['First line\n', 'Second line\n', 'Third line']
partition() and rpartition()
partition() not only splits but also retains the separator position:
text = "user@example.com"
# Split by @ into three parts: (left, separator, right)
parts = text.partition("@")
print(parts) # ('user', '@', 'example.com')
# If separator not found, returns (original_string, '', '')
print("hello".partition("@")) # ('hello', '', '')
# rpartition searches from the right
path = "/home/user/documents/file.txt"
last_slash = path.rpartition("/")
print(last_slash) # ('/home/user/documents', '/', 'file.txt')
partition() is better than split() for "extraction" scenarios — it tells you exactly what each of the three parts is.
Example: URL Parsing (Difficulty ⭐⭐)
# Parse a URL using partition
url = "https://www.example.com:8080/path/page.html?name=test&page=1"
# Extract protocol
proto, _, rest = url.partition("://")
print(f"Protocol: {proto}") # https
# Extract host and path
host_part, _, path_and_query = rest.partition("/")
print(f"Host: {host_part}") # www.example.com:8080
# Extract port
host, _, port = host_part.partition(":")
print(f"Domain: {host}") # www.example.com
print(f"Port: {port}") # 8080
# Extract path and query
path, _, query = path_and_query.partition("?")
print(f"Path: /{path}") # /path/page.html
print(f"Query: {query}") # name=test&page=1
Output:
Protocol: https
Host: www.example.com:8080
Domain: www.example.com
Port: 8080
Path: /path/page.html
Query: name=test&page=1
4. String Alignment and Padding
Python provides methods to align strings within a specified width — left, right, or center.
text = "Python"
print(text.ljust(10)) # 'Python ' (left-aligned, padded with spaces on the right)
print(text.rjust(10)) # ' Python' (right-aligned, padded on the left)
print(text.center(10)) # ' Python ' (centered, padded on both sides)
# Can specify a fill character
print(text.ljust(10, "-")) # 'Python----'
print(text.rjust(10, "-")) # '----Python'
print(text.center(10, "-")) # '--Python--'
zfill(): Zero Padding
Commonly used for numeric IDs:
print("42".zfill(5)) # 00042
print("-42".zfill(5)) # -0042 — negative sign comes first
print("3.14".zfill(8)) # 00003.14
# Generate 3-digit IDs
for i in range(1, 11):
filename = f"photo_{str(i).zfill(3)}.jpg"
print(filename, end=" ")
Output:
photo_001.jpg photo_002.jpg photo_003.jpg photo_004.jpg photo_005.jpg
photo_006.jpg photo_007.jpg photo_008.jpg photo_009.jpg photo_010.jpg
Example: Generating an Aligned Report (Difficulty ⭐⭐)
# Align a table using rjust and ljust
items = [
("Apple", 5.0, 3),
("Banana", 3.5, 5),
("Milk", 12.0, 2),
]
# Headers
header_name = "Item".ljust(8)
header_price = "Price".rjust(6)
header_qty = "Qty".rjust(4)
header_total = "Total".rjust(6)
print(f"{header_name}{header_price}{header_qty}{header_total}")
print("-" * 28)
# Data rows
for name, price, qty in items:
total = price * qty
name_col = name.ljust(8)
price_col = f"{price:.1f}".rjust(6)
qty_col = str(qty).rjust(4)
total_col = f"{total:.1f}".rjust(6)
print(f"{name_col}{price_col}{qty_col}{total_col}")
Output:
Item Price Qty Total
────────────────────────────
Apple 5.0 3 15.0
Banana 3.5 5 17.5
Milk 12.0 2 24.0
5. Character Encoding: ord() and chr()
Every character in a computer corresponds to a number (code point). ord() gets a character's numeric encoding; chr() does the reverse.
# View Unicode code points
print(ord("A")) # 65
print(ord("中")) # 20013
print(ord("❤")) # 10084
# Get character from code point
print(chr(65)) # A
print(chr(20013)) # 中
print(chr(10084)) # ❤
# Generate all uppercase letters
for code in range(ord("A"), ord("Z") + 1):
print(chr(code), end=" ")
# Output: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
What Is Unicode
Simply put, Unicode is a global character encoding standard — it assigns a unique number to every character from every writing system worldwide (Chinese, English, Arabic, Japanese, etc.). Python 3 stores all strings internally as Unicode, so it natively supports multiple languages.
# Python 3 strings natively support Unicode
text = "Hello, 你好, مرحبا, こんにちは"
print(text) # All displayed correctly
print(len(text)) # Character count
# You can also represent characters with \u escapes
print("\u0048") # H
print("\u4e2d\u6587") # 中文 (Chinese)
str (bytes) and unicode (text), causing frequent encoding errors. Python 3 unified them — all strings are Unicode, treating Chinese and English equally. This is one of Python 3's best design decisions.
Encoding and Decoding
While Python internally uses Unicode, file storage and network transmission use bytes. So you need encoding (encode) and decoding (decode):
text = "Hello, 世界"
# Encoding: string → bytes
utf8_bytes = text.encode("utf-8")
print(utf8_bytes) # b'\xe4\xbd\xa0\xe5\xa5\xbd...'
print(len(utf8_bytes)) # Number of bytes in UTF-8
gbk_bytes = text.encode("gbk")
print(len(gbk_bytes)) # Number of bytes in GBK
# Decoding: bytes → string
decoded = utf8_bytes.decode("utf-8")
print(decoded) # Hello, 世界
# If encode/decode don't match, error occurs
# gbk_bytes.decode("utf-8") # UnicodeDecodeError!
encoding="utf-8" when opening the file and it should work.
6. String Formatting Supplement
Beyond f-strings, Python has two older formatting methods. While f-strings are the modern standard, you may encounter these in older code.
format() Method (Python 3.0+)
# By position
print("My name is {}, I'm {} years old.".format("Alice", 18))
# My name is Alice, I'm 18 years old.
# By name
print("My name is {name}, I'm {age} years old.".format(name="Bob", age=22))
# My name is Bob, I'm 22 years old.
# Number formatting
print("Pi: {:.3f}".format(3.14159))
# Pi: 3.142
% Formatting (Python 2 Era)
# You may see this in older code
print("My name is %s, I'm %d years old." % ("Alice", 25))
# My name is Alice, I'm 25 years old.
print("Pi: %.2f" % 3.14159)
# Pi: 3.14
format() is still useful for "delayed formatting" (define a template now, fill in values later). The % formatting is just for recognition when you encounter it; don't use it in new code.
Common Use Cases
- Batch file renaming: Use
endswith()to filter file types,zfill()for uniform numbering. - URL parsing: Use
partition()andremoveprefix()to extract protocol, domain, and path. - CSV data processing: Use
split()withmaxsplitto control split count, avoiding comma interference within fields. - Log formatting: Use
center()andljust()for aligned log output. - Encoding conversion: Handle text files from different sources with
encode()anddecode(). - Data validation: Use
startswith()andendswith()to check formats (email, phone, links).
❓ FAQ
bytes type. This makes string operations intuitive — len("中") is always 1 on any system. The trade-off is you need manual encode/decode for file or network I/O.
⚠️ Q: When should I use split() vs partition()? A: For simple splitting, use split(). When you need the separator itself, use partition(). partition() always returns three elements (left, separator, right), handy for tuple unpacking: left, sep, right = text.partition(":"). split() returns a list of variable length. If the delimiter appears only once and you need content on both sides, partition() is the best choice.
Q: What's the difference between ljust() and f-string's {value:10}? A: They produce the same result — text.ljust(10) is equivalent to f"{text:<10}". The f-string version is more flexible (combines alignment with number formatting), while the method call is more intuitive. It's a matter of preference — pick one and stay consistent. Alignment symbols in f-strings: < left, > right, ^ center: f"{text:^10}".
Q: What's the difference between UTF-8 and GBK? Why does my Python file sometimes save with garbled characters? A: UTF-8 is the international standard — 1-4 bytes per character, universal. GBK is a Chinese standard (simplified Chinese) — 1-2 bytes per character, only supports Chinese and English. If your Python file is saved as GBK but contains Japanese or Arabic text, it will error or display garbled. Solution: save all Python files as UTF-8 — Python 3 defaults to UTF-8.📖 Summary
- Strings are immutable: each "modification" creates a new object; use
join()over+=for large concatenations - Query methods:
startswith()/endswith()check start/end; supports tuple arguments for multiple checks - Python 3.9+:
removeprefix()/removesuffix()for clean prefix/suffix removal - Advanced splitting:
split(maxsplit=N)limit,rsplit()from right,splitlines()by lines partition()retains separator info, ideal for structured parsing- Alignment:
ljust()/rjust()/center()/zfill(); custom fill characters supported ord()gets character Unicode code point;chr()converts back- Python 3 strings use Unicode internally; encode/decode with
encode()/decode(); UTF-8 recommended
📝 Exercises
-
Basic (Difficulty ⭐): Given
files = ["report.pdf", "photo.jpg", "script.py", "notes.txt", "index.html"], useendswith()to filter:- All image files (.jpg, .png, .gif)
- All document files (.pdf, .doc, .docx, .txt)
- All code files (.py, .js, .html, .css)
-
Intermediate (Difficulty ⭐⭐): Write a "batch file renamer." Given
photos = ["IMG_1.jpg", "IMG_2.jpg", ..., "IMG_12.jpg"], rename them tophoto_001.jpg,photo_002.jpg, ...,photo_012.jpg. Hint: Usepartition()to split filename and extension; usezfill()for zero-padding. -
Challenge (Difficulty ⭐⭐⭐): Write a "simple template engine." Given a template string
templateand a dictionarydata, replace{variable}placeholders with actual values.PYTHONtemplate = "Dear {name}, your order {order_id} has been {status}. Estimated delivery: {delivery_time}." data = { "name": "Alice", "order_id": "20260623001", "status": "shipped", "delivery_time": "3 business days" } # Your code: implement replace_template(template, data) # Output: Dear Alice, your order 20260623001 has been shipped. Estimated delivery: 3 business days.Extra requirement: If a template variable doesn't exist in data, leave
{variable}as-is (don't replace). Hint: Iterate over key-value pairs in data, callingreplace()on the template for each.



