subtotal = price * quantity
print(f"Subtotal: ${subtotal:.2f}")The calculation has a name and the formatting step is easy to verify.
Learn with diagrams, code, systems and practical examples.
Create, combine, inspect, and format text.
Applications constantly work with names, messages, file paths, API data, and user-facing text.
A string is a sequence of characters. You can create strings with quotes, access characters by position, call helpful methods, and format values into readable messages.
Run this example first. Do not change several things at once; confirm the basic behavior, then experiment.
name = "Maya"
items = 3
message = f"{name} has {items} items"
print(message)
print(name.lower()) A string is a sequence of characters. You can create strings with quotes, access characters by position, call helpful methods, and format values into readable messages.
Maya has 3 items
mayaThe example is intentionally small. Focus on the chapter’s main idea before adding extra syntax or framework code.
Building long messages with many + operators and forgetting spaces or type conversions.
Use f-strings for most readable value interpolation.
These are deliberately small. If you can complete them without copying the example, you are ready to continue.
Prepared deep chapter · Core Python
A program often needs to work with text before it does anything complicated. A customer name, a file path, a search term, a status message, and a line from a log file are all text. In Python, text is represented by the str type. A string is a sequence of Unicode characters, which means it can hold ordinary letters, digits, punctuation, spaces, and text from many writing systems.
The beginner goal is not to memorize dozens of string methods. It is to learn one reliable workflow: identify the text you have, decide what you need to read or change, produce a new string, then inspect the result. Strings are immutable, so operations that look like changes actually return new string values.
A string literal is text written directly in your program. Python accepts single or double quotes. Pick the style that makes the text easiest to read.
customer = "Maya"
status = 'ready'
print(customer)
print(status) Maya
readyThe quotes mark where the string begins and ends; they are not normally part of the value itself. A value such as "1975" is still a string because it is quoted, even though its characters look like a number.
If you need arithmetic, use a numeric type. If you need text such as an order code, postcode, phone-number label, or identifier, a string may be the better model even when it contains digits.
You can put a single quote inside a double-quoted string, or a double quote inside a single-quoted string. When the same quote character must appear inside the string, escape it with a backslash.
message = "Maya's order is ready."
quoted = 'The status is "ready".'
escaped = 'It\'s ready.'
print(message)
print(quoted)
print(escaped) Maya's order is ready.
The status is "ready".
It's ready.Escape sequences can also represent special characters. For example, \n creates a newline and \t represents a tab.
receipt = "Item: Notebook\nQty: 2"
print(receipt) Item: Notebook
Qty: 2A raw string begins with r, for example r"C:\Users\Maya". Most backslashes are treated literally, which can be useful for Windows-style paths or regular-expression patterns. Raw strings still have syntax rules, so they are not a universal “ignore every backslash” mode.
You can combine strings with +, but repeated concatenation quickly becomes hard to read when values are mixed into a sentence. For user-facing messages, an f-string usually makes the relationship between the sentence and its values clearer.
name = "Maya"
items = 3
message = f"Hello, {name}. You have {items} items."
print(message) Hello, Maya. You have 3 items.The expressions inside the braces are evaluated and converted for display. Keep f-string expressions small enough that the sentence remains easy to scan. If a calculation is complicated, compute it first, give it a meaningful name, then format the result.
subtotal = price * quantity
print(f"Subtotal: ${subtotal:.2f}")The calculation has a name and the formatting step is easy to verify.
print(f"Subtotal: ${price * quantity:.2f}")This is valid, but large expressions inside a message can hide business logic.
Strings are sequences. Each character has a position called an index. Python indexes start at zero, so the first character is position 0. Negative indexes count from the end.
code = "SV-2048"
print(code[0])
print(code[-1])
print(code[:2])
print(code[3:]) S
8
SV
2048A slice uses a start position and an end position, and the end position is excluded. That rule is useful because text[:n] contains at most the first n characters.
For word = "python", predict word[1:4]. The answer is "yth": indexes 1, 2, and 3 are included; index 4 is the stopping point.
Immutable means the string object cannot have one of its characters replaced in place. This fails:
name = "maya"
name[0] = "M" TypeError: 'str' object does not support item assignmentCreate a new string instead. Most string methods follow the same pattern: they return a new value and leave the original string unchanged.
name = "maya"
formatted = name.title()
print(name)
print(formatted) maya
MayaThis behavior matters when you clean input. Calling email.strip() without storing or using the returned value does not rewrite email.
Python strings have many methods, but beginners can solve a surprising number of tasks with a small set. Learn them by the problem they solve rather than by alphabetical order.
| Need | Method | Example result |
|---|---|---|
| Remove surrounding whitespace | strip() | " Maya ".strip() → "Maya" |
| Normalize case for comparison | lower() / casefold() | "READY".lower() → "ready" |
| Replace known text | replace() | "SV-2048".replace("SV", "APP") |
| Break text into pieces | split() | "red,blue".split(",") |
| Join pieces into text | join() | " / ".join(["api", "users"]) |
raw_tags = " cloud,python, beginner "
tags = [tag.strip() for tag in raw_tags.split(",")]
summary = " | ".join(tags)
print(summary) cloud | python | beginnerYou do not need to memorize the whole method catalog. Start with the transformation you need, inspect the returned value, and add methods only when a real task requires them.
String comparisons are exact unless you transform the values first. That means "READY" == "ready" is False. If the business rule says case should not matter, make that rule explicit.
entered = " READY "
normalized = entered.strip().casefold()
if normalized == "ready":
print("Continue") ContinueDo not automatically lowercase text just because you can. Usernames, identifiers, product codes, file paths, and external systems can have their own case rules. Normalize only when the domain says values should be compared that way.
text.upper() returns a new string. Assign it or use it directly.
+"Total: " + 5 raises TypeError. Convert deliberately or use an f-string.
The slice end is excluded. Predict positions before slicing user-facing identifiers.
split() without checking the input contractReal text may contain missing fields, extra separators, or unexpected whitespace. Validate structure before indexing pieces.
Case folding is useful only when the domain says case should not carry meaning.
A checkout form collects a customer name and a city with accidental surrounding spaces. We want a readable label and a normalized city for comparison.
raw_name = " maya chen "
raw_city = " Seattle "
order_id = 2048
name = raw_name.strip().title()
city = raw_city.strip()
city_key = city.casefold()
label = f"Order #{order_id} — {name} — {city}"
print(label)
print(city_key) Order #2048 — Maya Chen — Seattle
seattleEach transformation has one reason: strip() removes accidental outer whitespace, title() prepares a display name for this example, casefold() creates a separate comparison key, and the f-string builds the final label. Keeping display text and comparison text as separate variables makes the intent easier to inspect.
Real names do not all follow the same capitalization rules. The title() call above is a teaching example, not a universal name-cleaning policy. In production software, preserve user-provided names unless your product has a carefully defined normalization requirement.
raw = " python ", create a cleaned value without changing the original variable. Print both values."SubjectVision"[0:7] and "SubjectVision"[-6:], then run the code."red, green,blue", split it into pieces, remove surrounding whitespace from each piece, and join the cleaned values with " | ".casefold().str type.Next chapter: Booleans and Comparisons — where values become yes/no decisions for control flow.
Trace practice · predict before you run
Real programs rarely receive perfectly formatted text. A support tool might receive one line such as " INC-2048 | Payment failed | HIGH ". Before writing a clever parser, trace a few small transformations and inspect what each one produces.
The goal is not to memorize strip() or split(). The goal is to see the state change: raw text → cleaned text → pieces → validated fields → comparison key.
Before revealing the trace, decide which spaces should disappear and which capitalization should remain visible to the user. Should the summary Payment failed be lowercased just because the priority is compared case-insensitively?
raw = " INC-2048 | Payment failed | HIGH "The original input is useful evidence when debugging. Do not destroy it before you know what transformations are required.
cleaned = raw.strip()Now the leading and trailing spaces are gone, but the spaces around each separator still exist.
parts = cleaned.split("|")The result is a list of three strings. Each field still needs its own surrounding whitespace removed.
fields = [part.strip() for part in parts]The visible summary keeps its capitalization. Cleaning does not mean normalizing everything indiscriminately.
if len(fields) != 3: ...Check the structure before unpacking. Otherwise malformed input can raise ValueError at an unexpected place.
ticket_id, summary, priority = fields
priority_key = priority.casefold()The display value remains HIGH; the separate comparison key becomes high.
raw = " INC-2048 | Payment failed | HIGH "
cleaned = raw.strip()
fields = [part.strip() for part in cleaned.split("|")]
if len(fields) != 3:
print("Invalid ticket format")
else:
ticket_id, summary, priority = fields
priority_key = priority.casefold()
print(ticket_id)
print(summary)
print(priority_key) INC-2048
Payment failed
highIf the input is "INC-2048 Payment failed HIGH", splitting on | produces only one field. Checking len(fields) before unpacking turns a confusing exception into an explicit validation decision.
String processing is easier to debug when each transformation has one job. strip() removes unwanted outer whitespace. split() changes one string into pieces. A length check validates the expected shape. casefold() creates a comparison key only where the domain says case should not matter.
This separation also protects user-facing text. The support summary is not forced to lowercase merely because the priority is compared case-insensitively. That is the same principle used in the chapter's shipping-label example: preserve display data when possible and create a separate normalized value for comparison.
Mini-project · one new idea at a time
Suppose an order-import line has three logical fields: an order ID, a customer label, and a free-text delivery note. The note itself might contain a | character. A plain split("|") would split every separator and create too many pieces.
For " ORD-1042 | Priority Customer | leave at side | gate ", how many pieces would plain split("|") produce? It produces four. But the data contract says there should be only three fields.
Good to know: split(separator, maxsplit) can limit how many splits Python performs. With maxsplit=2, Python splits only the first two separators, so everything after the second separator stays in the final field.
raw = " ORD-1042 | Priority Customer | leave at side | gate "
cleaned = raw.strip()
parts = [part.strip() for part in cleaned.split("|", 2)]
if len(parts) != 3:
print("Invalid order format")
else:
order_id, customer_label, note = parts
comparison_key = customer_label.casefold()
print(order_id)
print(customer_label)
print(comparison_key)
print(note) ORD-1042
Priority Customer
priority customer
leave at side | gateNotice the two separate jobs: customer_label preserves the display text, while comparison_key is normalized only for comparison. The note is cleaned around the edges but otherwise preserved. That keeps a formatting decision from silently destroying user-entered information.
maxsplit=2 does not guarantee the separators exist. If the line contains only one separator, the result still has fewer than three fields. Keep the length check before unpacking.
priority CUSTOMER. Predict which printed value changes and which normalized key stays equivalent.Invalid order format without raising an unpacking exception." INC-2050 | Login issue | urgent ". Predict the display values and the normalized priority before running it.Reasoning lab · predict before you reveal
Each prompt asks you to predict what Python will do before reading the explanation. Focus on the rule behind the result rather than memorizing one line of code.
strip() change the original string?raw = " Maya "
raw.strip()
print(raw) Prediction: the spaces are still there. Strings are immutable, so strip() returns a new string. Store or use that returned value when you want the cleaned text.
+?count = 3
print("Items: " + count) Prediction: Python raises TypeError. Use an f-string such as f"Items: {count}" when the goal is readable output.
word = "python"
print(word[1:4]) Prediction: yth. The start index is included and the stop index is excluded.
split() fail?ticket = "INC-2048 Payment failed HIGH"
id_, summary, priority = ticket.split("|") Prediction: the split returns one piece, so unpacking three variables raises ValueError. Validate the number of fields before unpacking external text.
join()?parts = ["api", "users", "42"]
print("/".join(parts)) Prediction: api/users/42. The separator string owns join(); the list supplies the pieces to combine.
replace() edit text in place?code = "SV-2048"
updated = code.replace("SV", "APP")
print(code)
print(updated) Prediction: the original remains SV-2048; the new value is APP-2048. This is another consequence of string immutability.
display_priority = "HIGH"
priority_key = display_priority.casefold() Prediction: keep both when the interface should display HIGH but comparisons should ignore case. Normalize only the field whose domain rules require normalization.
title() be risky for real names?name = "o'connor"
print(name.title()) Prediction: a transformation may produce output that looks plausible, but capitalization rules for real names are not universal. Preserve user-provided names unless the product has a defined normalization policy.
Bookmark this · choose by intent
Start from the job you need to do, not from a method name you happen to remember. The safe pattern is: choose the operation, keep the original value when it still matters, then verify the result.
| Your intent | Start with | Good small example | Common misuse to avoid |
|---|---|---|---|
| Remove accidental whitespace at the edges | strip() | clean = raw.strip() | Calling raw.strip() and then continuing to use raw as though it changed. |
| Compare human-entered text without case differences | casefold() on a separate key | priority_key = priority.casefold() | Replacing the display value when the interface should preserve the user's original capitalization. |
| Split a delimited record but preserve free text at the end | split(separator, maxsplit) | | Unpacking the split result before confirming that external input has the expected number of fields. |
| Combine known pieces with a separator | separator.join(parts) | path = "/".join(parts) | Calling parts.join("/"); the separator string owns join(). |
| Build readable output from text plus values | an f-string | label = f"Order {order_id} — {name}" | Hiding large calculations inside the braces instead of naming the calculation first. |
| Replace one known substring according to a defined rule | replace() | new_code = code.replace("SV-", "APP-", 1) | Using replacement as a parser when the input can have several shapes or ambiguous occurrences. |
| Read a fixed position or range from a stable format | indexing or slicing | prefix = code[:2] | Using magic positions for free-form user text whose structure is not guaranteed. |
Good to know · Text and bytes
Most beginner Python programs can stay with str. You need to think about bytes when text crosses a boundary such as a file format, a network protocol, or a library that expects encoded data. An encoding is a rule for turning text characters into bytes. UTF-8 is a widely used encoding that can represent Unicode text.
message = "café"
encoded = message.encode("utf-8")
decoded = encoded.decode("utf-8")
print(message)
print(encoded)
print(decoded) Expected result: the first and third lines are the same human-readable text. The middle line is a bytes value, which Python displays with a leading b. The exact byte escapes depend on the characters in the text.
Will len("é") always equal len("é".encode("utf-8"))? No. The first counts Unicode characters in the Python string; the second counts encoded bytes. Different encodings and characters can use different byte counts.
Do not mix str and bytes as if they were interchangeable. If an API expects bytes, encode deliberately. If you receive bytes and need text, decode with the encoding that actually produced those bytes.
UnicodeDecodeError. Do not fix that by guessing encodings blindly; use the file, protocol, or API contract that defines the encoding.
"hello" as UTF-8, decode it again, and verify that the round trip preserves the original text."café". Explain why they can differ without saying that one result is “wrong.”